Skip to main content

scx_cake/
main.rs

1// SPDX-License-Identifier: GPL-2.0
2//
3// scx_cake — a clean-slate sched_ext scheduler.
4//
5// This software may be used and distributed according to the terms of the
6// GNU General Public License version 2.
7
8mod bpf_skel;
9mod build_identity;
10mod core_performance;
11pub use bpf_skel::*;
12pub mod bpf_intf;
13pub use bpf_intf::*;
14
15use std::collections::BTreeMap;
16use std::io::IsTerminal;
17use std::mem::MaybeUninit;
18use std::sync::Arc;
19use std::sync::atomic::AtomicBool;
20use std::sync::atomic::AtomicU64;
21use std::sync::atomic::Ordering;
22use std::time::Duration;
23
24use anyhow::Context;
25use anyhow::Result;
26use clap::Parser;
27use libbpf_rs::AsRawLibbpf;
28use libbpf_rs::MapCore;
29use libbpf_rs::MapFlags;
30use libbpf_rs::OpenObject;
31use log::info;
32use log::warn;
33use scx_utils::NR_CPU_IDS;
34use scx_utils::Topology;
35use scx_utils::UserExitInfo;
36use scx_utils::build_id;
37use scx_utils::compat;
38use scx_utils::scx_ops_attach;
39use scx_utils::scx_ops_load;
40use scx_utils::scx_ops_open;
41use scx_utils::try_set_rlimit_infinity;
42use scx_utils::uei_exited;
43use scx_utils::uei_report;
44
45const SCHEDULER_NAME: &str = "scx_cake";
46
47/// ops.cpu_acquire exists for the probe hold census only; a release run does
48/// not pay a BPF entry per RT-to-SCX switch to record nothing.
49fn configure_acquire_census(skel: &mut OpenBpfSkel<'_>, probe_on: bool) {
50    if !probe_on {
51        skel.struct_ops.cake_ops_mut().cpu_acquire = std::ptr::null_mut();
52        skel.progs.cake_cpu_acquire.set_autoload(false);
53    }
54}
55
56/// scx_cake: a gaming-first sched_ext scheduler.
57#[derive(Debug, PartialEq, Eq, Parser)]
58#[command(after_help = toggle_help())]
59struct Opts {
60    /// Print startup core topology and platform performance preferences without attaching.
61    #[clap(long)]
62    print_topology: bool,
63
64    /// Verbose libbpf output and runtime diagnostics: slice and queue
65    /// layout, kernel fast paths, the full toggle line, interrupt sinks as
66    /// they change, kernel event counts at exit.
67    #[clap(short = 'v', long, action = clap::ArgAction::SetTrue)]
68    verbose: bool,
69
70    /// Print the version and exit.
71    #[clap(short = 'V', long, action = clap::ArgAction::SetTrue)]
72    version: bool,
73
74    /// Override one construct toggle, NAME=0|1, repeatable; the list is
75    /// below. Rodata, so the verifier deletes an off arm. An unknown name
76    /// or value is dropped with a warning.
77    #[clap(long = "toggle", value_name = "NAME=0|1")]
78    toggle: Vec<String>,
79
80    /// Observe-only sweep: override the handoff floor (ns) so the probe
81    /// histograms show how the split moves with the threshold.
82    #[clap(long = "handoff-ns", value_name = "NS")]
83    handoff_ns: Option<u64>,
84}
85
86/// Every construct toggle: name, construct, BPF default. The default is
87/// checked against the compiled rodata at start so the two cannot drift;
88/// --help prints this table.
89const TOGGLES: [(&str, &str, u8); 1] = [("probe", "diagnostics", 0)];
90
91fn toggle_help() -> String {
92    let mut s = String::from("Toggles (--toggle NAME=0|1, repeatable):\n");
93    for (name, what, dfl) in TOGGLES {
94        s.push_str(&format!("  {name:<9} {what:<20} default {dfl}\n"));
95    }
96    s
97}
98
99/// Retain complete pairs only, ordered for entry-first teardown.
100fn attach_irq_pair<L>(
101    leave: impl FnOnce() -> Result<L>,
102    enter: impl FnOnce() -> Result<L>,
103) -> Result<[L; 2]> {
104    let leave = leave().context("exit hook failed")?;
105    let enter = enter().context("entry hook failed")?;
106    Ok([enter, leave])
107}
108
109struct Scheduler<'a> {
110    skel: BpfSkel<'a>,
111    struct_ops: Option<libbpf_rs::Link>,
112    /// Handler-edge tracepoint links; dropped on exit with the rest.
113    _irq_links: Vec<libbpf_rs::Link>,
114    /// Diagnostics on (--toggle probe=1): census, hold attribution, black box.
115    probe_on: bool,
116    /// Print runtime diagnostics. Off by default: a release run is not a
117    /// measurement session, and a scheduler logging into a game is noise.
118    verbose: bool,
119    /// Live IRQ-sink tracking state.
120    sinks: SinkMonitor,
121    /// Multi-LLC hosts: the die each V-cache mode prefers, and the mode last seen.
122    pref_die: Option<PreferredDie>,
123    x3d_mode: Option<String>,
124}
125
126impl<'a> Scheduler<'a> {
127    fn init(opts: &Opts, open_object: &'a mut MaybeUninit<OpenObject>) -> Result<Self> {
128        try_set_rlimit_infinity();
129
130        // The startup banner is cake's only telemetry, one-shot: version, machine
131        // shape, compiled constants, and the fast paths the running kernel provides.
132        let topo = Topology::new().context("failed to read topology")?;
133        let physical = topo.all_cores.len();
134        let total = topo.all_cpus.len();
135
136        let slice_us = bpf_intf::consts_SLICE_NS as u64 / bpf_intf::consts_NSEC_PER_USEC as u64;
137        let queued_wakeup = *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP != 0;
138        let dsq_peek = compat::ksym_exists("scx_bpf_dsq_peek").unwrap_or(false);
139
140        info!(
141            "{} {}",
142            SCHEDULER_NAME,
143            build_id::full_version(env!("CARGO_PKG_VERSION"))
144        );
145        match build_identity::binary_sha256() {
146            Ok(hash) => info!("Build SHA256 {hash}"),
147            Err(err) => warn!("Build SHA256 unavailable: {err:#}"),
148        }
149        info!("CPU          {physical} cores / {total} hardware threads");
150        info!(
151            "Cache        {} shared last-level cache domain{}",
152            topo.all_llcs.len(),
153            if topo.all_llcs.len() == 1 { "" } else { "s" }
154        );
155
156        // Open the BPF program.
157        let mut skel_builder = BpfSkelBuilder::default();
158        skel_builder.obj_builder.debug(opts.verbose);
159        // debug(false) disables every libbpf message, including verifier
160        // failures. Keep errors visible without the verbose loader chatter.
161        if !opts.verbose {
162            scx_utils::init_libbpf_logging(Some(libbpf_rs::PrintLevel::Warn));
163        }
164        let mut skel = scx_ops_open!(skel_builder, open_object, cake_ops, None)?;
165        // scx_ops_attach! attaches this map explicitly after the accounting hooks.
166        // The generated skeleton has no map link slot for automatic attachment.
167        // SAFETY: cake_ops is a live struct_ops map owned by the open skeleton.
168        let ret = unsafe {
169            libbpf_rs::libbpf_sys::bpf_map__set_autoattach(
170                skel.maps.cake_ops.as_libbpf_object().as_ptr(),
171                false,
172            )
173        };
174        anyhow::ensure!(ret == 0, "Failed to disable cake_ops auto-attach: {ret}");
175        // The handler-edge hooks attach by hand, exit first (below); the skeleton's
176        // auto-attach ran them in section order and a second time.
177        for prog in [
178            &mut skel.progs.cake_irq_enter,
179            &mut skel.progs.cake_irq_leave,
180            &mut skel.progs.cake_softirq_enter,
181            &mut skel.progs.cake_softirq_leave,
182            &mut skel.progs.cake_cpu_idle,
183        ] {
184            prog.set_autoattach(false);
185        }
186
187        // Linux CPU numbering does not guarantee that an SMT sibling is cpu +/-
188        // nr_cpus/2: populate the BPF lookup from sysfs topology. -1 means no online
189        // sibling, and the fallback uses the kernel idle picker.
190        let rodata = skel
191            .maps
192            .rodata_data
193            .as_mut()
194            .context("BPF rodata unavailable for CPU topology")?;
195
196        // The CPU id span the steal ring and neighbour probe scan: rodata, so the
197        // verifier folds it. It must cover the kernel's nr_cpu_ids, which counts
198        // POSSIBLE CPUs; the topology lists present ones only. ops.init refuses less.
199        let span = (*NR_CPU_IDS).max(
200            std::fs::read_to_string("/sys/devices/system/cpu/possible")
201                .ok()
202                .and_then(|t| last_cpu_id(&t))
203                .map_or(0, |last| last + 1),
204        );
205        anyhow::ensure!(
206            span <= bpf_intf::consts_MAX_CPUS as usize,
207            "host nr_cpu_ids {} exceeds Cake's compiled MAX_CPUS {}",
208            span,
209            bpf_intf::consts_MAX_CPUS
210        );
211        rodata.nr_cpu_span = span as u32;
212        // The census word covers PRESENT ids; hotplug headroom past 64 costs
213        // nothing, a present id past 64 turns the one-word paths off.
214        let one_word = *NR_CPU_IDS <= 64;
215        rodata.cake_one_word = u8::from(one_word);
216        rodata.cake_tick_ns = coarse_tick_ns();
217        if let Some(ns) = opts.handoff_ns {
218            rodata.cake_handoff_max_ns = ns;
219            warn!("   sweep   handoff_max {ns} ns (observe-only override)");
220        }
221        let (idle_driver, idle_states) = cpuidle_states();
222        for (slot, ns) in rodata.cake_idle_state_ns.iter_mut().zip(&idle_states) {
223            *slot = *ns;
224        }
225        rodata.cake_idle_exit_max_ns = idle_states.iter().copied().max().unwrap_or(0);
226        // The deep-idle word is read only by the one-word pickers: a wider host
227        // leaves the cpu_idle tracepoint unloaded rather than fed for nobody.
228        let idle_exit_max_ns = if one_word {
229            rodata.cake_idle_exit_max_ns
230        } else {
231            0
232        };
233        match &idle_driver {
234            Some(d) => info!(
235                "Idle         {d}: {} power-saving states, up to {} us exit latency",
236                idle_states.len(),
237                idle_states.iter().copied().max().unwrap_or(0) / 1000
238            ),
239            None => {
240                info!("Idle         No CPU idle driver; no idle-exit cost applied")
241            }
242        }
243        // Capacity asymmetry inside one cache domain is the hybrid signature;
244        // across dies it is binning or cache, which the die model handles.
245        if one_word && topo.all_llcs.len() == 1 && topo.has_little_cores() {
246            rodata.cake_cap_tiers = 1;
247            rodata.cake_cap_word = topo
248                .all_cpus
249                .values()
250                .filter(|c| c.core_type != scx_utils::CoreType::Little)
251                .fold(0u64, |w, c| w | (1u64 << c.id));
252            info!(
253                "Hybrid CPU   {} high-capacity logical CPUs preferred",
254                rodata.cake_cap_word.count_ones()
255            );
256        }
257        if !one_word {
258            warn!(
259                "   span    {} CPU ids exceed one census word: claim walk, seats, deep-idle avoidance and die-local pools off, kernel idle pick only",
260                *NR_CPU_IDS
261            );
262        }
263
264        // Campaign toggles land in rodata before load so the verifier prunes the off
265        // arms; one table names every toggle, a second line prints what differs.
266        let fields: [&mut u8; 1] = [&mut rodata.cake_tog_probe];
267        let mut slots: Vec<(&str, &str, &mut u8)> = TOGGLES
268            .iter()
269            .zip(fields)
270            .map(|((name, what, dfl), field)| {
271                if *field != *dfl {
272                    warn!("   toggle  {name} defaults to {} in BPF but {dfl} in TOGGLES; fix the table", *field);
273                }
274                (*name, *what, field)
275            })
276            .collect();
277        let defaults: Vec<u8> = slots.iter().map(|s| *s.2).collect();
278        for spec in &opts.toggle {
279            // A bad toggle is reported and ignored, never fatal: a stale
280            // flag from an old config must not keep the scheduler off.
281            let Some((name, val)) = spec.split_once('=') else {
282                warn!("   toggle  ignored `{spec}`: expected NAME=0|1");
283                continue;
284            };
285            let on = match val {
286                "0" => 0u8,
287                "1" => 1u8,
288                _ => {
289                    warn!("   toggle  ignored `{spec}`: value must be 0 or 1");
290                    continue;
291                }
292            };
293            if let Some(slot) = slots.iter_mut().find(|s| s.0 == name) {
294                *slot.2 = on;
295            } else {
296                warn!("   toggle  ignored `{spec}`: no toggle named {name}");
297            }
298        }
299
300        let core_performance = core_performance::CorePerformanceLayout::discover(&topo);
301        let rank_words = core_performance.rank_words();
302        if one_word && rank_words.len() > 1 {
303            rodata.cake_rank_tiers = rank_words.len() as u32;
304            rodata.cpu_perf_known = rank_words.iter().fold(0, |all, word| all | word);
305            for (slot, word) in rodata.cpu_perf_tier.iter_mut().zip(rank_words) {
306                *slot = word;
307            }
308        }
309        if rodata.cake_rank_tiers > 1 {
310            info!(
311                "Placement    Platform core preferences enabled ({} levels)",
312                rodata.cake_rank_tiers
313            );
314        } else {
315            info!("Placement    No additional core ranking applied");
316        }
317        if opts.verbose {
318            info!("Core order   {}", core_performance.summary());
319        }
320        let pref_die = preferred_die(&topo, &core_performance);
321        let x3d_mode = amd_x3d_mode();
322        if let Some(die) = &pref_die {
323            info!("Cache bias   Preferred cache domain selected");
324            let w = preferred_word(die, x3d_mode.as_deref());
325            if opts.verbose {
326                info!(
327                    "Cache mask   {w:#x} (x3d mode {})",
328                    x3d_mode.as_deref().unwrap_or("n/a")
329                );
330            }
331        }
332        if opts.verbose {
333            for core in core_performance.details() {
334                info!("   {core}");
335            }
336        }
337        if opts.verbose {
338            info!("   slice   {slice_us}µs, {total} per-CPU vtime queues + one wake pool per LLC");
339        }
340        // cake calls scx_bpf_dsq_peek() unconditionally; on a kernel without
341        // the ksym the load fails outright, so "MISSING" is the honest word.
342        let kernel_line = format!(
343            "   kernel  queued_wakeup {}, dsq_peek {}",
344            if queued_wakeup { "on" } else { "UNSUPPORTED" },
345            if dsq_peek { "native" } else { "MISSING" }
346        );
347        if !queued_wakeup || !dsq_peek {
348            warn!("{kernel_line}");
349        } else if opts.verbose {
350            info!("{kernel_line}");
351        }
352        let probe_on = slots.iter().any(|s| s.0 == "probe" && *s.2 == 1);
353        // Stock start says nothing about toggles. An override prints one
354        // line naming what moved; the full identity line is --verbose only.
355        let changed: Vec<String> = slots
356            .iter()
357            .zip(&defaults)
358            .filter(|(s, d)| *s.2 != **d)
359            .map(|(s, _)| format!("{}={} {}", s.0, s.2, s.1))
360            .collect();
361        if opts.verbose {
362            let line: Vec<String> = slots.iter().map(|s| format!("{}={}", s.0, s.2)).collect();
363            info!("   toggle  {}", line.join(" "));
364        }
365        if !changed.is_empty() {
366            info!("   toggle  {} (all others at default)", changed.join(", "));
367        } else if !opts.toggle.is_empty() {
368            info!("   toggle  all at default");
369        }
370
371        // Hardware-anchored thresholds: measured, never derived from the slice.
372        // Clamped so a probe perturbed by host load cannot mis-tune.
373        match probe_handoff_hop_ns() {
374            Some(probe) => {
375                // The admission threshold IS the tail of a genuine handoff, used directly.
376                // DIAGNOSTIC ONLY: driving cake_handoff_max_ns from this probe cost
377                // mutex-handoff 35%; it measures a clean ping-pong, not contended handoffs.
378                let (med, p99) = (probe.median, probe.p99);
379                let hm = rodata.cake_handoff_max_ns;
380                // The probe's p99 is the pick-to-landing horizon the tick predictor needs;
381                // a hop past a quarter tick would call every CPU tick-soon: off.
382                let hop_max = rodata.cake_tick_ns / 4;
383                rodata.cake_wake_hop_ns = if p99 <= hop_max {
384                    p99
385                } else {
386                    warn!("   probe   hop p99 {p99}ns exceeds {hop_max}ns; tick predictor off");
387                    0
388                };
389                if opts.verbose {
390                    info!(
391                        "Handoff      Hop median {med} ns / p99 {p99} ns; handoff limit {hm} ns (diagnostic)"
392                    );
393                }
394            }
395            None => {
396                log::warn!("   probe   handoff probe failed (diagnostic only)");
397            }
398        }
399
400        // Interrupt sinks are measured live, never guessed: per-CPU handler-time
401        // share off the run loop, cut at the distribution's widest gap. Nothing is
402        // sampled at attach (that measures launch), so the scheduler starts sink-free.
403
404        let siblings = &mut rodata.cpu_sibling;
405        siblings.fill(-1);
406        for core in topo.all_cores.values() {
407            let cpu_ids: Vec<usize> = core.cpus.keys().copied().collect();
408
409            if cpu_ids.len() < 2 {
410                continue;
411            }
412            for (idx, cpu) in cpu_ids.iter().copied().enumerate() {
413                let sibling = cpu_ids[(idx + 1) % cpu_ids.len()];
414
415                anyhow::ensure!(
416                    cpu < siblings.len() && sibling <= i32::MAX as usize,
417                    "CPU topology id exceeds Cake's compiled sibling map"
418                );
419                siblings[cpu] = sibling as i32;
420            }
421        }
422        if let Some((shift, left, right)) = smt_fold(siblings) {
423            rodata.cake_smt_shift = shift;
424            rodata.cake_smt_left = left;
425            rodata.cake_smt_right = right;
426        }
427
428        // Multi-CCD steal order is a runtime decision, never a build-host
429        // property: one binary must attach on any topology. Hosts wider than
430        // the fixed matrix span keep the generic ring walk.
431        {
432            let rodata = skel
433                .maps
434                .rodata_data
435                .as_mut()
436                .context("BPF rodata unavailable for cache topology")?;
437            let order = &mut rodata.cpu_steal_order;
438            let span = bpf_intf::consts_STEAL_SPAN as usize;
439            let fits = topo
440                .all_cpus
441                .keys()
442                .next_back()
443                .is_none_or(|cpu| *cpu < span);
444            let llc_of = |cpu: &scx_utils::Cpu| -> usize { cpu.llc_id };
445            let nr_llcs = topo.all_llcs.len();
446            let multi_ccd = nr_llcs > 1;
447            rodata.steal_order_live = u8::from(fits);
448            rodata.nr_steal_cpus = topo.all_cpus.len().saturating_sub(1) as u32;
449            order.fill(0);
450
451            // The per-CPU LLC word, the dense LLC id and the LLC count, from every
452            // CPU the topology describes.
453            {
454                let cpus: Vec<(usize, usize)> =
455                    topo.all_cpus.values().map(|c| (c.id, llc_of(c))).collect();
456                // Die-local pools need the one-word census: past it the pool kick is
457                // LLC-blind and lands on a die whose dispatch reads its own pool.
458                let max_llcs = if rodata.cake_one_word != 0 {
459                    bpf_intf::consts_MAX_LLCS as usize
460                } else {
461                    1
462                };
463                let layout = LlcLayout::build(&cpus, max_llcs);
464                if let Some(n) = layout.collapsed {
465                    warn!(
466                        "   llc     {n} domains, {max_llcs} pool(s) usable here; one pool, LLC-blind"
467                    );
468                }
469                rodata.cpu_llc_id.fill(0);
470                rodata.cpu_llc_domain.fill(u16::MAX);
471                rodata.cpu_llc_word.fill(u64::MAX);
472                for (cpu, llc) in &cpus {
473                    if *cpu < rodata.cpu_llc_id.len() {
474                        rodata.cpu_llc_id[*cpu] = layout.dense[llc];
475                        rodata.cpu_llc_domain[*cpu] = layout.domain[llc];
476                        rodata.cpu_llc_word[*cpu] = layout.word[llc];
477                    }
478                }
479                rodata.nr_llcs = layout.nr_llcs;
480            }
481
482            // Online CPUs per LLC and per host, tripled for the serial gate's
483            // three-quarters test; claim tries scale with the widest die.
484            {
485                let online = online_cpu_set(&topo);
486                let mut per_llc: BTreeMap<usize, u32> = BTreeMap::new();
487                for cpu in topo.all_cpus.values() {
488                    if online.contains(&cpu.id) {
489                        *per_llc.entry(llc_of(cpu)).or_insert(0) += 1;
490                    }
491                }
492                rodata.nr_cpu_online3 = per_llc.values().sum::<u32>() * 3;
493                rodata.cpu_llc_online3.fill(0);
494                let mut die_max = 0u32;
495                for cpu in topo.all_cpus.values() {
496                    let n = per_llc.get(&llc_of(cpu)).copied().unwrap_or(0);
497                    die_max = die_max.max(n);
498                    if cpu.id < rodata.cpu_llc_online3.len() {
499                        rodata.cpu_llc_online3[cpu.id] = n * 3;
500                    }
501                }
502                rodata.cake_claim_tries = (die_max / 4).max(bpf_intf::consts_CLAIM_TRIES_MIN);
503            }
504
505            if multi_ccd && !fits {
506                warn!("   ccd     host wider than steal matrix ({span} CPUs); ring steal only");
507            }
508            if fits {
509                let mut llc_cache: BTreeMap<usize, usize> = BTreeMap::new();
510                for cpu in topo.all_cpus.values() {
511                    let e = llc_cache.entry(llc_of(cpu)).or_insert(0);
512                    *e = (*e).max(cpu.cache_size);
513                }
514                let policy = bpf_intf::consts_CCD_STEAL_POLICY;
515                let nr_ids = *NR_CPU_IDS;
516
517                for src in topo.all_cpus.values() {
518                    let mut candidates: Vec<_> = topo
519                        .all_cpus
520                        .values()
521                        .filter(|dst| dst.id != src.id)
522                        .collect();
523                    candidates.sort_by_key(|dst| {
524                        let class = if llc_of(dst) == llc_of(src) {
525                            0
526                        } else if policy > 1 && llc_cache[&llc_of(dst)] == llc_cache[&llc_of(src)] {
527                            1
528                        } else {
529                            2
530                        };
531                        // Own die: the SMT sibling first, then by id distance.
532                        (
533                            class,
534                            u8::from(dst.core_id != src.core_id),
535                            (dst.id + nr_ids - src.id) % nr_ids,
536                        )
537                    });
538                    let base = src.id * span;
539                    for (slot, dst) in candidates.into_iter().enumerate() {
540                        order[base + slot] = dst.id as u16;
541                    }
542                }
543            }
544        }
545
546        configure_acquire_census(&mut skel, probe_on);
547        if idle_exit_max_ns == 0 {
548            skel.progs.cake_cpu_idle.set_autoload(false);
549        }
550
551        // Load and attach.
552        let mut skel = scx_ops_load!(skel, cake_ops, uei)?;
553
554        // Handler-edge tracepoints feed the in-handler depth. The exit hook of each
555        // pair attaches FIRST (an entry counted before its exit hook exists leaves
556        // that CPU mid-handler for the run); a pair whose exit fails skips its entry.
557        let mut irq_links = Vec::with_capacity(4);
558        for (name, leave, enter) in [
559            (
560                "irq",
561                &skel.progs.cake_irq_leave,
562                &skel.progs.cake_irq_enter,
563            ),
564            (
565                "softirq",
566                &skel.progs.cake_softirq_leave,
567                &skel.progs.cake_softirq_enter,
568            ),
569        ] {
570            match attach_irq_pair(|| Ok(leave.attach()?), || Ok(enter.attach()?)) {
571                Ok(links) => irq_links.extend(links),
572                Err(e) => warn!("   irq     {name} {e:#}; this source uses chronic steering only"),
573            }
574        }
575
576        if idle_exit_max_ns > 0 {
577            match skel.progs.cake_cpu_idle.attach() {
578                Ok(link) => irq_links.push(link),
579                Err(e) => warn!("   idle    cpu_idle tracepoint {e:#}; idle depth unknown"),
580            }
581        }
582        if let (Some(die), Some(bss)) = (&pref_die, skel.maps.bss_data.as_mut()) {
583            bss.cake_llc_pref_word = preferred_word(die, x3d_mode.as_deref());
584        }
585
586        // Begin scheduling only after handler accounting is installed.
587        let struct_ops = Some(scx_ops_attach!(skel, cake_ops)?);
588
589        if let Some(bss) = skel.maps.bss_data.as_ref() {
590            let on_cpu = match bss.cake_compat_on_cpu_size {
591                1 => "u8",
592                4 => "32-bit",
593                _ => "unknown (idle kick retained)",
594            };
595            let cpu_curr = if bss.cake_compat_cpu_curr_kfunc {
596                "kfunc"
597            } else {
598                "rq fallback"
599            };
600            let tracking = match (bss.cake_compat_on_cpu_size, bss.cake_compat_cpu_curr_kfunc) {
601                (1 | 4, true) => "Native kernel support",
602                (1 | 4, false) => "Compatibility fallback for this kernel",
603                _ => "Unknown task-state layout; conservative idle kicks enabled",
604            };
605            info!("Task state   {tracking}");
606            if opts.verbose {
607                info!("Kernel paths on_cpu={on_cpu} / cpu_curr={cpu_curr}");
608            }
609        }
610        info!("🍰 Running");
611        // ops.init chose the task-age clock; say which under -v.
612        let tick_ns = skel.maps.rodata_data.as_ref().map_or(0, |r| r.cake_tick_ns);
613        if opts.verbose {
614            if tick_ns > 0 {
615                info!("   age     tick clock ({tick_ns} ns per tick)");
616            } else {
617                info!("   age     precise clock (no tick-sized coarse clock resolution)");
618            }
619        }
620
621        // File capabilities are needed only to load and attach. Drop them all: any
622        // elevated capability left in the permitted set makes the kernel deny
623        // /proc/<pid>/exe to unprivileged observers, breaking hash-of-exe identity.
624        if let Err(err) = drop_privileges_for_observers() {
625            warn!("post-attach capability drop failed: {err}");
626        }
627
628        Ok(Self {
629            skel,
630            struct_ops,
631            _irq_links: irq_links,
632            probe_on,
633            verbose: opts.verbose,
634            sinks: SinkMonitor::new(*NR_CPU_IDS),
635            pref_die,
636            x3d_mode,
637        })
638    }
639
640    fn exited(&mut self) -> bool {
641        uei_exited!(&self.skel, uei)
642    }
643
644    fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
645        let mut polls: u32 = 0;
646        while !shutdown.load(Ordering::Relaxed) && !self.exited() {
647            std::thread::sleep(Duration::from_secs(1));
648            polls += 1;
649
650            // Self-check: a CPU mid-handler across two reads 50 ms apart is an entry
651            // counted before its exit hook existed. The healthy answer is zero.
652            if self.verbose && (polls == 2 || polls == 6) {
653                let depths = |bss: &bpf_skel::types::bss| -> Vec<u32> {
654                    bss.cake_irq_live
655                        .iter()
656                        .take(*NR_CPU_IDS)
657                        .map(|s| s.depth)
658                        .collect()
659                };
660                let first = self.skel.maps.bss_data.as_ref().map(|b| depths(b));
661                std::thread::sleep(Duration::from_millis(50));
662                let second = self.skel.maps.bss_data.as_ref().map(|b| depths(b));
663                if let (Some(a), Some(b)) = (first, second) {
664                    let stuck: Vec<String> = a
665                        .iter()
666                        .zip(&b)
667                        .enumerate()
668                        .filter(|(_, (x, y))| **x != 0 && **y != 0)
669                        .map(|(i, (x, y))| format!("slot{}.{}:{x}/{y}", i / 2, i % 2))
670                        .collect();
671                    info!(
672                        "   irq     in-handler depth stuck on {} CPU(s) at {polls} s {}",
673                        stuck.len(),
674                        stuck.join(" ")
675                    );
676                }
677            }
678
679            if let Some(set) = self.sinks.tick(*NR_CPU_IDS) {
680                self.publish_sinks(&set);
681            }
682            // A game-mode daemon flips the V-cache mode after attach.
683            if let Some(die) = &self.pref_die {
684                let mode = amd_x3d_mode();
685                if mode != self.x3d_mode {
686                    if let Some(bss) = self.skel.maps.bss_data.as_mut() {
687                        bss.cake_llc_pref_word = preferred_word(die, mode.as_deref());
688                    }
689                    info!("   die     x3d mode {}", mode.as_deref().unwrap_or("n/a"));
690                    self.x3d_mode = mode;
691                }
692            }
693        }
694
695        // Diagnostics (--toggle probe=1): black box of placements that waited > 10 ms, then the census.
696        if let (true, Some(bss)) = (self.probe_on, self.skel.maps.bss_data.as_ref()) {
697            let n = bss.cake_blackbox_n;
698            for i in 0..n.min(4) {
699                let b = &bss.cake_blackbox[i as usize];
700                let comm = String::from_utf8_lossy(
701                    &b.comm
702                        .iter()
703                        .map(|c| *c as u8)
704                        .take_while(|c| *c != 0)
705                        .collect::<Vec<u8>>(),
706                )
707                .to_string();
708                info!(
709                    "   BLACKBOX wait {:.2} ms  {} pid {} kind {} target cpu{} caller cpu{} waker {} ran_on cpu{}  seats {:#018x} core_free {:#018x} thread_free {:#018x} idle {:#018x}",
710                    b.wait_ns as f64 / 1e6,
711                    comm,
712                    b.pid,
713                    b.kind,
714                    b.target,
715                    b.caller,
716                    b.waker_pid,
717                    b.ran_on,
718                    b.seats,
719                    b.core_free,
720                    b.thread_free,
721                    b.idle_word
722                );
723            }
724        }
725        if let (true, Some(bss)) = (self.probe_on, self.skel.maps.bss_data.as_ref()) {
726            // Release census: by arrival path x displacer band, by reason; holds by band.
727            let bands: Vec<String> = (0..bss.cake_acquire_hist.len())
728                .map(|b| format!("{}us", 1u64 << b))
729                .collect();
730            info!("   g93     bands (>=) {}", bands.join(" "));
731            for (path, name) in ["non-immed", "immed", "slice-0"].iter().enumerate() {
732                let row: Vec<String> = bss.cake_release_census[path]
733                    .iter()
734                    .map(|v| v.to_string())
735                    .collect();
736                info!("   g93     release {:9} {}", name, row.join(" "));
737            }
738            let reasons: Vec<String> = bss
739                .cake_release_reason
740                .iter()
741                .map(|v| v.to_string())
742                .collect();
743            info!(
744                "   g93     release by reason rt/dl/stop/unknown {}",
745                reasons.join(" ")
746            );
747            let holds: Vec<String> = bss
748                .cake_acquire_hist
749                .iter()
750                .map(|v| v.to_string())
751                .collect();
752            info!("   g93     hold      {}", holds.join(" "));
753            // Observe-only histograms: bands summed over CPUs, the antimode split
754            // (the emptiest band between the two largest modes) and the median band.
755            let shift = bpf_intf::consts_CAKE_HIST_SHIFT;
756            for (kind, name) in ["hop", "handoff", "burst"].iter().enumerate() {
757                let bands = bss.cake_hist[0][kind].len();
758                let sum: Vec<u64> = (0..bands)
759                    .map(|b| bss.cake_hist.iter().map(|cpu| cpu[kind][b]).sum())
760                    .collect();
761                let total: u64 = sum.iter().sum();
762                if total == 0 {
763                    continue;
764                }
765                let mut peaks: Vec<usize> = (0..bands).collect();
766                peaks.sort_by_key(|b| std::cmp::Reverse(sum[*b]));
767                let (lo, hi) = (peaks[0].min(peaks[1]), peaks[0].max(peaks[1]));
768                let valley = (lo..=hi).min_by_key(|b| sum[*b]).unwrap_or(lo);
769                let mut acc = 0u64;
770                let median = (0..bands)
771                    .find(|b| {
772                        acc += sum[*b];
773                        acc * 2 >= total
774                    })
775                    .unwrap_or(0);
776                let rows: Vec<String> = sum.iter().map(|v| v.to_string()).collect();
777                info!(
778                    "   hist    {name:7} n={total} median>={}ns split>={}ns  {}",
779                    1u64 << (median as u32 + shift),
780                    1u64 << (valley as u32 + shift),
781                    rows.join(" ")
782                );
783            }
784            // Gates reached; fired counts are the stats rows serial, seat_retake,
785            // probe_fired, rej_tick.
786            let tried: Vec<String> = (0..4)
787                .map(|g| {
788                    bss.cake_tried
789                        .iter()
790                        .map(|row| row[g])
791                        .sum::<u64>()
792                        .to_string()
793                })
794                .collect();
795            info!("   tried   serial/retake/probe/tick {}", tried.join(" "));
796        }
797        if self.probe_on {
798            const NAMES: [&str; 138] = [
799                "select_calls",
800                "serial",
801                "home_warm",
802                "wp_attempt",
803                "wp_tiny",
804                "wp_small",
805                "wp_protect",
806                "wp_vtime",
807                "wp_starved",
808                "wp_fired",
809                "pl_local",
810                "pl_local_on",
811                "pl_cpuq_wake",
812                "pl_cpuq_cont",
813                "pl_global",
814                "h300_local",
815                "h300_local_on",
816                "h300_cpuq_wake",
817                "h300_cpuq_cont",
818                "h300_global",
819                "h1ms_local",
820                "h1ms_local_on",
821                "h1ms_cpuq_wake",
822                "h1ms_cpuq_cont",
823                "h1ms_global",
824                "pl_self",
825                "h300_self",
826                "h1ms_self",
827                "hd_sync",
828                "hd_starved",
829                "hd_irq",
830                "hd_aff",
831                "hd_contended",
832                "hd_notidle",
833                "home_busy",
834                "home_localq",
835                "h300_home_busy",
836                "ui_enter",
837                "ui_enter_idlew",
838                "ui_exit",
839                "ui_exit_idlew",
840                "qmark_set",
841                "qmark_set_skip",
842                "qmark_clr",
843                "qmark_clr_skip",
844                "seat_clr",
845                "seat_set",
846                "running",
847                "frontier_st",
848                "wake_served_st",
849                "wake_mark_st",
850                "taci",
851                "taci_win",
852                "taci_stage",
853                "taci_home",
854                "taci_groove",
855                "taci_warm_core",
856                "taci_warm_thread",
857                "taci_warm",
858                "taci_hint",
859                "taci_notify",
860                "pick_idle",
861                "kick",
862                "nrq",
863                "dsq_insert",
864                "move_local",
865                "kt",
866                "kt_period",
867                "kt_ticksoon",
868                "kt_occupant",
869                "kt_handoff",
870                "kt_wakeclock",
871                "kt_running",
872                "kt_probe",
873                "task_storage",
874                "cpu_curr",
875                "core_contended",
876                "stage_probe",
877                "taciw_stage",
878                "taciw_home",
879                "taciw_groove",
880                "taciw_warm_core",
881                "taciw_warm_thread",
882                "taciw_warm",
883                "taciw_hint",
884                "taciw_notify",
885                "t_nrq",
886                "t_taci",
887                "t_pick",
888                "t_kick",
889                "t_cpu_curr",
890                "t_task_storage",
891                "t_move",
892                "t_insert",
893                "t_ui_idlew",
894                "t_qmark",
895                "t_cal",
896                "leak_home",
897                "leak_kick",
898                "leak_dispatch",
899                "seat_immune",
900                "seat_retake",
901                "seat_reroute",
902                "seat_decline",
903                "pool_forward",
904                "claim_retry",
905                "kt_pool",
906                "x_serial",
907                "kt_local",
908                "x_kt_local",
909                "x_claim",
910                "notify_kick",
911                "x_notify_kick",
912                "probe_fired",
913                "x_probe_fired",
914                "pool_served",
915                "x_pool_served",
916                "steal_moved",
917                "x_steal_moved",
918                "pool_direct",
919                "kick_alone",
920                "seat_skip",
921                "grant_vacant",
922                "grant_expired",
923                "grant_lt_tick",
924                "grant_ge_tick",
925                "hd_corebusy",
926                "rej_irq",
927                "rej_tick",
928                "expiry_preempt",
929                "release",
930                "acquire",
931                "select_direct",
932                "ui_kick",
933                "pend_kick",
934                "pend_heal",
935                "release_serve",
936                "pinned_preempt",
937            ];
938            // The name table must match the BPF enum exactly; a drift prints
939            // zeros silently because out-of-range lookups fail quietly.
940            let nr = self
941                .skel
942                .maps
943                .cake_stats
944                .info()
945                .map(|i| i.info.max_entries)
946                .unwrap_or(0) as usize;
947            if nr != NAMES.len() {
948                warn!(
949                    "   census  name table has {} entries, map has {}: names are out of sync",
950                    NAMES.len(),
951                    nr
952                );
953            }
954            let mut tot = [0u64; NAMES.len()];
955            for (i, t) in tot.iter_mut().enumerate() {
956                let key = (i as u32).to_ne_bytes();
957                if let Ok(Some(percpu)) =
958                    self.skel.maps.cake_stats.lookup_percpu(&key, MapFlags::ANY)
959                {
960                    for cpu in &percpu {
961                        if cpu.len() >= 8 {
962                            *t += u64::from_ne_bytes(cpu[..8].try_into().unwrap());
963                        }
964                    }
965                }
966            }
967            let sel = tot[0].max(1) as f64;
968            for (i, name) in NAMES.iter().enumerate() {
969                info!(
970                    "   arms    {name:<13} {:>12}  {:>6.2}% of select",
971                    tot[i],
972                    tot[i] as f64 * 100.0 / sel
973                );
974            }
975        }
976
977        if let (true, Some(bss)) = (self.verbose, self.skel.maps.bss_data.as_mut()) {
978            let ev = &bss.cake_events;
979            info!(
980                "   events  select_fallback {} keep_last {} enq_skip_exiting {}",
981                ev.SCX_EV_SELECT_CPU_FALLBACK,
982                ev.SCX_EV_DISPATCH_KEEP_LAST,
983                ev.SCX_EV_ENQ_SKIP_EXITING
984            );
985        }
986
987        self.struct_ops.take();
988        info!("Stopped");
989        uei_report!(&self.skel, uei)
990    }
991
992    /// Publish the existing IRQ measurements as aligned CPU-mask words.
993    /// Wide hosts may observe different publication epochs across words;
994    /// these are placement preferences, never CPU admission or reservations.
995    fn publish_sinks(&mut self, set: &[bool]) {
996        const N: usize = bpf_intf::consts_QMASK_WORDS as usize;
997        let words = sink_words::<N>(set);
998        // The whole-core expansion, computed once per publication instead of on
999        // every claim walk in BPF: a sink's SMT sibling shares its core.
1000        let siblings: Vec<i32> = self
1001            .skel
1002            .maps
1003            .rodata_data
1004            .as_ref()
1005            .map(|ro| ro.cpu_sibling.to_vec())
1006            .unwrap_or_default();
1007        let mut cores = words;
1008        for (cpu, hot) in set.iter().take(N * 64).enumerate() {
1009            if let (true, Some(&sib)) = (*hot, siblings.get(cpu))
1010                && sib >= 0
1011                && (sib as usize) < N * 64
1012            {
1013                cores[sib as usize / 64] |= 1u64 << (sib as usize % 64);
1014            }
1015        }
1016        let Some(bss) = self.skel.maps.bss_data.as_mut() else {
1017            return;
1018        };
1019        for (published, word) in bss.cpu_irq_hot_words.iter_mut().zip(words) {
1020            // SAFETY: the skeleton maps writable, u64-aligned BSS that BPF
1021            // reads concurrently; the atomic view lives no longer than the
1022            // &mut it is built from. No cross-word atomicity assumed.
1023            unsafe { AtomicU64::from_ptr(published) }.store(word, Ordering::Relaxed);
1024        }
1025        for (published, word) in bss.cpu_irq_hot_cores.iter_mut().zip(cores) {
1026            // SAFETY: as above.
1027            unsafe { AtomicU64::from_ptr(published) }.store(word, Ordering::Relaxed);
1028        }
1029
1030        // Verbose only: the flip rate is a covariate of a frame capture, and a
1031        // release run is not a measurement session.
1032        if self.verbose {
1033            let named: Vec<usize> = set
1034                .iter()
1035                .enumerate()
1036                .filter_map(|(cpu, hot)| hot.then_some(cpu))
1037                .collect();
1038            info!("   irq     sinks {named:?} steered around");
1039        }
1040    }
1041}
1042
1043fn sink_words<const N: usize>(set: &[bool]) -> [u64; N] {
1044    let mut words = [0; N];
1045    for (cpu, hot) in set.iter().take(N * 64).enumerate() {
1046        if *hot {
1047            words[cpu / 64] |= 1u64 << (cpu % 64);
1048        }
1049    }
1050    words
1051}
1052
1053/// One host's wake+block+switch hop, as a distribution rather than a mean.
1054struct HandoffProbe {
1055    /// Typical hop — the switch-cost anchor for the preempt-protect window.
1056    median: u64,
1057    /// Tail of a GENUINE handoff: the admission threshold itself, not a base to
1058    /// multiply. A mean-derived cut loses the real handoffs that land above the
1059    /// mean; the dose-response wanted a high firing rate, hence a high percentile.
1060    p99: u64,
1061}
1062
1063/// Per-CPU time spent in interrupt handlers, in kernel ticks, from
1064/// `/proc/stat` (irq + softirq columns, CONFIG_IRQ_TIME_ACCOUNTING). Handler
1065/// TIME is the harm itself: a wake suffers when it lands on a running handler,
1066/// so line counts mis-price it; softirq is included because NAPI shadows wakes.
1067fn read_irq_ticks(nr_cpus: usize) -> Option<Vec<Option<u64>>> {
1068    let text = std::fs::read_to_string("/proc/stat").ok()?;
1069    let mut ticks = vec![None; nr_cpus];
1070    let mut seen = false;
1071    for line in text.lines() {
1072        let mut fields = line.split_whitespace();
1073        let Some(cpu) = fields
1074            .next()
1075            .and_then(|l| l.strip_prefix("cpu"))
1076            .and_then(|n| n.parse::<usize>().ok())
1077        else {
1078            continue;
1079        };
1080        if cpu >= nr_cpus {
1081            continue;
1082        }
1083        // cpuN user nice system idle iowait irq softirq ...
1084        let vals: Vec<u64> = fields.filter_map(|v| v.parse().ok()).collect();
1085        if vals.len() < 7 {
1086            continue;
1087        }
1088        ticks[cpu] = Some(vals[5] + vals[6]);
1089        seen = true;
1090    }
1091    seen.then_some(ticks)
1092}
1093
1094/// The highest CPU id in a sysfs cpulist ("0-15", "0-3,8-11", "0").
1095fn last_cpu_id(list: &str) -> Option<usize> {
1096    list.trim()
1097        .rsplit(',')
1098        .next()?
1099        .rsplit('-')
1100        .next()?
1101        .parse()
1102        .ok()
1103}
1104
1105/// The CPUs `/proc/stat` reported in both reads, and their handler-time
1106/// deltas in that order. Only those are ranked: a possible-but-offline id has
1107/// no handler time and would push the whole online set above the widest gap.
1108fn sink_deltas(current: &[Option<u64>], before: &[Option<u64>]) -> (Vec<usize>, Vec<u64>) {
1109    current
1110        .iter()
1111        .zip(before)
1112        .enumerate()
1113        .filter_map(|(cpu, (c, b))| Some((cpu, (*c)?.checked_sub((*b)?)?)))
1114        .unzip()
1115}
1116
1117/// Split the host's handler-time distribution at its widest gap: sinks are the
1118/// CPUs above the cut. No unit-carrying threshold, so it adapts to any host. A
1119/// zero delta enters at half a tick so every ratio stays finite. A flat set has
1120/// no sinks; a cut condemning half the machine is load, not affinity (None).
1121fn sinks_by_widest_gap(deltas: &[u64]) -> Option<Vec<bool>> {
1122    let mut ranked: Vec<(f64, usize)> = deltas
1123        .iter()
1124        .enumerate()
1125        .map(|(cpu, &d)| ((d as f64).max(0.5), cpu))
1126        .collect();
1127    ranked.sort_by(|a, b| b.0.total_cmp(&a.0));
1128
1129    let mut cut = 0;
1130    let mut widest = 1.0f64;
1131    for i in 1..ranked.len() {
1132        let ratio = ranked[i - 1].0 / ranked[i].0;
1133        if ratio > widest {
1134            widest = ratio;
1135            cut = i;
1136        }
1137    }
1138    if cut == 0 {
1139        return Some(vec![false; deltas.len()]);
1140    }
1141    if cut * 2 >= deltas.len() {
1142        return None;
1143    }
1144
1145    let mut hot = vec![false; deltas.len()];
1146    for &(_, cpu) in &ranked[..cut] {
1147        hot[cpu] = true;
1148    }
1149    Some(hot)
1150}
1151
1152/// Live IRQ-sink tracking riding the 1 s run loop. Each window the handler-time
1153/// deltas are split at their widest gap; a CPU above the cut for FLAG_POLLS
1154/// windows is published, released after UNFLAG_POLLS below it (a loading screen
1155/// must not flap the mask). An unchanged set doubles the interval up to
1156/// INTERVAL_MAX; a disagreeing window resets to tick rate. All four are counts.
1157struct SinkMonitor {
1158    /// Last accepted read; deltas span the full gap between accepted reads,
1159    /// so a slower cadence measures a longer, smoother window.
1160    prev: Option<Vec<Option<u64>>>,
1161    /// The set currently pushed to the scheduler.
1162    published: Vec<bool>,
1163    /// Consecutive windows each CPU has ranked above the cut.
1164    hot_streak: Vec<u32>,
1165    /// Consecutive windows each published sink has ranked below the cut.
1166    quiet: Vec<u32>,
1167    /// Windows since the published set last changed.
1168    stable: u32,
1169    /// Current sampling interval in run-loop ticks.
1170    interval: u32,
1171    ticks: u32,
1172}
1173
1174impl SinkMonitor {
1175    /// Unchanged windows that earn an interval doubling.
1176    const STABLE_POLLS: u32 = 8;
1177    /// Sampling never slows past this many ticks: one window of lag before a
1178    /// moved sink is seen, then FLAG_POLLS or UNFLAG_POLLS at tick rate.
1179    const INTERVAL_MAX: u32 = 4;
1180    /// Above-cut windows before a CPU is published.
1181    const FLAG_POLLS: u32 = 2;
1182    /// Below-cut windows before a published sink is removed.
1183    const UNFLAG_POLLS: u32 = 3;
1184
1185    fn new(nr_cpus: usize) -> Self {
1186        Self {
1187            prev: None,
1188            published: vec![false; nr_cpus],
1189            hot_streak: vec![0; nr_cpus],
1190            quiet: vec![0; nr_cpus],
1191            stable: 0,
1192            interval: 1,
1193            ticks: 0,
1194        }
1195    }
1196
1197    /// One run-loop tick; Some(set) when the published set changed.
1198    fn tick(&mut self, nr_cpus: usize) -> Option<Vec<bool>> {
1199        self.ticks = self.ticks.wrapping_add(1);
1200        if !self.ticks.is_multiple_of(self.interval) {
1201            return None;
1202        }
1203        self.observe(read_irq_ticks(nr_cpus))
1204    }
1205
1206    /// Unknown windows are not consecutive evidence for either direction.
1207    /// Keep the last published preference, but restart confidence promptly.
1208    fn uncertain(&mut self) {
1209        self.hot_streak.fill(0);
1210        self.quiet.fill(0);
1211        self.stable = 0;
1212        self.interval = 1;
1213    }
1214
1215    fn observe(&mut self, sample: Option<Vec<Option<u64>>>) -> Option<Vec<bool>> {
1216        let Some(sample) = sample else {
1217            self.prev = None;
1218            self.uncertain();
1219            return None;
1220        };
1221        let before = self.prev.replace(sample)?;
1222        let current = self.prev.as_ref()?;
1223        let (present, deltas) = sink_deltas(current, &before);
1224        if present.is_empty() {
1225            self.uncertain();
1226            return None;
1227        }
1228        let Some(ranked) = sinks_by_widest_gap(&deltas) else {
1229            self.uncertain();
1230            return None;
1231        };
1232        let mut hot = vec![false; self.published.len()];
1233        for (&cpu, &is_hot) in present.iter().zip(&ranked) {
1234            if cpu < hot.len() {
1235                hot[cpu] = is_hot;
1236            }
1237        }
1238
1239        let mut changed = false;
1240        let mut disagree = false;
1241        for (cpu, &is_hot) in hot.iter().enumerate() {
1242            if present.binary_search(&cpu).is_err() {
1243                self.hot_streak[cpu] = 0;
1244                self.quiet[cpu] = 0;
1245                continue;
1246            }
1247            disagree |= is_hot != self.published[cpu];
1248            if is_hot {
1249                self.quiet[cpu] = 0;
1250                self.hot_streak[cpu] = (self.hot_streak[cpu] + 1).min(Self::FLAG_POLLS);
1251                // The cut wanders under load, so flagged CPUs accrete across polls; bound
1252                // the UNION here so the published set stays a strict minority.
1253                if !self.published[cpu]
1254                    && self.hot_streak[cpu] >= Self::FLAG_POLLS
1255                    && (self.published.iter().filter(|h| **h).count() + 1) * 2 < present.len()
1256                {
1257                    self.published[cpu] = true;
1258                    changed = true;
1259                }
1260            } else {
1261                self.hot_streak[cpu] = 0;
1262                if self.published[cpu] {
1263                    self.quiet[cpu] += 1;
1264                    if self.quiet[cpu] >= Self::UNFLAG_POLLS {
1265                        self.published[cpu] = false;
1266                        self.quiet[cpu] = 0;
1267                        changed = true;
1268                    }
1269                }
1270            }
1271        }
1272
1273        if changed {
1274            self.stable = 0;
1275            self.interval = 1;
1276            return Some(self.published.clone());
1277        }
1278        if disagree {
1279            // A rank the published set does not show yet: confirm or
1280            // dismiss it at tick rate instead of after the slowed window.
1281            self.stable = 0;
1282            self.interval = 1;
1283            return None;
1284        }
1285        self.stable += 1;
1286        if self.stable >= Self::STABLE_POLLS {
1287            self.stable = 0;
1288            self.interval = (self.interval * 2).min(Self::INTERVAL_MAX);
1289        }
1290        None
1291    }
1292}
1293
1294/// The tick period from the coarse clock's resolution, which the kernel sets
1295/// to one jiffy. 0 when the resolution is not a plausible tick (a VM or an
1296/// odd clocksource): the precise task-age clock is used instead.
1297fn coarse_tick_ns() -> u64 {
1298    const TICK_MIN_NS: u64 = 500_000;
1299    const TICK_MAX_NS: u64 = 20_000_000;
1300    let mut ts = libc::timespec {
1301        tv_sec: 0,
1302        tv_nsec: 0,
1303    };
1304    // SAFETY: clock_getres writes only the timespec it is given.
1305    if unsafe { libc::clock_getres(libc::CLOCK_MONOTONIC_COARSE, &mut ts) } != 0 {
1306        return 0;
1307    }
1308    let ns = ts.tv_sec as u64 * 1_000_000_000 + ts.tv_nsec as u64;
1309    if (TICK_MIN_NS..=TICK_MAX_NS).contains(&ns) {
1310        ns
1311    } else {
1312        0
1313    }
1314}
1315
1316/// cpuidle exit latency per state index (ns, enabled states only) from cpu0's
1317/// sysfs, and the driver name; empty on a host without a driver.
1318fn cpuidle_states() -> (Option<String>, Vec<u64>) {
1319    let base = std::path::Path::new("/sys/devices/system/cpu");
1320    let driver = std::fs::read_to_string(base.join("cpuidle/current_driver"))
1321        .ok()
1322        .map(|s| s.trim().to_string())
1323        .filter(|s| !s.is_empty() && s != "none");
1324    let mut states = Vec::new();
1325    if driver.is_some() {
1326        let mut idx = 0;
1327        while let Ok(lat) =
1328            std::fs::read_to_string(base.join(format!("cpu0/cpuidle/state{idx}/latency")))
1329        {
1330            let disabled =
1331                std::fs::read_to_string(base.join(format!("cpu0/cpuidle/state{idx}/disable")))
1332                    .is_ok_and(|d| d.trim() == "1");
1333            let us: u64 = lat.trim().parse().unwrap_or(0);
1334            states.push(if disabled { 0 } else { us * 1000 });
1335            idx += 1;
1336        }
1337    }
1338    (driver, states)
1339}
1340
1341/// The AMD 3D V-cache mode ("cache" or "frequency") when the platform driver
1342/// exposes it.
1343fn amd_x3d_mode() -> Option<String> {
1344    let dir = std::fs::read_dir("/sys/bus/platform/drivers/amd_x3d_vcache").ok()?;
1345    for entry in dir.flatten() {
1346        if let Ok(mode) = std::fs::read_to_string(entry.path().join("amd_x3d_mode")) {
1347            return Some(mode.trim().to_string());
1348        }
1349    }
1350    None
1351}
1352
1353/// Per-LLC idle-word masks and the LLC each mode prefers: the die with the most
1354/// high-capacity cores, else the largest cache under mode "cache", else the
1355/// highest summed platform preference. Multi-LLC hosts within one word only.
1356struct PreferredDie {
1357    cache: u64,
1358    frequency: u64,
1359}
1360
1361fn preferred_die(
1362    topo: &Topology,
1363    perf: &core_performance::CorePerformanceLayout,
1364) -> Option<PreferredDie> {
1365    if topo.all_llcs.len() < 2 || *NR_CPU_IDS > 64 {
1366        return None;
1367    }
1368    let mut word: BTreeMap<usize, u64> = BTreeMap::new();
1369    let mut big: BTreeMap<usize, u32> = BTreeMap::new();
1370    let mut cache: BTreeMap<usize, usize> = BTreeMap::new();
1371    for cpu in topo.all_cpus.values() {
1372        *word.entry(cpu.llc_id).or_insert(0) |= 1u64 << cpu.id;
1373        if cpu.core_type != scx_utils::CoreType::Little {
1374            *big.entry(cpu.llc_id).or_insert(0) += 1;
1375        }
1376        let e = cache.entry(cpu.llc_id).or_insert(0);
1377        *e = (*e).max(cpu.cache_size);
1378    }
1379    let mut pref: BTreeMap<usize, u64> = BTreeMap::new();
1380    for core in &perf.cores {
1381        *pref.entry(core.identity.llc).or_insert(0) += u64::from(core.preference.unwrap_or(0));
1382    }
1383    let best = |key: &dyn Fn(&usize) -> u64| -> u64 {
1384        word.keys()
1385            .max_by_key(|l| (key(l), std::cmp::Reverse(**l)))
1386            .map_or(0, |l| word[l])
1387    };
1388    let by_big = best(&|l| u64::from(big.get(l).copied().unwrap_or(0)));
1389    let big_differs = big.values().max() != big.values().min();
1390    let by_cache = best(&|l| cache.get(l).copied().unwrap_or(0) as u64);
1391    let by_pref = best(&|l| pref.get(l).copied().unwrap_or(0));
1392    let all: u64 = word.values().fold(0, |a, w| a | w);
1393    let cache_differs = cache.values().max() != cache.values().min();
1394    Some(if big_differs {
1395        PreferredDie {
1396            cache: by_big,
1397            frequency: by_big,
1398        }
1399    } else if cache_differs && word.len() == 2 {
1400        // A V-cache pair: the platform re-ranks cores with the mode, so the
1401        // frequency die is the other die, not the ranking read at start.
1402        PreferredDie {
1403            cache: by_cache,
1404            frequency: all & !by_cache,
1405        }
1406    } else {
1407        PreferredDie {
1408            cache: by_cache,
1409            frequency: by_pref,
1410        }
1411    })
1412}
1413
1414fn preferred_word(die: &PreferredDie, mode: Option<&str>) -> u64 {
1415    if mode == Some("cache") {
1416        die.cache
1417    } else {
1418        die.frequency
1419    }
1420}
1421
1422/// CPUs online now, from sysfs; every present CPU when the file is unreadable.
1423fn online_cpu_set(topo: &Topology) -> std::collections::BTreeSet<usize> {
1424    let all: std::collections::BTreeSet<usize> = topo.all_cpus.keys().copied().collect();
1425    let Ok(text) = std::fs::read_to_string("/sys/devices/system/cpu/online") else {
1426        return all;
1427    };
1428    let mut set = std::collections::BTreeSet::new();
1429    for part in text.trim().split(',') {
1430        let (lo, hi) = match part.split_once('-') {
1431            Some((a, b)) => (a.parse::<usize>(), b.parse::<usize>()),
1432            None => (part.parse::<usize>(), part.parse::<usize>()),
1433        };
1434        if let (Ok(lo), Ok(hi)) = (lo, hi) {
1435            set.extend(lo..=hi);
1436        }
1437    }
1438    if set.is_empty() { all } else { set }
1439}
1440
1441/// Measure one wake + block + switch hop on THIS host: two threads ping-pong
1442/// through a condvar, the blocking handoff `cake_handoff_max_ns` is a proxy
1443/// for. Runs before attach, so it measures the stock kernel path; ~2200 round
1444/// trips. Returns ns per one-way hop, or None, and callers keep the defaults.
1445fn probe_handoff_hop_ns() -> Option<HandoffProbe> {
1446    const WARMUP: u32 = 200;
1447    const ITERS: u32 = 2_000;
1448    let total = WARMUP + ITERS;
1449
1450    let pair = Arc::new((std::sync::Mutex::new(0u32), std::sync::Condvar::new()));
1451    let peer = Arc::clone(&pair);
1452
1453    let responder = std::thread::Builder::new()
1454        .name("cake-probe".into())
1455        .spawn(move || {
1456            let (lock, cv) = &*peer;
1457            let mut turn = lock.lock().ok()?;
1458            for _ in 0..total {
1459                while turn.is_multiple_of(2) {
1460                    turn = cv.wait(turn).ok()?;
1461                }
1462                *turn = turn.wrapping_add(1);
1463                cv.notify_one();
1464            }
1465            Some(())
1466        })
1467        .ok()?;
1468
1469    let mut hops: Vec<u64> = Vec::with_capacity(ITERS as usize);
1470    {
1471        let (lock, cv) = &*pair;
1472        let mut turn = lock.lock().ok()?;
1473        for i in 0..total {
1474            let t0 = std::time::Instant::now();
1475            *turn = turn.wrapping_add(1);
1476            cv.notify_one();
1477            while *turn % 2 == 1 {
1478                turn = cv.wait(turn).ok()?;
1479            }
1480            if i >= WARMUP {
1481                // Two hops per round trip: our wake of the peer, its wake of us.
1482                hops.push(t0.elapsed().as_nanos() as u64 / 2);
1483            }
1484        }
1485    }
1486    responder.join().ok()?;
1487
1488    if hops.len() < ITERS as usize / 2 {
1489        return None;
1490    }
1491    hops.sort_unstable();
1492    let median = hops[hops.len() / 2];
1493    let p99 = hops[hops.len() * 99 / 100];
1494    if median == 0 || p99 == 0 {
1495        return None;
1496    }
1497    Some(HandoffProbe { median, p99 })
1498}
1499
1500/// Parse the command line, dropping options cake does not have instead of
1501/// refusing to start (a legacy scx_loader flag must not keep the scheduler
1502/// off); dropped tokens are reported once logging is up. Real errors keep clap's.
1503fn parse_opts_lenient(
1504    args: impl IntoIterator<Item = impl Into<std::ffi::OsString>>,
1505) -> std::result::Result<(Opts, Vec<String>), clap::Error> {
1506    use clap::error::ContextKind;
1507    use clap::error::ErrorKind;
1508
1509    let mut args: Vec<std::ffi::OsString> = args.into_iter().map(Into::into).collect();
1510    let mut dropped: Vec<String> = Vec::new();
1511    loop {
1512        match Opts::try_parse_from(&args) {
1513            Ok(opts) => return Ok((opts, dropped)),
1514            Err(e) if e.kind() == ErrorKind::UnknownArgument => {
1515                let Some(bad) = e
1516                    .get(ContextKind::InvalidArg)
1517                    .map(|v| v.to_string())
1518                    .filter(|s| !s.is_empty())
1519                else {
1520                    return Err(e);
1521                };
1522                // The offending token, matched on the option name so that
1523                // both `--x=v` and `--x v` forms are found.
1524                let name = bad.split('=').next().unwrap_or(&bad).to_string();
1525                let short = name
1526                    .strip_prefix('-')
1527                    .filter(|s| s.chars().count() == 1)
1528                    .and_then(|s| s.chars().next());
1529                let Some((pos, split)) = args.iter().enumerate().skip(1).find_map(|(pos, a)| {
1530                    let s = a.to_string_lossy();
1531                    if s == bad || s == name || s.starts_with(&format!("{name}=")) {
1532                        return Some((pos, 0));
1533                    }
1534                    // Clap reports only `-p` for `-pgaming` or `-vpgaming`.
1535                    // Keep the accepted prefix; the unknown option owns the
1536                    // suffix, which may be its attached value.
1537                    if s.starts_with('-')
1538                        && !s.starts_with("--")
1539                        && let Some(short) = short
1540                    {
1541                        return s
1542                            .char_indices()
1543                            .skip(1)
1544                            .find_map(|(split, c)| (c == short).then_some((pos, split)));
1545                    }
1546                    None
1547                }) else {
1548                    return Err(e);
1549                };
1550                let mut tok = args.remove(pos).to_string_lossy().into_owned();
1551                let mut next_pos = pos;
1552                if split > 0 {
1553                    if split > 1 {
1554                        args.insert(pos, tok[..split].into());
1555                        next_pos += 1;
1556                    }
1557                    tok = format!("-{}", &tok[split..]);
1558                }
1559                // `--x v`: the bare value that follows is part of the same
1560                // mistake; a token starting with `-` is another option.
1561                if tok == name && tok.starts_with('-') && next_pos < args.len() {
1562                    let next = args[next_pos].to_string_lossy();
1563                    if !next.starts_with('-') {
1564                        tok.push(' ');
1565                        tok.push_str(&next);
1566                        args.remove(next_pos);
1567                    }
1568                }
1569                dropped.push(tok);
1570            }
1571            Err(e) => return Err(e),
1572        }
1573    }
1574}
1575
1576/// Drop the calling thread's capability sets and restore process inspection.
1577/// Call after privileged setup. Earlier-created threads retain their own
1578/// capabilities; a subsequent privileged initialization requires re-exec.
1579fn drop_privileges_for_observers() -> std::io::Result<()> {
1580    #[repr(C)]
1581    struct CapHeader {
1582        version: u32,
1583        pid: i32,
1584    }
1585    #[repr(C)]
1586    #[derive(Clone, Copy)]
1587    struct CapData {
1588        effective: u32,
1589        permitted: u32,
1590        inheritable: u32,
1591    }
1592    let header = CapHeader {
1593        version: 0x2008_0522,
1594        pid: 0,
1595    };
1596    let data = [CapData {
1597        effective: 0,
1598        permitted: 0,
1599        inheritable: 0,
1600    }; 2];
1601    if unsafe { libc::syscall(libc::SYS_capset, &header, data.as_ptr()) } != 0 {
1602        return Err(std::io::Error::last_os_error());
1603    }
1604    if unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 1, 0, 0, 0) } != 0 {
1605        return Err(std::io::Error::last_os_error());
1606    }
1607    Ok(())
1608}
1609
1610/// Restore file capabilities for a requested restart after dropping the link.
1611fn reexec_self() -> Result<()> {
1612    use std::os::unix::process::CommandExt;
1613    let exe = std::fs::read_link("/proc/self/exe")
1614        .context("failed to resolve /proc/self/exe for restart")?;
1615    let err = std::process::Command::new(exe)
1616        .args(std::env::args_os().skip(1))
1617        .exec();
1618    Err(anyhow::Error::new(err).context("re-exec after kernel restart request failed"))
1619}
1620
1621fn main() -> Result<()> {
1622    let (opts, ignored_args) = parse_opts_lenient(std::env::args_os()).unwrap_or_else(|e| e.exit());
1623
1624    let mut lcfg = simplelog::ConfigBuilder::new();
1625    lcfg.set_time_level(simplelog::LevelFilter::Error)
1626        .set_location_level(simplelog::LevelFilter::Off)
1627        .set_target_level(simplelog::LevelFilter::Off)
1628        .set_thread_level(simplelog::LevelFilter::Off);
1629    simplelog::TermLogger::init(
1630        simplelog::LevelFilter::Info,
1631        lcfg.build(),
1632        simplelog::TerminalMode::Stderr,
1633        simplelog::ColorChoice::Auto,
1634    )?;
1635    for a in &ignored_args {
1636        warn!("   args    ignored unknown option `{a}`; cake has no such option, defaults used");
1637    }
1638
1639    if opts.version {
1640        println!(
1641            "{} {}",
1642            SCHEDULER_NAME,
1643            build_id::full_version(env!("CARGO_PKG_VERSION"))
1644        );
1645        match build_identity::binary_sha256() {
1646            Ok(hash) => println!("Build SHA256: {hash}"),
1647            Err(err) => println!("Build SHA256: unavailable ({err:#})"),
1648        }
1649        return Ok(());
1650    }
1651    if opts.print_topology {
1652        let topo = Topology::new().context("failed to read topology")?;
1653        let layout = core_performance::CorePerformanceLayout::discover(&topo);
1654        println!("{}", layout.summary());
1655        for core in layout.details() {
1656            println!("{core}");
1657        }
1658        return Ok(());
1659    }
1660
1661    let shutdown = Arc::new(AtomicBool::new(false));
1662    let shutdown_clone = shutdown.clone();
1663    ctrlc::set_handler(move || {
1664        // Terminal Ctrl+C echo has no newline. Finish that line before the
1665        // run loop starts logging shutdown; leave redirected logs untouched.
1666        if std::io::stderr().is_terminal() {
1667            eprintln!();
1668        }
1669        shutdown_clone.store(true, Ordering::Relaxed);
1670    })
1671    .context("Error setting Ctrl-C handler")?;
1672
1673    let mut open_object = MaybeUninit::uninit();
1674    let mut sched = Scheduler::init(&opts, &mut open_object)?;
1675
1676    if sched.run(shutdown.clone())?.should_restart() {
1677        info!("Restart requested by the kernel — re-executing");
1678        reexec_self()?;
1679    }
1680
1681    Ok(())
1682}
1683
1684/// Factor the narrow sibling map into two shifts when every edge has the
1685/// same distance. Irregular numbering keeps the exact per-CPU lookup.
1686fn smt_fold(siblings: &[i32]) -> Option<(u32, u64, u64)> {
1687    let (mut shift, mut left, mut right) = (0, 0, 0);
1688    for (cpu, &sib) in siblings.iter().take(64).enumerate() {
1689        if !(0..64).contains(&sib) || sib as usize == cpu {
1690            continue;
1691        }
1692        let distance = (cpu as u32).abs_diff(sib as u32);
1693        if shift != 0 && shift != distance {
1694            return None;
1695        }
1696        shift = distance;
1697        if cpu < sib as usize {
1698            left |= 1u64 << cpu;
1699        } else {
1700            right |= 1u64 << cpu;
1701        }
1702    }
1703    Some((shift, left, right))
1704}
1705
1706/// The LLC topology the loader publishes to rodata, built from
1707/// (cpu id, llc id) pairs so any host shape can be tested without a machine.
1708struct LlcLayout {
1709    /// Physical identity is never collapsed to the bounded pool namespace.
1710    domain: BTreeMap<usize, u16>,
1711    /// Dense pool index per topology LLC id; every id maps, ids are
1712    /// assigned in ascending LLC order.
1713    dense: BTreeMap<usize, u8>,
1714    /// The CPUs (ids below 64) sharing each LLC, as one word. An LLC
1715    /// with no CPU below 64 gets all ones: the census paths that read the
1716    /// word only run inside one word, and all ones is the LLC-blind walk.
1717    word: BTreeMap<usize, u64>,
1718    nr_llcs: u32,
1719    /// Set when more LLCs exist than pools: everything went to pool 0.
1720    collapsed: Option<usize>,
1721}
1722
1723impl LlcLayout {
1724    fn build(cpus: &[(usize, usize)], max_llcs: usize) -> Self {
1725        let mut word: BTreeMap<usize, u64> = BTreeMap::new();
1726        for (cpu, llc) in cpus {
1727            let w = word.entry(*llc).or_insert(0);
1728            if *cpu < 64 {
1729                *w |= 1u64 << cpu;
1730            }
1731        }
1732        for w in word.values_mut() {
1733            if *w == 0 {
1734                *w = u64::MAX;
1735            }
1736        }
1737        let n = word.len();
1738        let collapsed = (n > max_llcs).then_some(n);
1739        let domain = word
1740            .keys()
1741            .enumerate()
1742            .map(|(i, llc)| (*llc, i as u16))
1743            .collect();
1744        let dense: BTreeMap<usize, u8> = word
1745            .keys()
1746            .enumerate()
1747            .map(|(i, llc)| (*llc, if collapsed.is_some() { 0 } else { i as u8 }))
1748            .collect();
1749        let nr_llcs = if collapsed.is_some() {
1750            1
1751        } else {
1752            n.max(1) as u32
1753        };
1754        Self {
1755            domain,
1756            dense,
1757            word,
1758            nr_llcs,
1759            collapsed,
1760        }
1761    }
1762}
1763
1764#[cfg(test)]
1765mod tests {
1766    #[test]
1767    fn legacy_profiles_use_default_options() {
1768        let (defaults, _) = super::parse_opts_lenient(["scx_cake"]).unwrap();
1769        for args in [
1770            vec!["--profile", "gaming"],
1771            vec!["--profile=performance"],
1772            vec!["-p", "powersave"],
1773            vec!["-pperformance"],
1774            vec!["--profile"],
1775            vec!["--performance", "--powersave"],
1776        ] {
1777            let (opts, dropped) =
1778                super::parse_opts_lenient(std::iter::once("scx_cake").chain(args.clone())).unwrap();
1779            assert_eq!(opts, defaults, "{args:?}");
1780            assert!(!dropped.is_empty(), "{args:?}");
1781        }
1782    }
1783
1784    #[test]
1785    fn legacy_options_preserve_supported_arguments() {
1786        let (expected, _) =
1787            super::parse_opts_lenient(["scx_cake", "-v", "--toggle", "g85=0", "--print-topology"])
1788                .unwrap();
1789        let (opts, dropped) = super::parse_opts_lenient([
1790            "scx_cake",
1791            "--profile",
1792            "gaming",
1793            "-v",
1794            "--obsolete=42",
1795            "--toggle",
1796            "g85=0",
1797            "--powersave",
1798            "--print-topology",
1799        ])
1800        .unwrap();
1801        assert_eq!(opts, expected);
1802        assert_eq!(
1803            dropped,
1804            ["--profile gaming", "--obsolete=42", "--powersave"]
1805        );
1806    }
1807
1808    #[test]
1809    fn legacy_short_option_preserves_cluster_prefix() {
1810        let (opts, dropped) =
1811            super::parse_opts_lenient(["scx_cake", "-vpperformance", "--toggle=g85=0"]).unwrap();
1812        assert!(opts.verbose);
1813        assert_eq!(opts.toggle, ["g85=0"]);
1814        assert_eq!(dropped, ["-pperformance"]);
1815    }
1816
1817    #[test]
1818    fn supported_option_errors_and_help_remain_errors() {
1819        use clap::error::ErrorKind;
1820        for (args, kind) in [
1821            (vec!["--toggle"], ErrorKind::InvalidValue),
1822            (vec!["--verbose=garbage"], ErrorKind::TooManyValues),
1823            (
1824                vec!["--profile", "gaming", "--help"],
1825                ErrorKind::DisplayHelp,
1826            ),
1827        ] {
1828            let err = super::parse_opts_lenient(std::iter::once("scx_cake").chain(args.clone()))
1829                .unwrap_err();
1830            assert_eq!(err.kind(), kind, "{args:?}");
1831        }
1832    }
1833
1834    #[test]
1835    fn drop_privileges_in_child() {
1836        const CHILD: &str = "SCX_CAKE_OBSERVER_TEST_CHILD";
1837        if std::env::var_os(CHILD).is_none() {
1838            let status = std::process::Command::new(std::env::current_exe().unwrap())
1839                .args(["--exact", "tests::drop_privileges_in_child"])
1840                .env(CHILD, "1")
1841                .status()
1842                .unwrap();
1843            assert!(status.success());
1844            return;
1845        }
1846        super::drop_privileges_for_observers().unwrap();
1847        let status = std::fs::read_to_string("/proc/thread-self/status").unwrap();
1848        for set in ["CapInh:", "CapPrm:", "CapEff:"] {
1849            let value = status.lines().find(|line| line.starts_with(set)).unwrap();
1850            assert_eq!(
1851                u64::from_str_radix(value.split_whitespace().nth(1).unwrap(), 16).unwrap(),
1852                0
1853            );
1854        }
1855        assert_eq!(unsafe { libc::prctl(libc::PR_GET_DUMPABLE, 0, 0, 0, 0) }, 1);
1856    }
1857
1858    #[test]
1859    fn smt_fold_matches_every_single_cpu_mapping() {
1860        let mut siblings = [-1; 64];
1861        assert_eq!(super::smt_fold(&siblings), Some((0, 0, 0)));
1862        // Covers adjacent, split-half, sparse and one-way maps, including
1863        // the maximum legal shift. OR-linearity extends these checks to
1864        // every possible combination of reserved CPUs.
1865        for distance in 1..64 {
1866            for (cpu, sib) in siblings.iter_mut().enumerate() {
1867                *sib = if cpu + distance < 64 {
1868                    (cpu + distance) as i32
1869                } else if cpu >= distance {
1870                    (cpu - distance) as i32
1871                } else {
1872                    -1
1873                };
1874            }
1875            let (shift, left, right) = super::smt_fold(&siblings).unwrap();
1876            for (cpu, &sib) in siblings.iter().enumerate() {
1877                let bit = 1u64 << cpu;
1878                let actual = bit | ((bit & left) << shift) | ((bit & right) >> shift);
1879                let expected = bit | if sib >= 0 { 1u64 << sib } else { 0 };
1880                assert_eq!(actual, expected);
1881            }
1882        }
1883        siblings.fill(-1);
1884        siblings[0] = 1;
1885        siblings[1] = 0;
1886        siblings[2] = 4;
1887        assert_eq!(super::smt_fold(&siblings), None);
1888        siblings.fill(64); // beyond-word edges do not affect the narrow mask
1889        assert_eq!(super::smt_fold(&siblings), Some((0, 0, 0)));
1890    }
1891
1892    #[test]
1893    fn idle_and_release_callbacks_are_registered() {
1894        use libbpf_rs::skel::SkelBuilder;
1895        use std::mem::MaybeUninit;
1896
1897        let mut object = MaybeUninit::uninit();
1898        let builder = super::BpfSkelBuilder::default();
1899        let mut skel = builder.open(&mut object).expect("open embedded BPF object");
1900        assert!(!skel.struct_ops.cake_ops_mut().update_idle.is_null());
1901        assert!(!skel.struct_ops.cake_ops_mut().cpu_release.is_null());
1902        assert!(!skel.struct_ops.cake_ops_mut().disable.is_null());
1903        assert!(skel.progs.cake_cpu_release.autoload());
1904    }
1905
1906    use super::{
1907        LlcLayout, SinkMonitor, attach_irq_pair, last_cpu_id, sink_deltas, sink_words,
1908        sinks_by_widest_gap,
1909    };
1910
1911    // Load only: synthetic topology exposes paths pruned on the build host.
1912    // Never attach struct_ops or tracepoints, or execute the synthetic policy.
1913    #[test]
1914    #[ignore = "requires BPF load capabilities and a sched_ext kernel"]
1915    fn verifier_load_topologies() -> anyhow::Result<()> {
1916        use super::BpfSkelBuilder;
1917        use scx_utils::{scx_ops_load, scx_ops_open};
1918        use std::mem::MaybeUninit;
1919
1920        libbpf_rs::set_print(Some((libbpf_rs::PrintLevel::Warn, |_, message| {
1921            eprint!("{message}");
1922        })));
1923        let mut failures = Vec::new();
1924        let steal_span = super::bpf_intf::consts_STEAL_SPAN as usize;
1925        for (cpus, llcs) in [
1926            (1usize, 1usize),
1927            (16, 1),
1928            (32, 1),
1929            (32, 2),
1930            (64, 16),
1931            (48, 4),
1932            (96, 4),
1933            (128, 8),
1934            (128, 1),
1935            (1024, 1),
1936        ] {
1937            let mut object = MaybeUninit::uninit();
1938            let builder = BpfSkelBuilder::default();
1939            let mut skel = scx_ops_open!(builder, &mut object, cake_ops, None)?;
1940            let one_word = cpus <= 64;
1941            let cores = cpus.div_ceil(2);
1942            let ro = skel.maps.rodata_data.as_mut().unwrap();
1943            ro.nr_cpu_span = cpus as u32;
1944            ro.nr_llcs = llcs as u32;
1945            ro.cake_one_word = u8::from(one_word);
1946            ro.cake_rank_tiers = if one_word { cores as u32 } else { 0 };
1947            ro.cake_claim_tries = ((cpus / llcs.max(1)) as u32 / 4).max(4);
1948            ro.cpu_perf_known = if one_word { u64::MAX >> (64 - cpus) } else { 0 };
1949            ro.cpu_sibling.fill(-1);
1950            ro.cpu_llc_domain.fill(u16::MAX);
1951            ro.cake_smt_shift = if cpus == 1 { 0 } else { cores.min(64) as u32 };
1952            if cores < 64 {
1953                ro.cake_smt_left = u64::MAX >> (64 - cores);
1954                ro.cake_smt_right = ro.cake_smt_left << cores;
1955            }
1956            ro.steal_order_live = u8::from(llcs > 1 && cpus <= steal_span);
1957            ro.nr_steal_cpus = (cpus - 1) as u32;
1958            for cpu in 0..cpus {
1959                let domain = (cpu % cores) / (cores / llcs);
1960                if cpus > 1 {
1961                    ro.cpu_sibling[cpu] = (cpu ^ cores) as i32;
1962                }
1963                ro.cpu_llc_id[cpu] = domain as u8;
1964                ro.cpu_llc_domain[cpu] = domain as u16;
1965                if one_word {
1966                    ro.cpu_perf_tier[cpu % cores] |= 1u64 << cpu;
1967                }
1968                for peer in 0..cpus.min(64) {
1969                    if (peer % cores) / (cores / llcs) == domain {
1970                        ro.cpu_llc_word[cpu] |= 1u64 << peer;
1971                    }
1972                }
1973                if ro.steal_order_live != 0 {
1974                    for peer in 1..cpus {
1975                        ro.cpu_steal_order[cpu * steal_span + peer - 1] =
1976                            ((cpu + peer) % cpus) as u16;
1977                    }
1978                }
1979            }
1980            match scx_ops_load!(skel, cake_ops, uei) {
1981                Ok(loaded) => {
1982                    eprintln!("verifier accepted: {cpus} CPUs, {llcs} LLCs");
1983                    drop(loaded);
1984                }
1985                Err(error) => failures.push(format!("{cpus} CPUs, {llcs} LLCs: {error:#}")),
1986            };
1987        }
1988        anyhow::ensure!(failures.is_empty(), "{}", failures.join("\n"));
1989        Ok(())
1990    }
1991
1992    #[test]
1993    fn irq_publication_preserves_sparse_wide_cpu_ids_and_clears_old_bits() {
1994        let mut set = vec![false; 257];
1995        for cpu in [0, 63, 64, 127, 128, 255, 256] {
1996            set[cpu] = true;
1997        }
1998        assert_eq!(
1999            sink_words::<4>(&set),
2000            [1 | (1 << 63), 1 | (1 << 63), 1, 1 << 63]
2001        );
2002        assert_eq!(sink_words::<4>(&[false; 1]), [0; 4]);
2003        assert_eq!(sink_words::<4>(&[]), [0; 4]);
2004    }
2005
2006    #[test]
2007    fn unknown_irq_windows_break_streaks_without_inventing_quiet() {
2008        let mut monitor = SinkMonitor::new(8);
2009        monitor.published[0] = true;
2010        monitor.hot_streak[1] = 1;
2011        monitor.quiet[0] = 2;
2012        monitor.stable = 7;
2013        monitor.interval = SinkMonitor::INTERVAL_MAX;
2014        assert_eq!(monitor.observe(None), None);
2015        assert!(monitor.published[0]);
2016        assert_eq!(monitor.hot_streak, vec![0; 8]);
2017        assert_eq!(monitor.quiet, vec![0; 8]);
2018        assert_eq!((monitor.stable, monitor.interval), (0, 1));
2019        assert_eq!(monitor.observe(Some(vec![Some(0); 8])), None);
2020        // Half-machine split is untrusted, not a second confirming sample.
2021        monitor.hot_streak[1] = 1;
2022        assert_eq!(
2023            monitor.observe(Some(vec![
2024                Some(100),
2025                Some(100),
2026                Some(100),
2027                Some(100),
2028                Some(0),
2029                Some(0),
2030                Some(0),
2031                Some(0)
2032            ])),
2033            None
2034        );
2035        assert_eq!(monitor.hot_streak, vec![0; 8]);
2036        assert!(monitor.published[0]);
2037    }
2038
2039    #[test]
2040    fn irq_counter_reset_and_missing_cpu_are_unknown_not_zero_load() {
2041        assert_eq!(
2042            sink_deltas(&[Some(2), Some(5)], &[Some(10), Some(3)]),
2043            (vec![1], vec![2])
2044        );
2045        let mut monitor = SinkMonitor::new(8);
2046        monitor.observe(Some(vec![Some(100); 8]));
2047        monitor.published[0] = true;
2048        monitor.quiet[0] = 2;
2049        let mut next = vec![Some(101); 8];
2050        next[0] = None;
2051        monitor.observe(Some(next));
2052        assert!(monitor.published[0]);
2053        assert_eq!(monitor.quiet[0], 0);
2054    }
2055
2056    #[test]
2057    fn irq_rank_disagreement_resets_the_slowed_interval_before_publication() {
2058        let mut monitor = SinkMonitor::new(8);
2059        monitor.observe(Some(vec![Some(0); 8]));
2060        monitor.published[0] = true;
2061        monitor.stable = SinkMonitor::STABLE_POLLS - 1;
2062        monitor.interval = SinkMonitor::INTERVAL_MAX;
2063        // CPU 1 now carries the load: one window is not yet a publication
2064        // (FLAG_POLLS), but the confirmation must come at tick rate.
2065        let mut next = vec![Some(1); 8];
2066        next[1] = Some(1000);
2067        assert_eq!(monitor.observe(Some(next)), None);
2068        assert_eq!((monitor.stable, monitor.interval), (0, 1));
2069        assert!(monitor.published[0]);
2070        assert!(!monitor.published[1]);
2071        // An agreeing window keeps the slow-down bookkeeping.
2072        monitor.published = vec![false; 8];
2073        monitor.published[1] = true;
2074        monitor.hot_streak.fill(0);
2075        let mut next = vec![Some(2); 8];
2076        next[1] = Some(2000);
2077        assert_eq!(monitor.observe(Some(next)), None);
2078        assert_eq!((monitor.stable, monitor.interval), (1, 1));
2079    }
2080    use std::{cell::RefCell, rc::Rc};
2081
2082    #[test]
2083    fn irq_pair_orders_attach_teardown_and_cleans_partial_failures() {
2084        struct Link(&'static str, Rc<RefCell<Vec<&'static str>>>);
2085        impl Drop for Link {
2086            fn drop(&mut self) {
2087                self.1.borrow_mut().push(self.0);
2088            }
2089        }
2090        for fail in [None, Some("leave"), Some("enter")] {
2091            let events = Rc::new(RefCell::new(Vec::new()));
2092            let pair = attach_irq_pair(
2093                || {
2094                    events.borrow_mut().push("attach leave");
2095                    anyhow::ensure!(fail != Some("leave"), "injected");
2096                    Ok(Link("drop leave", events.clone()))
2097                },
2098                || {
2099                    events.borrow_mut().push("attach enter");
2100                    anyhow::ensure!(fail != Some("enter"), "injected");
2101                    Ok(Link("drop enter", events.clone()))
2102                },
2103            );
2104            assert_eq!(pair.is_ok(), fail.is_none());
2105            drop(pair);
2106            let expected = match fail {
2107                None => vec!["attach leave", "attach enter", "drop enter", "drop leave"],
2108                Some("leave") => vec!["attach leave"],
2109                _ => vec!["attach leave", "attach enter", "drop leave"],
2110            };
2111            assert_eq!(*events.borrow(), expected);
2112        }
2113    }
2114
2115    #[test]
2116    fn llc_identity_survives_wide_pool_collapse_and_sparse_llc_ids() {
2117        let cpus: Vec<_> = (0..128).map(|c| (c, 1000 + (c % 64) / 4)).collect();
2118        let l = LlcLayout::build(&cpus, 1);
2119        assert_eq!(l.nr_llcs, 1);
2120        assert_ne!(l.domain[&cpus[0].1], l.domain[&cpus[80].1]);
2121        assert_eq!(l.domain[&cpus[0].1], l.domain[&cpus[64].1]);
2122        assert!(l.dense.values().all(|d| *d == 0));
2123    }
2124
2125    #[test]
2126    fn cpulist_last_id() {
2127        assert_eq!(last_cpu_id("0-15\n"), Some(15));
2128        assert_eq!(last_cpu_id("0-3,8-11"), Some(11));
2129        assert_eq!(last_cpu_id("0"), Some(0));
2130        assert_eq!(last_cpu_id(""), None);
2131    }
2132
2133    fn grid(nr_cpus: usize, per_llc: usize) -> Vec<(usize, usize)> {
2134        (0..nr_cpus).map(|c| (c, c / per_llc)).collect()
2135    }
2136
2137    #[test]
2138    fn offline_possible_ids_are_not_ranked() {
2139        // Eight online CPUs at equal handler time and 56 possible-but-
2140        // offline ids: ranked as zeros they would make every online CPU a
2141        // sink. Absent ids leave the ranking instead.
2142        let before: Vec<Option<u64>> = (0..64).map(|c| (c < 8).then_some(100)).collect();
2143        let current: Vec<Option<u64>> = (0..64).map(|c| (c < 8).then_some(103)).collect();
2144        let (present, deltas) = sink_deltas(&current, &before);
2145        assert_eq!(present, (0..8).collect::<Vec<_>>());
2146        assert_eq!(deltas, vec![3; 8]);
2147        assert_eq!(sinks_by_widest_gap(&deltas).unwrap(), vec![false; 8]);
2148    }
2149
2150    #[test]
2151    fn llc_layout_counts_domains_above_cpu_63() {
2152        // 128 CPUs in 16 LLCs of 8: every LLC is a pool, and a CPU above
2153        // 63 lands in its own LLC's pool, not pool 0.
2154        let l = LlcLayout::build(&grid(128, 8), 16);
2155        assert_eq!(l.nr_llcs, 16);
2156        assert!(l.collapsed.is_none());
2157        assert_eq!(l.dense[&9], 9);
2158        assert_eq!(l.word[&0], 0xff);
2159        assert_eq!(l.word[&7], 0xff << 56);
2160        // No CPU of LLC 9 fits a word: LLC-blind for the census paths.
2161        assert_eq!(l.word[&9], u64::MAX);
2162    }
2163
2164    #[test]
2165    fn llc_layout_dual_ccd() {
2166        let l = LlcLayout::build(&grid(32, 16), 16);
2167        assert_eq!(l.nr_llcs, 2);
2168        assert_eq!(l.dense[&0], 0);
2169        assert_eq!(l.dense[&1], 1);
2170        assert_eq!(l.word[&1], 0xffff << 16);
2171    }
2172
2173    #[test]
2174    fn llc_layout_collapses_past_max_llcs() {
2175        let l = LlcLayout::build(&grid(256, 4), 16);
2176        assert_eq!(l.collapsed, Some(64));
2177        assert_eq!(l.nr_llcs, 1);
2178        assert!(l.dense.values().all(|d| *d == 0));
2179        assert_eq!(l.domain[&0], 0);
2180        assert_eq!(l.domain[&63], 63);
2181    }
2182
2183    #[test]
2184    fn llc_layout_one_llc_and_sparse_ids() {
2185        let l = LlcLayout::build(&[(0, 3), (2, 3), (70, 3)], 16);
2186        assert_eq!(l.nr_llcs, 1);
2187        assert_eq!(l.dense[&3], 0);
2188        assert_eq!(l.word[&3], 0b101);
2189    }
2190
2191    fn flagged(hot: &[bool]) -> Vec<usize> {
2192        hot.iter()
2193            .enumerate()
2194            .filter_map(|(cpu, h)| h.then_some(cpu))
2195            .collect()
2196    }
2197
2198    #[test]
2199    fn bimodal_host_cuts_above_the_gap() {
2200        // The development host under load: nvidia on 13, network on 5,
2201        // everything else at or under one tick.
2202        let deltas = [0, 1, 0, 0, 0, 20, 0, 1, 0, 0, 0, 0, 0, 32, 0, 0];
2203        let hot = sinks_by_widest_gap(&deltas).unwrap();
2204        assert_eq!(flagged(&hot), vec![5, 13]);
2205    }
2206
2207    #[test]
2208    fn flat_distribution_has_no_sinks() {
2209        assert_eq!(sinks_by_widest_gap(&[3; 16]).unwrap(), vec![false; 16]);
2210        assert_eq!(sinks_by_widest_gap(&[0; 16]).unwrap(), vec![false; 16]);
2211    }
2212
2213    #[test]
2214    fn half_machine_cut_is_untrusted() {
2215        // Eight equally-loud CPUs is host-wide load, not pinned affinity.
2216        let deltas = [50, 50, 50, 50, 50, 50, 50, 50, 0, 0, 0, 0, 0, 0, 0, 0];
2217        assert!(sinks_by_widest_gap(&deltas).is_none());
2218    }
2219
2220    #[test]
2221    fn lone_loud_cpu_separates_from_quiet_machine() {
2222        let mut deltas = [0u64; 16];
2223        deltas[13] = 4;
2224        let hot = sinks_by_widest_gap(&deltas).unwrap();
2225        assert_eq!(flagged(&hot), vec![13]);
2226    }
2227}