Skip to main content

scx_pandemonium/
adaptive.rs

1// PANDEMONIUM ADAPTIVE CONTROL LOOP
2// SINGLE-THREAD CLOSED-LOOP TUNING SYSTEM
3//
4// ONE THREAD: MONITOR LOOP (1-SECOND CONTROL LOOP)
5//   READS THE PER-CPU STATS ARRAY, UNCOLLAPSED, AND BUILDS THE LIVE LOAD
6//   GRAPH: NODES ARE CPUs WEIGHTED BY RUNQUEUE DEPTH, TRAFFIC SHAPE,
7//   CRITICAL SLOWING AND PERSISTENCE; EDGES ARE CPU PAIRS WEIGHTED BY
8//   COUPLING. EVERY MEASURE IS RECOMPUTED FROM THE RAW WINDOW EACH TICK.
9//
10//   EVERY KNOB IS DERIVED, NONE ARE LEARNED. depth -> slice, depth+lag-1 ->
11//   preempt, burstiness -> batch/burst ceilings, Hurst -> rescue threshold,
12//   Bandt-Pompe H -> spill temperature, R_eff -> CoDel equilibrium. Coupling is
13//   MEASURED and not actuated: deriving affinity from it cost 16x on IPC pipe
14//   p50 in the first PRISM pass and was withdrawn. There is no expert set, no loss pathway and no convergence
15//   window: a derived knob is correct this tick, where a learned one was
16//   correct several convergence windows later if the regime held still.
17//
18// BPF PRODUCES HISTOGRAMS, RUST READS AND REACTS. RUST WRITES KNOBS,
19// BPF READS THEM ON THE VERY NEXT SCHEDULING DECISION.
20
21use std::sync::atomic::{AtomicBool, Ordering};
22use std::time::Duration;
23
24use anyhow::Result;
25
26use crate::chaos::{self, Priced, RawWindow};
27use crate::scheduler::{PandemoniumStats, Scheduler};
28use crate::topology::CpuTopology;
29use crate::tuning::{self, detect_regime, scaled_regime_knobs, Regime, HIST_BUCKETS};
30
31// CHAOS WINDOW SIZE. 16 SAMPLES AT 1HZ = 16-SECOND REGIME MEMORY.
32// SIZED FOR BENCH-SCALE RESPONSIVENESS (16-30s iterations) RATHER THAN
33// MINUTE-SCALE DESKTOP STEADY STATE. HVG IS O(N^2) = 256 COMPARISONS
34// PER TICK; BP D=3 GETS 14 LENGTH-3 PATTERNS OVER 6 BUCKETS -- LIMITED
35// RESOLUTION BUT ENOUGH TO DISTINGUISH PERIODIC FROM RANDOM IN ONE
36// BENCH ITERATION.
37const CHAOS_WIN: usize = 16;
38
39// REGIME THRESHOLDS, PROFILES, AND KNOB COMPUTATION LIVE IN tuning.rs
40// (ZERO BPF DEPENDENCIES, TESTABLE OFFLINE)
41
42// THE LIVE LOAD GRAPH
43//
44// Nodes are CPUs weighted by mean runqueue depth; edges are CPU pairs weighted
45// by Pecora-Carroll coupling between their depth series. The chip's electrical
46// graph is a constant and R_eff already prices against it; this is the graph
47// the WORKLOAD forms, which moves every second and which nothing measured.
48//
49// BPF cannot build this. Relating CPU i to CPU j means reading another CPU's
50// state, which per-CPU maps exist to avoid, and the spectral work that follows
51// needs f64 and unbounded loops. This is the half of the boundary that
52// justifies a userspace loop existing at all.
53//
54// EDGES ARE THE MEASUREMENT THAT DECIDES THE HORIZON. If every pair couples
55// identically the graph is complete-and-uniform, there is no structure to
56// exploit, and pricing against it can only reproduce the static case. That is
57// falsifiable from the summary below and is the point of emitting it before
58// anything consumes it.
59pub struct LoadGraph {
60    // Mean runqueue depth per CPU over the last window. NaN where a CPU has
61    // no usable series yet -- absent, not empty.
62    pub node_depth: Vec<f64>,
63    // TRAFFIC SHAPE per CPU (Kim-Jo finite-size-corrected burstiness, [-1, 1]).
64    // -1 perfectly regular / deadline-paced, 0 Poisson / longrun, +1 maximally
65    // bursty / starvation-shaped. This is the continuous form of the property
66    // the three-valued regime enum was approximating for the whole machine.
67    pub node_burst: Vec<Option<chaos::Priced>>,
68    // CRITICAL SLOWING per CPU (lag-1 autocorrelation, [-1, 1]). Rises BEFORE
69    // saturation rather than at window close, which is the one thing no other
70    // signal in this loop does.
71    pub node_slowing: Vec<Option<chaos::Priced>>,
72    // PERSISTENCE per CPU (Veitch-Abry Hurst, [0, 1]). > 0.5 says this CPU's
73    // load pattern tends to continue, which is precisely the "the same task is
74    // likely to return" premise the warm-stay price already assumes.
75    pub node_persist: Vec<Option<chaos::Priced>>,
76    // Upper-triangular coupling, index pair_index(i, j). None where either
77    // series is too short or flat to couple.
78    pub edge: Vec<Option<f64>>,
79    pub n: usize,
80}
81
82fn pair_index(n: usize, i: usize, j: usize) -> usize {
83    // i < j; row-major over the strict upper triangle.
84    debug_assert!(i < j && j < n);
85    i * n - (i * (i + 1)) / 2 + (j - i - 1)
86}
87
88impl LoadGraph {
89    pub fn build<const N: usize>(depth_win: &[RawWindow<N>]) -> LoadGraph {
90        let n = depth_win.len();
91        let node_depth = depth_win
92            .iter()
93            .map(|w| {
94                if w.is_empty() {
95                    f64::NAN
96                } else {
97                    chaos::mean(w)
98                }
99            })
100            .collect();
101        // NODE ATTRIBUTES. Each answers a different question about the same
102        // series, which is why they derive different knobs: burstiness is
103        // SHAPE, lag-1 is DIRECTION, Hurst is DURATION. A learner searching one
104        // loss signal could not separate them; measured, they do not need to be.
105        // PRICED, NOT GATED. Each answers wherever its arithmetic is defined and
106        // carries how much the window is worth; the derivations below scale their
107        // effect by that confidence. A knob moves a little on thin evidence and
108        // fully on a complete window, instead of not at all and then all at once.
109        let node_burst = depth_win
110            .iter()
111            .map(chaos::kim_jo_burstiness_priced)
112            .collect();
113        let node_slowing = depth_win.iter().map(chaos::lag1_autocorr_priced).collect();
114        let node_persist = depth_win
115            .iter()
116            .map(chaos::veitch_abry_hurst_priced)
117            .collect();
118
119        let mut edge = vec![None; n * (n.saturating_sub(1)) / 2];
120        for i in 0..n {
121            for j in (i + 1)..n {
122                edge[pair_index(n, i, j)] = chaos::pecora_carroll(&depth_win[i], &depth_win[j]);
123            }
124        }
125        LoadGraph {
126            node_depth,
127            node_burst,
128            node_slowing,
129            node_persist,
130            edge,
131            n,
132        }
133    }
134
135    // DERIVE PER-CPU SLICE PRESSURE FROM THE GRAPH.
136    //
137    // This is what the graph is FOR. A CPU carrying a deep queue needs a
138    // shorter slice so the queue drains; a CPU near-empty needs a longer one so
139    // it stops paying context-switch cost for work that is not contending. MWU
140    // could only ever search for one slice for the whole machine, so it was
141    // solving the average of a distribution it could not see -- the average
142    // being wrong for every CPU that is not at the mean.
143    //
144    // DERIVED, NOT LEARNED. There is no loss signal, no convergence window and
145    // no weight vector: depth is measured and the slice follows from it this
146    // tick. codel_eq_ns has worked this way since v5.16.0 (computed from R_eff,
147    // explicitly excluded from MWU); this extends that pattern to the knobs a
148    // learner was guessing at.
149    //
150    // Returns one knob set per CPU, seeded from `base` so every field the graph
151    // does not own is carried through untouched.
152    pub fn derive_percpu_knobs(
153        &self,
154        base: &crate::tuning::TuningKnobs,
155    ) -> Vec<crate::tuning::TuningKnobs> {
156        // Reference depth: the mean over CPUs that actually reported. A CPU at
157        // the reference keeps the base slice exactly, so a uniformly-loaded
158        // machine is bit-identical to the pre-graph behavior.
159        let present: Vec<f64> = self
160            .node_depth
161            .iter()
162            .copied()
163            .filter(|d| d.is_finite())
164            .collect();
165        let refd = if present.is_empty() {
166            0.0
167        } else {
168            present.iter().sum::<f64>() / present.len() as f64
169        };
170        self.node_depth
171            .iter()
172            .enumerate()
173            .map(|(i, d)| {
174                let mut k = *base;
175                // Absent series or a flat machine: carry the base through.
176                if !d.is_finite() || refd <= f64::EPSILON {
177                    return k;
178                }
179                // Deeper than reference -> shorter slice, and inversely.
180                // Clamped to [1/2, 2x] so one anomalous tick cannot hand a CPU
181                // a slice far outside anything the regime profile intended.
182                let ratio = (refd / (d + refd).max(f64::EPSILON) * 2.0).clamp(0.5, 2.0);
183                k.slice_ns = ((base.slice_ns as f64) * ratio) as u64;
184                k.preempt_thresh_ns = ((base.preempt_thresh_ns as f64) * ratio) as u64;
185
186                // BURSTINESS -> BATCH AND BURST CEILINGS.
187                // Bursty traffic (B > 0) arrives in clumps with quiet between,
188                // so a batch task wants a LONGER ceiling: it will not be
189                // contending most of the time, and cutting it short pays
190                // context-switch cost for contention that is not there. Paced
191                // traffic (B < 0) is the opposite -- steady arrivals mean a
192                // long ceiling holds the CPU against work that is genuinely
193                // queued. Scaled +/-50% across the full [-1, 1] range.
194                if let Some(b) = self.node_burst.get(i).and_then(|v| *v) {
195                    // weighted() collapses toward the neutral factor 1.0 as
196                    // confidence falls, so a thin window nudges rather than swings.
197                    let f = Priced {
198                        value: 1.0 + 0.5 * b.value.clamp(-1.0, 1.0),
199                        confidence: b.confidence,
200                    }
201                    .weighted(1.0);
202                    k.batch_slice_ns = ((base.batch_slice_ns as f64) * f) as u64;
203                    k.burst_slice_ns = ((base.burst_slice_ns as f64) * f) as u64;
204                }
205
206                // CRITICAL SLOWING -> PREEMPT WINDOW, AHEAD OF THE BURST.
207                // r1 rising toward 1 means perturbations on this CPU are
208                // persisting instead of damping: it is approaching saturation
209                // and has not got there yet. Tightening the preempt window now
210                // is the only place in this loop that acts on a prediction
211                // rather than on a completed loss. Only the positive half
212                // matters -- an oscillating CPU (r1 < 0) is already recovering.
213                if let Some(r1) = self.node_slowing.get(i).and_then(|v| *v) {
214                    if r1.value > 0.0 {
215                        let tighten = Priced {
216                            value: 1.0 - 0.4 * r1.value.clamp(0.0, 1.0),
217                            confidence: r1.confidence,
218                        }
219                        .weighted(1.0);
220                        k.preempt_thresh_ns = ((k.preempt_thresh_ns as f64) * tighten) as u64;
221                    }
222                }
223
224                // PERSISTENCE -> CoDel RESCUE THRESHOLD.
225                // H > 0.5 says this CPU's load pattern tends to continue, so a
226                // queue that is deep now will likely still be deep: rescue
227                // sooner. H < 0.5 is mean-reverting, where a deep queue is
228                // likely transient and rescuing early only scatters a task that
229                // was about to be served anyway. Centered on H = 0.5 so an
230                // uncorrelated CPU keeps the base exactly.
231                if let Some(h) = self.node_persist.get(i).and_then(|v| *v) {
232                    let f = Priced {
233                        value: (1.0f64 - (h.value.clamp(0.0, 1.0) - 0.5)).clamp(0.5, 1.5),
234                        confidence: h.confidence,
235                    }
236                    .weighted(1.0);
237                    k.codel_thresh_ns = ((base.codel_thresh_ns as f64) * f) as u64;
238                }
239
240                // AFFINITY IS NOT DERIVED, AND THE REASON IS MEASURED.
241                //
242                // This briefly read coupling and bound strongly above a
243                // midpoint, on the argument that runqueues moving together mean
244                // tasks sharing data. That argument was reasoning, not
245                // measurement, and the first full PRISM pass on v5.18.0 put a
246                // number against it: baseline_gate reported every IPC primitive
247                // regressed on every arm, worst pipe p50 20 -> 336us at drift
248                // 1.04x, which is 16x with the environment ruled out.
249                //
250                // IPC is precisely a two-task ping-pong with tightly coupled
251                // queue depths, so the derivation fired hardest exactly where
252                // the damage landed. Whether coupling should raise affinity,
253                // lower it, or not touch it is now an open question with
254                // evidence on one side only, so the knob keeps the base value
255                // until something measures the direction. mean_coupling() stays
256                // as telemetry -- the reading is still wanted, the actuation is
257                // not.
258                k
259            })
260            .collect()
261    }
262
263    // Is there structure worth pricing against? Returns (available_edges,
264    // mean, min, max) over the edges that computed. A spread near zero means
265    // the coupling matrix is flat and the live graph adds nothing over the
266    // static one.
267    pub fn edge_summary(&self) -> (usize, f64, f64, f64) {
268        let vals: Vec<f64> = self.edge.iter().filter_map(|e| *e).collect();
269        if vals.is_empty() {
270            return (0, 0.0, 0.0, 0.0);
271        }
272        let sum: f64 = vals.iter().sum();
273        let mut lo = f64::INFINITY;
274        let mut hi = f64::NEG_INFINITY;
275        for v in &vals {
276            if *v < lo {
277                lo = *v;
278            }
279            if *v > hi {
280                hi = *v;
281            }
282        }
283        (vals.len(), sum / vals.len() as f64, lo, hi)
284    }
285}
286
287// SLEEP PATTERN BUCKETS: CLASSIFY IO-WAIT VS IDLE WORKLOADS
288const SLEEP_BUCKETS: usize = 4;
289
290// MONITOR LOOP
291
292// 1-SECOND CONTROL LOOP. READS BPF HISTOGRAMS, COMPUTES P99,
293// DETECTS WORKLOAD REGIME, TIGHTENS/RELAXES KNOBS.
294// RUNS ON THE MAIN THREAD.
295pub fn monitor_loop(
296    sched: &mut Scheduler,
297    shutdown: &'static AtomicBool,
298    verbose: bool,
299    nr_cpus: u64,
300    phi_scale: Option<u64>,
301) -> Result<bool> {
302    // HOTPLUG POLL STATE: the online count as last observed. The poll below
303    // re-derives topology on change AND refreshes the Rust-local tau_ns --
304    // without that refresh, a hotplug that changes tau without flipping the
305    // regime left MWU computing every knob off pre-hotplug tau until a regime
306    // change happened to land (the stale-MWU-tau gap).
307    let mut last_online = CpuTopology::online_cpu_count();
308    let mut prev = PandemoniumStats::default();
309    let mut prev_hist = [[0u64; HIST_BUCKETS]; 3];
310    let mut prev_sleep = [0u64; SLEEP_BUCKETS];
311    let mut regime = Regime::Mixed;
312
313    // CHAOS RAW WINDOWS. idle_pct DRIVES REGIME DETECTION; wakeup_rate
314    // DRIVES THE MWU CHAOS-TRANSITION PATHWAY (BANDT-POMPE IS ORDINAL,
315    // SO ABSOLUTE RATE SCALE DOES NOT MATTER -- THE PATTERN DOES).
316    let mut idle_win: RawWindow<CHAOS_WIN> = RawWindow::new();
317    let mut wake_win: RawWindow<CHAOS_WIN> = RawWindow::new();
318    // PER-CPU RUNQUEUE-DEPTH WINDOWS: THE NODES OF THE LIVE LOAD GRAPH.
319    // One series per CPU, each the interval-mean depth on that CPU, which is
320    // what rq_depth_sum/rq_depth_samples differenced gives. Until v5.18.0
321    // there was exactly ONE series in this whole loop -- a system-wide integer
322    // percentage -- which is why a coupling measure had nothing to couple.
323    let mut depth_win: Vec<RawWindow<CHAOS_WIN>> =
324        (0..nr_cpus as usize).map(|_| RawWindow::new()).collect();
325    let mut prev_percpu: Vec<PandemoniumStats> = Vec::new();
326    let mut prev_bp_h: f64 = 0.0;
327    let chaos_count = chaos::ChaosCounter::new();
328    let mut prev_lambda_above: bool = false;
329    // READ CURRENT tau SNAPSHOT FROM THE BPF-SIDE KNOB MAP. main.rs WROTE IT
330    // ONCE AT TOPOLOGY DETECT; THE ADAPTIVE LOOP RE-READS SO TAU-SCALED REGIME
331    // KNOBS AGREE WITH TAU-SCALED BPF INIT AT FIRST TICK AND EVERY REGIME CHANGE.
332    let mut tau_ns = sched.read_tuning_knobs().topology_tau_ns;
333    let mut pending_regime = regime;
334    let mut regime_hold: u32 = 0;
335    let mut light_ticks: u64 = 0;
336    let mut mixed_ticks: u64 = 0;
337    let mut heavy_ticks: u64 = 0;
338    // STABILITY SCORE IS A WEAK PRE-CHAOS STEADY-STATE PROXY, KEPT FOR
339    // TELEMETRY GATING ONLY -- THE REAL STEADY-STATE GATE
340    // IS `quiesce.frozen` BELOW. DO NOT WIRE stability_score INTO THE
341    // FREEZE DECISION (TWO COMPETING "AM I STEADY" SIGNALS = A BUG).
342    let mut stability_score: u32 = 0;
343    let mut tick_counter: u64 = 0;
344
345    // QUIESCENCE GATE + ADAPTIVE-RARITY RETUNE STATE. The gate latches
346    // a "frozen" flag from HVG-lambda + RQA-DET + MWU convergence and
347    // the loop then skips the expensive MWU retune + knob write. When
348    // not frozen, the retune interval stretches on sub-threshold deltas.
349    let mut quiesce = tuning::QuiescenceState::new();
350    let mut retune_interval: u32 = tuning::RETUNE_INTERVAL_BASE;
351    let mut ticks_since_retune: u32 = 0;
352    let mut frozen_ticks: u64 = 0;
353    // GRAPH SUMMARY ACCUMULATORS. The scheduler writes its own telemetry file
354    // at shutdown rather than relying on a bench to capture stdout: three
355    // separate benches discarded it three different ways (no recording dir, a
356    // path that bypassed the marker writer, and stdout sent to DEVNULL), and
357    // the coupling reading went unread each time. A producer that owns its own
358    // artifact cannot be defeated by a consumer's plumbing.
359    let mut g_tick: u64 = 0;
360    let mut g_edge_ticks: u64 = 0;
361    let mut g_edges_total: u64 = 0;
362    let mut g_mean_sum: f64 = 0.0;
363    let mut g_spread_sum: f64 = 0.0;
364    let mut g_spread_max: f64 = 0.0;
365
366    // APPLY INITIAL REGIME. scaled_regime_knobs RETURNS topology_tau_ns/codel_eq_ns=0;
367    // OVERLAY THE LIVE BPF VALUES SO THE FIRST WRITE DOESN'T CLOBBER WHAT
368    // write_topology_fields() PUT IN THE MAP. Mirrors the regime-change path at line 230.
369    let live = sched.read_tuning_knobs();
370    let mut rk = scaled_regime_knobs(regime, nr_cpus, tau_ns);
371    rk.topology_tau_ns = tau_ns;
372    rk.codel_eq_ns = live.codel_eq_ns;
373    sched.write_tuning_knobs(&rk)?;
374    // SEED THE MWU BASELINE WITH THE LIVE PHI EQUILIBRIUM AT TICK 0. new()
375    // BUILT mwu FROM scaled_regime_knobs (codel_eq_ns=0); set_baseline IS
376    // OTHERWISE ONLY CALLED ON A REGIME CHANGE. WITHOUT THIS SEED THE SOJOURN
377    // FLOOR (tuning.rs) FALLS BACK TO THE DEAD 4ms CONSTANT FOR ANY RUN WHOSE
378    // REGIME NEVER CHANGES (E.G. A STEADY MIXED BENCH), SO THE PHI-COHERENT
379    // FLOOR WOULD NEVER ENGAGE.
380    // COMMIT-ON-CHANGE BASELINE: the last knob set actually written to
381    // the BPF map. Updated here at init, on every regime-change write,
382    // and on every conditional write. MWU-owned fields drive the diff.
383    let mut last_written_knobs = rk;
384
385    while !shutdown.load(Ordering::Relaxed) && !sched.exited() {
386        crate::watchdog::LOOP_HEARTBEAT.fetch_add(1, Ordering::Relaxed);
387        std::thread::sleep(Duration::from_secs(1));
388
389        if CpuTopology::poll_hotplug(sched, nr_cpus as usize, phi_scale, &mut last_online) {
390            tau_ns = sched.read_tuning_knobs().topology_tau_ns;
391        }
392
393        // THE ARRAY, UNCOLLAPSED. `stats` below is the same fold this loop
394        // always consumed; `percpu` is the spatial dimension that used to be
395        // discarded at the boundary. Both come from ONE syscall.
396        let percpu = sched.read_stats_percpu();
397        let stats = Scheduler::fold_stats(&percpu);
398        let cur_hist = sched.read_wake_lat_hist();
399        let cur_sleep = sched.read_sleep_hist();
400
401        // WRAP GUARD: BPF RELOAD, UEI RECOVERY, OR HOTPLUG CAN RESET KERNEL-SIDE
402        // CUMULATIVE COUNTERS WHILE RUST'S PREV STILL HOLDS OLD VALUES. WITHOUT
403        // THIS CHECK, WRAPPING_SUB PRODUCES A GARBAGE POSITIVE DELTA THAT POISONS
404        // P99 AND FEEDS NONSENSE TO MWU. RESET BASELINE AND SKIP THE TICK.
405        let mut wrapped = stats.nr_dispatches < prev.nr_dispatches;
406        if !wrapped {
407            'wrap: for tier in 0..3 {
408                for b in 0..HIST_BUCKETS {
409                    if cur_hist[tier][b] < prev_hist[tier][b] {
410                        wrapped = true;
411                        break 'wrap;
412                    }
413                }
414            }
415        }
416        if !wrapped {
417            for i in 0..SLEEP_BUCKETS {
418                if cur_sleep[i] < prev_sleep[i] {
419                    wrapped = true;
420                    break;
421                }
422            }
423        }
424        if wrapped {
425            log_warn!("WRAP DETECTED: BASELINE RESET, SKIPPING ADAPTIVE UPDATE");
426            prev = stats;
427            prev_hist = cur_hist;
428            prev_sleep = cur_sleep;
429            continue;
430        }
431
432        // COMPUTE DELTAS
433        let delta_d = stats.nr_dispatches.wrapping_sub(prev.nr_dispatches);
434        let delta_idle = stats.nr_idle_hits.wrapping_sub(prev.nr_idle_hits);
435        let delta_shared = stats.nr_shared.wrapping_sub(prev.nr_shared);
436        let delta_preempt = stats.nr_preempt.wrapping_sub(prev.nr_preempt);
437        let delta_keep = stats.nr_keep_running.wrapping_sub(prev.nr_keep_running);
438        let delta_parks = stats.nr_osc_park.wrapping_sub(prev.nr_osc_park);
439        let delta_wake_sum = stats.wake_lat_sum.wrapping_sub(prev.wake_lat_sum);
440        let delta_wake_samples = stats.wake_lat_samples.wrapping_sub(prev.wake_lat_samples);
441        let delta_hard = stats.nr_hard_kicks.wrapping_sub(prev.nr_hard_kicks);
442        let delta_soft = stats.nr_soft_kicks.wrapping_sub(prev.nr_soft_kicks);
443        let delta_steal = stats.nr_steal.wrapping_sub(prev.nr_steal);
444        let delta_enq_wake = stats.nr_enq_wakeup.wrapping_sub(prev.nr_enq_wakeup);
445        let delta_enq_requeue = stats.nr_enq_requeue.wrapping_sub(prev.nr_enq_requeue);
446        let delta_rescue = stats
447            .nr_overflow_rescue
448            .wrapping_sub(prev.nr_overflow_rescue);
449        // CROSS-DOMAIN SCATTER (PATHWAY 6 INPUT). PLACEMENT-SIDE PATHS ONLY:
450        // XDOM_SEL_* + XDOM_ENQ_T1/T2 (INDICES 0..6). THE PHI-CORRECT WORK-
451        // CONSERVATION PATHS XDOM_STEAL (6) AND XDOM_STEP5 (7) ARE EXCLUDED --
452        // PENALIZING THEM WOULD MAKE MWU FIGHT THE BPF'S DELIBERATE REBALANCING.
453        // saturating_sub ABSORBS A COUNTER RESET (BPF RELOAD) AS 0, NO GARBAGE.
454        let wake_avg_us = if delta_wake_samples > 0 {
455            delta_wake_sum / delta_wake_samples / 1000
456        } else {
457            0
458        };
459
460        // PER-PATH LATENCY
461        let d_idle_sum = stats.wake_lat_idle_sum.wrapping_sub(prev.wake_lat_idle_sum);
462        let d_idle_cnt = stats.wake_lat_idle_cnt.wrapping_sub(prev.wake_lat_idle_cnt);
463        let d_kick_sum = stats.wake_lat_kick_sum.wrapping_sub(prev.wake_lat_kick_sum);
464        let d_kick_cnt = stats.wake_lat_kick_cnt.wrapping_sub(prev.wake_lat_kick_cnt);
465        let lat_idle_us = if d_idle_cnt > 0 {
466            d_idle_sum / d_idle_cnt / 1000
467        } else {
468            0
469        };
470        let lat_kick_us = if d_kick_cnt > 0 {
471            d_kick_sum / d_kick_cnt / 1000
472        } else {
473            0
474        };
475        let delta_reenq = stats.nr_reenqueue.wrapping_sub(prev.nr_reenqueue);
476
477        // L2 CACHE AFFINITY DELTAS
478        let dl2_hb = stats.nr_l2_hit_batch.wrapping_sub(prev.nr_l2_hit_batch);
479        let dl2_mb = stats.nr_l2_miss_batch.wrapping_sub(prev.nr_l2_miss_batch);
480        let dl2_hi = stats
481            .nr_l2_hit_interactive
482            .wrapping_sub(prev.nr_l2_hit_interactive);
483        let dl2_mi = stats
484            .nr_l2_miss_interactive
485            .wrapping_sub(prev.nr_l2_miss_interactive);
486        let l2_pct_b = if dl2_hb + dl2_mb > 0 {
487            dl2_hb * 100 / (dl2_hb + dl2_mb)
488        } else {
489            0
490        };
491        let l2_pct_i = if dl2_hi + dl2_mi > 0 {
492            dl2_hi * 100 / (dl2_hi + dl2_mi)
493        } else {
494            0
495        };
496
497        let idle_pct = if delta_d > 0 {
498            delta_idle * 100 / delta_d
499        } else {
500            0
501        };
502
503        // COMPUTE HISTOGRAM DELTAS (cur_hist READ AT TOP FOR WRAP GUARD)
504        let mut delta_hist = [[0u64; HIST_BUCKETS]; 3];
505        for tier in 0..3 {
506            for b in 0..HIST_BUCKETS {
507                delta_hist[tier][b] = cur_hist[tier][b] - prev_hist[tier][b];
508            }
509        }
510
511        // COMPUTE P99 PER TIER
512        let tp99_b_ns = tuning::compute_p99_from_histogram(&delta_hist[0]);
513        let tp99_i_ns = tuning::compute_p99_from_histogram(&delta_hist[1]);
514        let tp99_l_ns = tuning::compute_p99_from_histogram(&delta_hist[2]);
515
516        // AGGREGATE P99
517        let mut agg = [0u64; HIST_BUCKETS];
518        for t in 0..3 {
519            for b in 0..HIST_BUCKETS {
520                agg[b] += delta_hist[t][b];
521            }
522        }
523        let p99_ns = tuning::compute_p99_from_histogram(&agg);
524
525        // SLEEP HISTOGRAM DELTAS (cur_sleep READ AT TOP FOR WRAP GUARD)
526        let mut delta_sleep = [0u64; SLEEP_BUCKETS];
527        for i in 0..SLEEP_BUCKETS {
528            delta_sleep[i] = cur_sleep[i] - prev_sleep[i];
529        }
530        let sleep_total: u64 = delta_sleep.iter().sum();
531        let io_pct = if sleep_total > 0 {
532            (delta_sleep[0] + delta_sleep[1]) * 100 / sleep_total
533        } else {
534            0
535        };
536
537        // CHAOS UPDATE: PUSH RAW SAMPLES INTO WINDOWS BEFORE COMPUTING
538        // ANY DERIVED FEATURES. WAKE WINDOW USES THE PER-SECOND DELTA
539        // (delta_enq_wake) RATHER THAN AN INSTANTANEOUS RATE.
540        idle_win.push(idle_pct as f64);
541        wake_win.push(delta_enq_wake as f64);
542
543        // NODE WEIGHTS: MEAN RUNQUEUE DEPTH PER CPU OVER THIS INTERVAL.
544        // Both fields are monotonic, so the interval mean is the ratio of the
545        // two deltas -- the same shape wake_lat_sum/wake_lat_samples is already
546        // consumed with. A CPU whose sample count did not advance contributes
547        // nothing rather than a zero: no samples is not an empty queue.
548        if prev_percpu.len() == percpu.len() {
549            for (i, w) in depth_win.iter_mut().enumerate() {
550                if i >= percpu.len() {
551                    break;
552                }
553                let ds = percpu[i]
554                    .rq_depth_samples
555                    .wrapping_sub(prev_percpu[i].rq_depth_samples);
556                if ds > 0 {
557                    let dd = percpu[i]
558                        .rq_depth_sum
559                        .wrapping_sub(prev_percpu[i].rq_depth_sum);
560                    w.push(dd as f64 / ds as f64);
561                }
562            }
563        }
564        prev_percpu = percpu.clone();
565
566        // CHAOS PRIMITIVES. ONE O(N^2) HVG PASS + ONE O(N^2) RQA PASS
567        // PER WINDOW; BP IS O(N). RQA-DET RUNS ON THE SAME idle_win AS
568        // HVG SO THE QUIESCENCE GATE SEES IDENTICAL SAMPLES. rqa IS
569        // None UNTIL THE WINDOW HAS RQA_MIN_SAMPLES FILLED.
570        let (idle_lambda, _idle_hvg_s) = chaos::hvg_stats(&idle_win);
571        let wake_bp_h = chaos::bandt_pompe_d3(&wake_win);
572
573        // BUILD THE LIVE LOAD GRAPH. Measured and reported before anything
574        // prices against it: if the coupling spread is ~0 the graph is
575        // complete-and-uniform and carries no structure the static topology
576        // does not already have. Nothing downstream consumes it yet -- this
577        // tick is the falsification, not the feature.
578        let graph = LoadGraph::build(&depth_win);
579        let (g_edges, g_mean, g_min, g_max) = graph.edge_summary();
580        // OSCILLATOR POSITION, READ NOT RE-DERIVED. The BPF damped oscillator owns
581        // codel_target_ns; this reads where it currently sits (0.0 floor/tightened,
582        // 1.0 max/relaxed) so the layer can SEE the target it must not fight. The
583        // reader existed with no consumer, which meant the one piece of state that
584        // says whether the BPF has already responded was unobservable from up here.
585        // Reporting only -- the defer gate that would ACT on it is still open.
586        let osc = sched.read_oscillator_state();
587        let osc_pos = osc.position();
588        // The Phi release point the warm-stay and STEP-1 steal actually let a task
589        // leave home at: codel_target plus the nearest-peer hold. Reported beside
590        // the raw position because position() alone cannot say whether the BPF has
591        // genuinely responded -- that comparison is the defer gate, still open.
592        let osc_rel_us = osc.effective_release_ns() / 1000;
593        g_tick += 1;
594        if g_edges > 0 {
595            g_edge_ticks += 1;
596            g_edges_total += g_edges as u64;
597            g_mean_sum += g_mean;
598            let spread = g_max - g_min;
599            g_spread_sum += spread;
600            if spread > g_spread_max {
601                g_spread_max = spread;
602            }
603        }
604        // SPILL-Phi CHAOS->TEMPERATURE BRIDGE: OVERLAID ONTO EVERY KNOB
605        // WRITE BELOW LIKE topology_tau_ns; INERT IN BPF UNTIL THE SPILL
606        // PRICE CONSUMES IT.
607        let bp_delta = wake_bp_h - prev_bp_h;
608        let mean_idle = chaos::mean(&idle_win);
609        let rqa = chaos::rqa_det(&idle_win);
610
611        // CHAOS CROSSING DIAGNOSTIC: BUMP COUNTER ON EITHER GATE FIRING.
612        // chaos_crossing IS ALSO REUSED BELOW AS THE ADAPTIVE-RARITY
613        // "disturbed" SIGNAL -- CAPTURE IT BEFORE prev_lambda_above IS
614        // OVERWRITTEN.
615        let lambda_above = idle_lambda >= chaos::HVG_LAMBDA_CHAOTIC_MIN;
616        let chaos_crossing = (lambda_above && !prev_lambda_above) || bp_delta > 0.10;
617        if chaos_crossing {
618            chaos_count.bump();
619        }
620        prev_lambda_above = lambda_above;
621
622        // DETECT REGIME (CHAOS-DRIVEN + 2-TICK HOLD)
623        let detected = detect_regime(mean_idle, idle_lambda, wake_bp_h);
624
625        let mut regime_changed_this_tick = false;
626        if detected != regime {
627            if detected == pending_regime {
628                regime_hold += 1;
629            } else {
630                pending_regime = detected;
631                regime_hold = 1;
632            }
633            if regime_hold >= 2 {
634                regime = detected;
635                // REFRESH tau IN CASE HOTPLUG/TOPOLOGY CHANGED.
636                // scaled_regime_knobs RETURNS topology_tau_ns/codel_eq_ns=0;
637                // OVERLAY THE LIVE BPF VALUES (BOTH OWNED BY TOPOLOGY LAYER).
638                let live = sched.read_tuning_knobs();
639                tau_ns = live.topology_tau_ns;
640                let mut rk = scaled_regime_knobs(regime, nr_cpus, tau_ns);
641                rk.topology_tau_ns = tau_ns;
642                rk.codel_eq_ns = live.codel_eq_ns;
643                // Regime baseline, spread per CPU by the graph on the same
644                // terms as the retune path below.
645                sched.write_tuning_knobs_percpu(&graph.derive_percpu_knobs(&rk))?;
646                last_written_knobs = rk;
647                regime_changed_this_tick = true;
648                // RESET ONLY THE NEW REGIME'S WEIGHT VECTOR + EDGE STATE
649                // (THE OTHER REGIMES KEEP THEIR LEARNED VECTORS), AND
650                // SNAP THE ADAPTIVE-RARITY INTERVAL BACK TO BASE.
651                retune_interval = tuning::RETUNE_INTERVAL_BASE;
652                ticks_since_retune = 0;
653            }
654        } else {
655            pending_regime = regime;
656            regime_hold = 0;
657        }
658
659        // QUIESCENCE GATE. HVG-lambda in the periodic band + RQA-DET
660        // steady + the active-regime MWU vector converged -> latch
661        // `frozen` and skip the expensive MWU retune + knob write. The
662        // loop still ticks at 1 Hz; the chaos sensors above are the
663        // exit condition for frozen mode. A regime change moves lambda
664        // out of the steady band, so the gate thaws on the same/next
665        // tick -- the two gates compose without conflict.
666        // The freeze gate's third term was MWU convergence. With no learner
667        // there is nothing to converge, so the gate is the chaos band alone.
668        let frozen = quiesce.update(idle_lambda, rqa, true);
669        if frozen {
670            frozen_ticks += 1;
671        }
672
673        // MWU ORCHESTRATOR: UNIFIED KNOB CONTROL
674        // GATED BY !regime_changed_this_tick (a fresh regime already
675        // wrote its baseline) AND !frozen (steady state -- stop the
676        // machinery). When neither gate is set, the adaptive-rarity
677        // counter throttles how often the retune actually fires.
678        if !regime_changed_this_tick && !frozen {
679            ticks_since_retune += 1;
680            if ticks_since_retune >= retune_interval {
681                ticks_since_retune = 0;
682                // KNOBS COME FROM THE REGIME BASELINE, SPREAD BY THE GRAPH.
683                // No expert weights, no loss pathways, no convergence window:
684                // the baseline is a function of regime and tau, and per-CPU
685                // slice pressure is a function of measured queue depth. Both
686                // are computed this tick from this tick's data.
687                let live = sched.read_tuning_knobs();
688                let mut knobs = scaled_regime_knobs(regime, nr_cpus, tau_ns);
689                knobs.topology_tau_ns = live.topology_tau_ns;
690                knobs.codel_eq_ns = live.codel_eq_ns;
691                // COMMIT-ON-CHANGE: only push when a field actually moved. The
692                // BPF side reads the map unsynchronized, so skipping redundant
693                // writes strictly reduces torn-read exposure.
694                let changed = tuning::knobs_differ(&knobs, &last_written_knobs);
695                if changed {
696                    sched.write_tuning_knobs_percpu(&graph.derive_percpu_knobs(&knobs))?;
697                    last_written_knobs = knobs;
698                }
699                let disturbed = chaos_crossing;
700                retune_interval =
701                    tuning::next_retune_interval(retune_interval, !changed, disturbed);
702            }
703        }
704
705        // STABILITY TRACKING
706        let tighten_delta = if chaos_crossing { 1u64 } else { 0u64 };
707        stability_score = tuning::compute_stability_score(
708            stability_score,
709            regime_changed_this_tick,
710            tighten_delta,
711            p99_ns,
712            regime.p99_ceiling(),
713        );
714
715        let p99_us = p99_ns / 1000;
716        let tp99_b = tp99_b_ns / 1000;
717        let tp99_i = tp99_i_ns / 1000;
718        let tp99_l = tp99_l_ns / 1000;
719        let knobs = sched.read_tuning_knobs();
720
721        let sojourn_ms = stats.batch_sojourn_ns / 1_000_000;
722        let sojourn_thresh_ms = knobs.codel_thresh_ns / 1_000_000;
723        let longrun_label = if stats.longrun_mode_active > 0 {
724            " LONGRUN"
725        } else {
726            ""
727        };
728
729        if verbose && tuning::should_print_telemetry(tick_counter, stability_score) {
730            let rqa_disp = rqa.unwrap_or(-1.0);
731            let frozen_disp = if frozen { 1 } else { 0 };
732            println!(
733                "d/s: {:<8} idle: {}% shared: {:<6} preempt: {:<4} keep: {:<4} kick: H={:<4} S={:<4} enq: W={:<4} R={:<4} wake: {}us p99: {}us [B:{} I:{} L:{}] lat_idle: {}us lat_kick: {}us sleep: io={}% slice: {}us batch: {}us reenq: {} sjrn: {}ms/{}ms rescue: {} l2: B={}% I={}% chaos: lam={:.2} H={:.2} det={:.2} x={} frozen: {} (n={}) retune_iv: {} [{}{}] graph: n={} e={} cpl={:.2}/{:.2}/{:.2} osc: {:.2}/{}us",
734                delta_d, idle_pct, delta_shared, delta_preempt, delta_keep,
735                delta_hard, delta_soft, delta_enq_wake, delta_enq_requeue,
736                wake_avg_us, p99_us, tp99_b, tp99_i, tp99_l,
737                lat_idle_us, lat_kick_us,
738                io_pct, knobs.slice_ns / 1000, knobs.batch_slice_ns / 1000,
739                delta_reenq, sojourn_ms, sojourn_thresh_ms,
740                delta_rescue,
741                l2_pct_b, l2_pct_i,
742                idle_lambda, wake_bp_h, rqa_disp, chaos_count.load(),
743                frozen_disp, frozen_ticks, retune_interval,
744                graph.n, g_edges, g_mean, g_min, g_max,
745                osc_pos, osc_rel_us,
746                regime.label(), longrun_label,
747            );
748        }
749
750        sched.log.snapshot(
751            delta_d,
752            delta_idle,
753            delta_shared,
754            delta_preempt,
755            delta_keep,
756            delta_parks,
757            wake_avg_us,
758            delta_hard,
759            delta_soft,
760            lat_idle_us,
761            lat_kick_us,
762            delta_steal,
763        );
764
765        match regime {
766            Regime::Light => light_ticks += 1,
767            Regime::Mixed => mixed_ticks += 1,
768            Regime::Heavy => heavy_ticks += 1,
769        }
770
771        tick_counter += 1;
772        prev_hist = cur_hist;
773        prev_sleep = cur_sleep;
774        prev = stats;
775        prev_bp_h = wake_bp_h;
776    }
777
778    // KNOBS SUMMARY: CAPTURED BY TEST HARNESS FOR ARCHIVE
779    let final_knobs = sched.read_tuning_knobs();
780    let final_stats = sched.read_stats();
781    let l2_total_b = final_stats.nr_l2_hit_batch + final_stats.nr_l2_miss_batch;
782    let l2_total_i = final_stats.nr_l2_hit_interactive + final_stats.nr_l2_miss_interactive;
783    let l2_cum_b = if l2_total_b > 0 {
784        final_stats.nr_l2_hit_batch * 100 / l2_total_b
785    } else {
786        0
787    };
788    let l2_cum_i = if l2_total_i > 0 {
789        final_stats.nr_l2_hit_interactive * 100 / l2_total_i
790    } else {
791        0
792    };
793    // CROSS-DOMAIN SCATTER ATTRIBUTION (PER XDOM_* PATH). scatter_pct IS THE
794    // PLACEMENT-SIDE FRACTION (idx 0..6) PATHWAY 6 ACTS ON; THE PER-PATH COUNTS
795    // ARE THE PERMANENT ATTRIBUTION SURFACED TO THE BENCH SUITE EVERY RUN.
796    let x = &final_stats.nr_cross_domain;
797    let x_scatter: u64 = x[0..6].iter().sum();
798    let x_scatter_pct = if final_stats.nr_dispatches > 0 {
799        x_scatter * 100 / final_stats.nr_dispatches
800    } else {
801        0
802    };
803    // osc_park IS THE OSCILLATOR ENVELOPE'S OWN COLLAPSE DETECTOR: ZERO PARKS
804    // AFTER AN IDLE-HEAVY RUN MEANS THE ENVELOPE NEVER WENT QUIET, WHICH IS THE
805    // MINIMUM-ATTENTION-COLLAPSE FAILURE MODE. IT WAS IN THE STATS STRUCT AND ON
806    // THE TERMINAL, BUT NOT ON THIS LINE, SO IT NEVER REACHED THE BENCH ARCHIVE
807    // OR THE PRISM .prom -- THE IDLE POWER QUESTION HAD NO COUNTER BEHIND IT.
808    // THE COUPLING READING, WRITTEN WHERE A BENCH CANNOT LOSE IT.
809    // edge_ticks == 0 is the falsification: no CPU pair ever produced a usable
810    // coupling value, so the live graph has nodes and no edges and there is
811    // nothing for a spectral price to solve over. That is a real answer and it
812    // must survive to disk to count as one.
813    {
814        let dir = std::path::Path::new("/tmp/pandemonium");
815        let _ = std::fs::create_dir_all(dir);
816        let denom = g_edge_ticks.max(1) as f64;
817        let body = format!(
818            "pandemonium_graph_ticks {}\n\
819             pandemonium_graph_edge_ticks {}\n\
820             pandemonium_graph_edges_mean {:.2}\n\
821             pandemonium_graph_coupling_mean {:.4}\n\
822             pandemonium_graph_coupling_spread_mean {:.4}\n\
823             pandemonium_graph_coupling_spread_max {:.4}\n\
824             pandemonium_chaos_frozen_ticks {}\n\
825             pandemonium_chaos_frozen_fraction {:.4}\n",
826            g_tick,
827            g_edge_ticks,
828            g_edges_total as f64 / denom,
829            g_mean_sum / denom,
830            g_spread_sum / denom,
831            g_spread_max,
832            frozen_ticks,
833            frozen_ticks as f64 / g_tick.max(1) as f64
834        );
835        let path = dir.join(format!("graph-{}.prom", std::process::id()));
836        let _ = std::fs::write(&path, body);
837        println!("[GRAPH] {}", path.display());
838    }
839
840    println!(
841        "[KNOBS] regime={} slice_ns={} batch_ns={} preempt_ns={} mwu={:.3} ticks=L:{}/M:{}/H:{} frozen={} l2_hit=B:{}%/I:{}% cross_domain_scatter_pct={} cross_domain_sel_tight={} cross_domain_sel_sync={} cross_domain_sel_normal={} cross_domain_sel_dfl={} cross_domain_enq_t1={} cross_domain_enq_t2={} cross_domain_steal={} cross_domain_step5={} osc_park={}",
842        regime.label(), final_knobs.slice_ns, final_knobs.batch_slice_ns,
843        final_knobs.preempt_thresh_ns,
844        0.0f64,
845        light_ticks, mixed_ticks, heavy_ticks, frozen_ticks,
846        l2_cum_b, l2_cum_i,
847        x_scatter_pct, x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7],
848        final_stats.nr_osc_park,
849    );
850
851    // READ UEI EXIT REASON
852    let should_restart = sched.read_exit_info();
853    Ok(should_restart)
854}
855
856// THE LIVE LOAD GRAPH SEES STRUCTURE THAT THE STATIC TOPOLOGY CANNOT
857//
858// The static chip graph is a constant: two CPUs in the same cache domain are
859// always "close", whatever the workload does. These build a graph from
860// synthetic per-CPU depth series where the COUPLING is known by construction
861// and the wiring is irrelevant, and require the graph to report it. A build
862// that returned a flat matrix would still produce a plausible telemetry line,
863// which is exactly why the discriminating case is asserted rather than eyeballed.
864#[cfg(test)]
865mod load_graph_tests {
866    use super::*;
867
868    fn lcg(s: &mut u64) -> f64 {
869        *s = s
870            .wrapping_mul(6364136223846793005)
871            .wrapping_add(1442695040888963407);
872        ((*s >> 33) as f64 / (1u64 << 31) as f64) - 0.5
873    }
874
875    fn win(vals: &[f64]) -> RawWindow<CHAOS_WIN> {
876        let mut w = RawWindow::new();
877        for v in vals {
878            w.push(*v);
879        }
880        w
881    }
882
883    #[test]
884    fn pair_index_is_a_bijection_over_the_upper_triangle() {
885        for n in 2..12usize {
886            let mut seen = vec![false; n * (n - 1) / 2];
887            for i in 0..n {
888                for j in (i + 1)..n {
889                    let k = pair_index(n, i, j);
890                    assert!(!seen[k], "n={n} collision at ({i},{j})");
891                    seen[k] = true;
892                }
893            }
894            assert!(seen.iter().all(|s| *s), "n={n} left a hole");
895        }
896    }
897
898    #[test]
899    fn coupled_cpus_read_higher_than_independent_ones() {
900        // CPUs 0 and 1 share a driving signal; CPU 2 runs its own. No cache
901        // topology is involved -- this is structure the static graph cannot
902        // express, which is the entire claim.
903        let mut s = 4242u64;
904        let drive: Vec<f64> = (0..CHAOS_WIN).map(|_| 2.0 + lcg(&mut s)).collect();
905        let mut s2 = 99u64;
906        let indep: Vec<f64> = (0..CHAOS_WIN).map(|_| 2.0 + lcg(&mut s2)).collect();
907        let scaled: Vec<f64> = drive.iter().map(|v| 3.0 * v + 1.0).collect();
908
909        let g = LoadGraph::build(&[win(&drive), win(&scaled), win(&indep)]);
910        let c01 = g.edge[pair_index(3, 0, 1)].expect("0-1 must couple");
911        let c02 = g.edge[pair_index(3, 0, 2)].expect("0-2 must compute");
912        assert!(
913            c01 > c02,
914            "coupled pair {c01} must exceed independent pair {c02}"
915        );
916        assert!(c01 > 0.9, "an affine copy should read as slaved, got {c01}");
917    }
918
919    #[test]
920    fn a_flat_machine_produces_no_edges_not_fake_ones() {
921        // Every CPU pinned at the same depth: no dynamics, nothing to couple.
922        // Reporting perfect synchrony here would make an idle box look maximally
923        // structured, which is the most common state a desktop is in.
924        let g = LoadGraph::build(&[win(&[4.0; CHAOS_WIN]), win(&[4.0; CHAOS_WIN])]);
925        assert_eq!(g.edge_summary().0, 0, "a flat machine must yield no edges");
926    }
927
928    #[test]
929    fn edge_summary_spread_is_the_falsification() {
930        // A uniform-coupling machine has ~zero spread and the live graph adds
931        // nothing over the static one. A mixed machine must not.
932        let mut s = 7u64;
933        let a: Vec<f64> = (0..CHAOS_WIN).map(|_| 2.0 + lcg(&mut s)).collect();
934        let b: Vec<f64> = a.iter().map(|v| 2.0 * v).collect();
935        let mut s2 = 8u64;
936        let c: Vec<f64> = (0..CHAOS_WIN).map(|_| 2.0 + lcg(&mut s2)).collect();
937        let g = LoadGraph::build(&[win(&a), win(&b), win(&c)]);
938        let (n, _mean, lo, hi) = g.edge_summary();
939        assert_eq!(n, 3);
940        assert!(
941            hi - lo > 0.3,
942            "mixed machine should show spread, got {}",
943            hi - lo
944        );
945    }
946
947    #[test]
948    fn nodes_absent_where_a_cpu_has_no_series() {
949        // An offline or never-sampled CPU is ABSENT, not depth zero. Zero would
950        // read as a permanently idle CPU and drag every mean toward it.
951        let g = LoadGraph::build(&[win(&[1.0, 2.0, 3.0]), win(&[])]);
952        assert!(g.node_depth[0].is_finite());
953        assert!(
954            g.node_depth[1].is_nan(),
955            "an unsampled CPU must not read as 0"
956        );
957    }
958}
959
960// THE GRAPH ACTUALLY DRIVES THE KNOBS
961//
962// derive_percpu_knobs is the consumer LoadGraph exists for. These pin the two
963// properties that decide whether it is safe to have MWU stop owning the slice:
964// a machine with no structure must come out exactly where it went in, and a
965// machine WITH structure must come out different in the right direction.
966#[cfg(test)]
967mod derive_tests {
968    use super::*;
969    use crate::tuning::TuningKnobs;
970
971    fn base() -> TuningKnobs {
972        let mut k = TuningKnobs::default();
973        k.slice_ns = 1_000_000;
974        k.preempt_thresh_ns = 1_000_000;
975        k.codel_thresh_ns = 5_000_000;
976        k
977    }
978
979    fn win(vals: &[f64]) -> RawWindow<CHAOS_WIN> {
980        let mut w = RawWindow::new();
981        for v in vals {
982            w.push(*v);
983        }
984        w
985    }
986
987    #[test]
988    fn a_uniform_machine_is_a_no_op() {
989        // The safety property. Every CPU at the same depth must derive the base
990        // slice exactly -- otherwise turning this on changes behavior on
991        // machines that have no structure to exploit, which is most of them.
992        let g = LoadGraph::build(&[win(&[3.0; 8]), win(&[3.0; 8]), win(&[3.0; 8])]);
993        let b = base();
994        for k in g.derive_percpu_knobs(&b) {
995            assert_eq!(
996                k.slice_ns, b.slice_ns,
997                "uniform depth must not move the slice"
998            );
999            assert_eq!(k.preempt_thresh_ns, b.preempt_thresh_ns);
1000        }
1001    }
1002
1003    #[test]
1004    fn a_deep_queue_gets_a_shorter_slice_than_a_shallow_one() {
1005        // The whole point: one machine-wide slice is the average of a
1006        // distribution MWU could not see. CPU 0 is buried, CPU 2 is nearly idle.
1007        let g = LoadGraph::build(&[win(&[12.0; 8]), win(&[3.0; 8]), win(&[0.2; 8])]);
1008        let k = g.derive_percpu_knobs(&base());
1009        assert!(
1010            k[0].slice_ns < k[1].slice_ns,
1011            "deep queue {} must slice shorter than mid {}",
1012            k[0].slice_ns,
1013            k[1].slice_ns
1014        );
1015        assert!(
1016            k[1].slice_ns < k[2].slice_ns,
1017            "mid {} must slice shorter than shallow {}",
1018            k[1].slice_ns,
1019            k[2].slice_ns
1020        );
1021    }
1022
1023    #[test]
1024    fn derived_slices_stay_inside_the_clamp() {
1025        // One anomalous tick must not hand a CPU a slice far outside what the
1026        // regime profile intended. 2x either way, no further.
1027        let g = LoadGraph::build(&[win(&[1e9; 8]), win(&[0.0001; 8])]);
1028        let b = base();
1029        for k in g.derive_percpu_knobs(&b) {
1030            assert!(k.slice_ns >= b.slice_ns / 2, "under-clamp: {}", k.slice_ns);
1031            assert!(k.slice_ns <= b.slice_ns * 2, "over-clamp: {}", k.slice_ns);
1032        }
1033    }
1034
1035    #[test]
1036    fn fields_the_graph_does_not_own_are_carried_through() {
1037        let g = LoadGraph::build(&[win(&[9.0; 8]), win(&[1.0; 8])]);
1038        let b = base();
1039        for k in g.derive_percpu_knobs(&b) {
1040            assert_eq!(
1041                k.codel_thresh_ns, b.codel_thresh_ns,
1042                "the graph must not touch knobs it does not own"
1043            );
1044        }
1045    }
1046
1047    #[test]
1048    fn an_unsampled_cpu_keeps_the_base() {
1049        // NaN node -> no derivation. A CPU we have not measured must not be
1050        // handed a slice computed from a number we do not have.
1051        let g = LoadGraph::build(&[win(&[9.0; 8]), win(&[])]);
1052        let b = base();
1053        let k = g.derive_percpu_knobs(&b);
1054        assert_eq!(k[1].slice_ns, b.slice_ns);
1055    }
1056}
1057
1058// EACH SENSOR DERIVES ITS OWN KNOB
1059//
1060// The claim these defend is that the four node attributes are not redundant:
1061// burstiness is SHAPE, lag-1 is DIRECTION, Hurst is DURATION, depth is
1062// MAGNITUDE. If any two moved the same knob the same way, one of them would be
1063// a second name for the other and MWU's single loss signal would have been
1064// adequate after all. Each test isolates one sensor and requires the others to
1065// stay out of the way.
1066#[cfg(test)]
1067mod derivation_tests {
1068    use super::*;
1069    use crate::tuning::TuningKnobs;
1070
1071    fn base() -> TuningKnobs {
1072        let mut k = TuningKnobs::default();
1073        k.slice_ns = 1_000_000;
1074        k.preempt_thresh_ns = 1_000_000;
1075        k.batch_slice_ns = 20_000_000;
1076        k.burst_slice_ns = 1_000_000;
1077        k.codel_thresh_ns = 5_000_000;
1078        k
1079    }
1080
1081    fn win(vals: &[f64]) -> RawWindow<CHAOS_WIN> {
1082        let mut w = RawWindow::new();
1083        for v in vals {
1084            w.push(*v);
1085        }
1086        w
1087    }
1088
1089    fn lcg(s: &mut u64) -> f64 {
1090        *s = s
1091            .wrapping_mul(6364136223846793005)
1092            .wrapping_add(1442695040888963407);
1093        ((*s >> 33) as f64 / (1u64 << 31) as f64) - 0.5
1094    }
1095
1096    // Spiky: long quiet punctuated by clumps. Burstiness positive.
1097    fn bursty() -> Vec<f64> {
1098        let mut v = vec![0.05f64; CHAOS_WIN];
1099        v[3] = 30.0;
1100        v[11] = 40.0;
1101        v
1102    }
1103
1104    // Steady arrivals with tiny jitter. Burstiness negative.
1105    fn paced() -> Vec<f64> {
1106        let mut s = 3u64;
1107        (0..CHAOS_WIN).map(|_| 5.0 + 0.01 * lcg(&mut s)).collect()
1108    }
1109
1110    #[test]
1111    fn bursty_traffic_lengthens_the_batch_ceiling_paced_shortens_it() {
1112        let g = LoadGraph::build(&[win(&bursty()), win(&paced())]);
1113        let b = base();
1114        let k = g.derive_percpu_knobs(&b);
1115        assert!(
1116            k[0].batch_slice_ns > b.batch_slice_ns,
1117            "bursty CPU should widen the batch ceiling: {} vs {}",
1118            k[0].batch_slice_ns,
1119            b.batch_slice_ns
1120        );
1121        assert!(
1122            k[1].batch_slice_ns < b.batch_slice_ns,
1123            "paced CPU should tighten it: {} vs {}",
1124            k[1].batch_slice_ns,
1125            b.batch_slice_ns
1126        );
1127    }
1128
1129    #[test]
1130    fn critical_slowing_tightens_the_preempt_window() {
1131        // A ramp is maximally autocorrelated: r1 -> 1, the approach to
1132        // saturation. Compare against an oscillating CPU at the same mean
1133        // depth, where r1 < 0 and no tightening should apply.
1134        let ramp: Vec<f64> = (0..CHAOS_WIN).map(|i| 4.0 + i as f64 * 0.1).collect();
1135        let osc: Vec<f64> = (0..CHAOS_WIN)
1136            .map(|i| if i % 2 == 0 { 4.7 } else { 4.0 })
1137            .collect();
1138        let g = LoadGraph::build(&[win(&ramp), win(&osc)]);
1139        let k = g.derive_percpu_knobs(&base());
1140        assert!(
1141            k[0].preempt_thresh_ns < k[1].preempt_thresh_ns,
1142            "a CPU that is slowing critically must preempt sooner: {} vs {}",
1143            k[0].preempt_thresh_ns,
1144            k[1].preempt_thresh_ns
1145        );
1146    }
1147
1148    #[test]
1149    fn persistence_rescues_sooner_than_mean_reversion() {
1150        // Random walk: H -> 1, a deep queue will stay deep, rescue sooner.
1151        // Differenced noise: H -> 0, transient, do not scatter the task.
1152        let mut s = 7u64;
1153        let mut acc = 5.0;
1154        let walk: Vec<f64> = (0..CHAOS_WIN)
1155            .map(|_| {
1156                acc += lcg(&mut s);
1157                acc.abs() + 1.0
1158            })
1159            .collect();
1160        let mut s2 = 11u64;
1161        let noise: Vec<f64> = (0..CHAOS_WIN + 1).map(|_| lcg(&mut s2)).collect();
1162        let diff: Vec<f64> = (1..=CHAOS_WIN)
1163            .map(|i| 5.0 + (noise[i] - noise[i - 1]))
1164            .collect();
1165        let g = LoadGraph::build(&[win(&walk), win(&diff)]);
1166        let k = g.derive_percpu_knobs(&base());
1167        assert!(
1168            k[0].codel_thresh_ns < k[1].codel_thresh_ns,
1169            "persistent load should rescue sooner than mean-reverting: {} vs {}",
1170            k[0].codel_thresh_ns,
1171            k[1].codel_thresh_ns
1172        );
1173    }
1174
1175    #[test]
1176    fn the_sensors_move_different_knobs() {
1177        // The non-redundancy claim, asserted directly. Two CPUs with the SAME
1178        // mean depth but different shape must differ on the burst-derived
1179        // knobs and agree on the depth-derived slice.
1180        let mut s = 5u64;
1181        let a: Vec<f64> = {
1182            let mut v = vec![0.2f64; CHAOS_WIN];
1183            v[4] = 20.0;
1184            v
1185        };
1186        let mean_a: f64 = a.iter().sum::<f64>() / a.len() as f64;
1187        let b: Vec<f64> = (0..CHAOS_WIN)
1188            .map(|_| mean_a + 0.001 * lcg(&mut s))
1189            .collect();
1190        let g = LoadGraph::build(&[win(&a), win(&b)]);
1191        let k = g.derive_percpu_knobs(&base());
1192        // Within rounding: the ratio is a float and the two means differ in
1193        // the last bits. What matters is that SHAPE did not leak into the
1194        // depth-derived knob.
1195        let d = k[0].slice_ns.abs_diff(k[1].slice_ns);
1196        assert!(
1197            d * 10_000 < k[0].slice_ns,
1198            "equal mean depth must derive the same slice, got {} vs {}",
1199            k[0].slice_ns,
1200            k[1].slice_ns
1201        );
1202        assert_ne!(
1203            k[0].batch_slice_ns, k[1].batch_slice_ns,
1204            "different traffic shape must derive a different batch ceiling"
1205        );
1206    }
1207
1208    #[test]
1209    fn data_flows_below_the_old_gate() {
1210        // THE POINT OF PRICING. Three samples is below the old burstiness floor
1211        // of four. Under the gate the knob did not move at all -- the reading
1212        // was discarded. Now it lands, weighted by what the window is worth.
1213        let g = LoadGraph::build(&[win(&[0.1, 9.0, 0.1])]);
1214        let b = base();
1215        assert_ne!(
1216            g.derive_percpu_knobs(&b)[0].batch_slice_ns,
1217            b.batch_slice_ns,
1218            "a sub-floor window must still reach the knob"
1219        );
1220    }
1221
1222    #[test]
1223    fn hurst_stays_gated_because_its_floor_is_arithmetic() {
1224        // The one estimator that is NOT priced. Its floor is not statistical
1225        // thinness -- the wavelet fit needs three octaves, and a short window
1226        // yields two, which is a line through two points rather than a
1227        // regression. There is no weakly-known answer, so codel_thresh_ns holds.
1228        let g = LoadGraph::build(&[win(&[2.0, 3.0, 2.5])]);
1229        let b = base();
1230        assert_eq!(
1231            g.derive_percpu_knobs(&b)[0].codel_thresh_ns,
1232            b.codel_thresh_ns
1233        );
1234    }
1235
1236    #[test]
1237    fn a_uniform_machine_derives_uniformly_but_not_necessarily_the_base() {
1238        // THE SAFETY PROPERTY, CORRECTED. With only the depth derivation an
1239        // unstructured machine came out exactly where it went in. With four
1240        // sensors that is no longer true and should not be: a machine with no
1241        // SPATIAL structure can still have TEMPORAL structure -- every CPU
1242        // slowing critically at once is a real state, and deriving from it is
1243        // the entire point.
1244        //
1245        // What must still hold is that identical CPUs get identical knobs.
1246        // Divergence without structure would mean a sensor is reading noise.
1247        let p = paced();
1248        let g = LoadGraph::build(&[win(&p), win(&p), win(&p)]);
1249        let k = g.derive_percpu_knobs(&base());
1250        for w in k.windows(2) {
1251            assert_eq!(
1252                w[0].slice_ns, w[1].slice_ns,
1253                "identical CPUs diverged on slice"
1254            );
1255            assert_eq!(
1256                w[0].preempt_thresh_ns, w[1].preempt_thresh_ns,
1257                "identical CPUs diverged on preempt"
1258            );
1259            assert_eq!(
1260                w[0].batch_slice_ns, w[1].batch_slice_ns,
1261                "identical CPUs diverged on batch ceiling"
1262            );
1263            assert_eq!(
1264                w[0].codel_thresh_ns, w[1].codel_thresh_ns,
1265                "identical CPUs diverged on rescue threshold"
1266            );
1267        }
1268    }
1269}
1270
1271// AFFINITY IS NOT DERIVED FROM COUPLING
1272//
1273// It was, for one release candidate, and the first PRISM pass measured the
1274// cost. These pin the retreat: coupling is still READ, and it no longer moves
1275// the knob.
1276#[cfg(test)]
1277mod affinity_tests {
1278    use super::*;
1279    use crate::tuning::{TuningKnobs, AFFINITY_WEAK};
1280
1281    fn win(vals: &[f64]) -> RawWindow<CHAOS_WIN> {
1282        let mut w = RawWindow::new();
1283        for v in vals {
1284            w.push(*v);
1285        }
1286        w
1287    }
1288    fn lcg(s: &mut u64) -> f64 {
1289        *s = s
1290            .wrapping_mul(6364136223846793005)
1291            .wrapping_add(1442695040888963407);
1292        ((*s >> 33) as f64 / (1u64 << 31) as f64) - 0.5
1293    }
1294
1295    #[test]
1296    fn coupling_no_longer_moves_affinity() {
1297        // Two tightly coupled runqueues -- the IPC ping-pong shape that
1298        // regressed 16x when this derived. The knob must keep the base.
1299        let mut s = 21u64;
1300        let a: Vec<f64> = (0..CHAOS_WIN).map(|_| 4.0 + lcg(&mut s)).collect();
1301        let b: Vec<f64> = a.iter().map(|v| 2.0 * v + 1.0).collect();
1302        let g = LoadGraph::build(&[win(&a), win(&b)]);
1303        assert!(
1304            g.edge_summary().1 > 0.9,
1305            "the pair must still READ as coupled"
1306        );
1307        let mut base = TuningKnobs::default();
1308        base.affinity_mode = AFFINITY_WEAK;
1309        for k in g.derive_percpu_knobs(&base) {
1310            assert_eq!(
1311                k.affinity_mode, AFFINITY_WEAK,
1312                "affinity must carry the base through, not be derived"
1313            );
1314        }
1315    }
1316}