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;
9pub use bpf_skel::*;
10pub mod bpf_intf;
11pub use bpf_intf::*;
12
13use std::collections::BTreeMap;
14use std::mem::MaybeUninit;
15use std::sync::atomic::AtomicBool;
16use std::sync::atomic::Ordering;
17use std::sync::Arc;
18use std::time::Duration;
19
20use anyhow::Context;
21use anyhow::Result;
22use clap::Parser;
23use libbpf_rs::MapCore;
24use libbpf_rs::MapFlags;
25use libbpf_rs::OpenObject;
26use log::info;
27use log::warn;
28use scx_utils::build_id;
29use scx_utils::compat;
30use scx_utils::scx_ops_attach;
31use scx_utils::scx_ops_load;
32use scx_utils::scx_ops_open;
33use scx_utils::try_set_rlimit_infinity;
34use scx_utils::uei_exited;
35use scx_utils::uei_report;
36use scx_utils::Topology;
37use scx_utils::UserExitInfo;
38use scx_utils::NR_CPU_IDS;
39
40const SCHEDULER_NAME: &str = "scx_cake";
41
42/// scx_cake: a gaming-first sched_ext scheduler — one master algorithm on
43/// kernel primitives, no feature flags, no knobs, no runtime telemetry.
44/// Placement is the kernel's idle-CPU pick with direct dispatch on a hit.
45/// Under saturation, wakeups queue on one global vtime queue while
46/// slice-expired tasks requeue on their own CPU's queue ("wakeups global,
47/// continuations local"), and each CPU dispatches the earliest eligible of
48/// the two. The time slice is a compile-time constant.
49#[derive(Debug, Parser)]
50struct Opts {
51    /// Enable verbose libbpf output and runtime diagnostics. A release run
52    /// without this prints only identity, attach and exit.
53    #[clap(short = 'v', long, action = clap::ArgAction::SetTrue)]
54    verbose: bool,
55
56    /// Print the version and exit.
57    #[clap(short = 'V', long, action = clap::ArgAction::SetTrue)]
58    version: bool,
59
60    /// Campaign toggle `NAME=0|1` (g43, g44, g45, g46), repeatable. One
61    /// binary serves both arms of an on/off pair: the verifier deletes the
62    /// off arm at attach. Scaffolding for the 2026-08-22 toggle campaign
63    /// (STATE.md); defaults are tip behavior. Unknown names refuse to start.
64    #[clap(long = "toggle", value_name = "NAME=0|1")]
65    toggle: Vec<String>,
66}
67
68struct Scheduler<'a> {
69    skel: BpfSkel<'a>,
70    struct_ops: Option<libbpf_rs::Link>,
71    /// Handler-edge tracepoint links (§G35); dropped on exit with the rest.
72    _irq_links: Vec<libbpf_rs::Link>,
73    /// Diagnostics on (--toggle probe=1): census, hold attribution, black box.
74    probe_on: bool,
75    /// Frame-clock incumbent bucket, held against near-ties (§R.22).
76    frame_bucket: Option<u32>,
77    /// Last published period; the reference for slow-direction hysteresis.
78    frame_period: u64,
79    /// Consecutive polls a slower candidate has won; slow switches need
80    /// agreement, fast ones are instant (§G27.1).
81    slow_polls: u32,
82    /// Pessimistic frame estimate: fast down, slow up. Feeds the slice cap,
83    /// which must not widen when the mode wobbles (§G18).
84    frame_floor: u64,
85    /// Print runtime diagnostics. Off by default: a release run is not a
86    /// measurement session, and a scheduler logging into a game is noise.
87    verbose: bool,
88    /// Live IRQ-sink tracking state (§G30, §G33).
89    sinks: SinkMonitor,
90    /// The first live sink set completes the startup banner at INFO; later
91    /// changes are diagnostics and follow --verbose.
92    sinks_logged: bool,
93}
94
95impl<'a> Scheduler<'a> {
96    fn init(opts: &Opts, open_object: &'a mut MaybeUninit<OpenObject>) -> Result<Self> {
97        try_set_rlimit_infinity();
98
99        // The startup banner is the only "telemetry" cake emits, and it is
100        // one-shot: version, machine shape, the compiled constants, and
101        // which kernel fast paths the RUNNING kernel provides (probed from
102        // BTF, not what the source requested) — so every log is
103        // self-describing about what actually loaded.
104        let topo = Topology::new().context("failed to read topology")?;
105        let physical = topo.all_cores.len();
106        let total = topo.all_cpus.len();
107        let smt = total.saturating_sub(physical);
108
109        let slice_us = bpf_intf::consts_SLICE_NS as u64 / bpf_intf::consts_NSEC_PER_USEC as u64;
110        let queued_wakeup = *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP != 0;
111        let dsq_peek = compat::ksym_exists("scx_bpf_dsq_peek").unwrap_or(false);
112
113        info!(
114            "🍰 {} {}",
115            SCHEDULER_NAME,
116            build_id::full_version(env!("CARGO_PKG_VERSION"))
117        );
118        info!("   cores   {physical} physical + {smt} SMT = {total} CPUs");
119        info!("   slice   {slice_us}µs · queues {total} per-CPU vtime + 1 global wake");
120        info!(
121            "   kernel  queued_wakeup {} · dsq_peek {}",
122            if queued_wakeup { "on" } else { "UNSUPPORTED" },
123            // cake calls scx_bpf_dsq_peek() unconditionally -- the
124            // __COMPAT_ iterator arm was deleted with the other compat
125            // ladders. On a kernel without the ksym the load fails
126            // outright, so "MISSING" is the honest word, not "fallback".
127            if dsq_peek { "native" } else { "MISSING" },
128        );
129
130        // Open the BPF program.
131        let mut skel_builder = BpfSkelBuilder::default();
132        skel_builder.obj_builder.debug(opts.verbose);
133        let mut skel = scx_ops_open!(skel_builder, open_object, cake_ops, None)?;
134
135        // Linux CPU numbering does not guarantee that an SMT sibling is
136        // cpu +/- nr_cpus/2. Populate the immutable BPF lookup from sysfs
137        // topology and cycle within wider-than-SMT2 cores. The fallback path
138        // treats -1 as "no online sibling" and uses the kernel idle picker.
139        let rodata = skel
140            .maps
141            .rodata_data
142            .as_mut()
143            .context("BPF rodata unavailable for CPU topology")?;
144
145        // The CPU id span the steal ring and neighbour probe scan. It is a
146        // load-time constant, so it goes in rodata (frozen before load, hence
147        // folded by the verifier) instead of being written into mutable BPF
148        // state from ops.init. `ops.init` refuses to attach if this is
149        // narrower than the kernel's own nr_cpu_ids.
150        anyhow::ensure!(
151            *NR_CPU_IDS <= bpf_intf::consts_MAX_CPUS as usize,
152            "host nr_cpu_ids {} exceeds Cake's compiled MAX_CPUS {}",
153            *NR_CPU_IDS,
154            bpf_intf::consts_MAX_CPUS
155        );
156        rodata.nr_cpu_span = *NR_CPU_IDS as u32;
157        rodata.cake_span_mask = (*NR_CPU_IDS as u32).next_power_of_two() - 1;
158
159        // Campaign toggles land in rodata before load so the verifier prunes
160        // the off arms; the logged line is each arm's identity in an on/off
161        // pair (STATE.md, toggle campaign 2026-08-22).
162        for spec in &opts.toggle {
163            let (name, val) = spec
164                .split_once('=')
165                .with_context(|| format!("--toggle {spec}: expected NAME=0|1"))?;
166            let on = match val {
167                "0" => 0u8,
168                "1" => 1u8,
169                _ => anyhow::bail!("--toggle {spec}: value must be 0 or 1"),
170            };
171            match name {
172                "g39b" => rodata.cake_tog_g39b = on,
173                "g46" => rodata.cake_tog_g46 = on,
174                "m6" => rodata.cake_tog_m6 = on,
175                "g51" => rodata.cake_tog_g51 = on,
176                "g52" => rodata.cake_tog_g52 = on,
177                "g56" => rodata.cake_tog_g56 = on,
178                "g57" => rodata.cake_tog_g57 = on,
179                "g58" => rodata.cake_tog_g58 = on,
180                "g59" => rodata.cake_tog_g59 = on,
181                "g60" => rodata.cake_tog_g60 = on,
182                "g61" => rodata.cake_tog_g61 = on,
183                "g62" => rodata.cake_tog_g62 = on,
184                "g63" => rodata.cake_tog_g63 = on,
185                "g64" => rodata.cake_tog_g64 = on,
186                "g65" => rodata.cake_tog_g65 = on,
187                "g66" => rodata.cake_tog_g66 = on,
188                "g67" => rodata.cake_tog_g67 = on,
189                "g68" => rodata.cake_tog_g68 = on,
190                "g69" => rodata.cake_tog_g69 = on,
191                "g70" => rodata.cake_tog_g70 = on,
192                "g72" => rodata.cake_tog_g72 = on,
193                "g71" => rodata.cake_tog_g71 = on,
194                "g73" => rodata.cake_tog_g73 = on,
195                "g74" => rodata.cake_tog_g74 = on,
196                "g75" => rodata.cake_tog_g75 = on,
197                "g77" => rodata.cake_tog_g77 = on,
198                "g78" => rodata.cake_tog_g78 = on,
199                "g79" => rodata.cake_tog_g79 = on,
200                "g81" => rodata.cake_tog_g81 = on,
201                "m7" => rodata.cake_tog_m7 = on,
202                "probe" => rodata.cake_tog_probe = on,
203                _ => anyhow::bail!("--toggle {spec}: unknown name {name}"),
204            }
205        }
206        // §G59 reads the §G51 mirror, so it forces the producer on.
207        if rodata.cake_tog_g68 == 1 || rodata.cake_tog_g70 == 1 {
208            // EXPERIMENT §G68: the VIP process is found by comm prefix at attach.
209            let mut vip = 0u32;
210            if let Ok(rd) = std::fs::read_dir("/proc") {
211                for e in rd.flatten() {
212                    if let Ok(comm) = std::fs::read_to_string(e.path().join("comm")) {
213                        if comm.starts_with("FPSAimTrainer") {
214                            if let Ok(pid) = e.file_name().to_string_lossy().parse::<u32>() {
215                                vip = pid;
216                                break;
217                            }
218                        }
219                    }
220                }
221            }
222            rodata.cake_vip_tgid = vip;
223            info!("   g68     VIP process tgid {vip} (0 = not found, VIP inert)");
224        }
225        if rodata.cake_tog_g59 == 1 {
226            rodata.cake_tog_g51 = 1;
227        }
228        let probe_on = rodata.cake_tog_probe == 1;
229        info!(
230            "   toggle  g39b={} g46={} g51={} g52={} g56={} g57={} g58={} g59={} g60={} g61={} g62={} g63={} g64={} g65={} g66={} g67={} g68={} g69={} g70={} g72={} g71={} g73={} g74={} g75={} g77={} g78={} g79={} g81={} m6={} m7={}",
231            rodata.cake_tog_g39b,
232            rodata.cake_tog_g46,
233            rodata.cake_tog_g51,
234            rodata.cake_tog_g52,
235            rodata.cake_tog_g56,
236            rodata.cake_tog_g57,
237            rodata.cake_tog_g58,
238            rodata.cake_tog_g59,
239            rodata.cake_tog_g60,
240            rodata.cake_tog_g61,
241            rodata.cake_tog_g62,
242            rodata.cake_tog_g63,
243            rodata.cake_tog_g64,
244            rodata.cake_tog_g65,
245            rodata.cake_tog_g66,
246            rodata.cake_tog_g67,
247            rodata.cake_tog_g68,
248            rodata.cake_tog_g69,
249            rodata.cake_tog_g70,
250            rodata.cake_tog_g72,
251            rodata.cake_tog_g71,
252            rodata.cake_tog_g73,
253            rodata.cake_tog_g74,
254            rodata.cake_tog_g75,
255            rodata.cake_tog_g77,
256            rodata.cake_tog_g78,
257            rodata.cake_tog_g79,
258            rodata.cake_tog_g81,
259            rodata.cake_tog_m6,
260            rodata.cake_tog_m7
261        );
262
263        // §G51: cpuidle exit-latency table from sysfs; absent driver leaves
264        // zeros and the depth model inert. §G52: CPPC highest_perf per CPU.
265        let mut deepest_us = 0u32;
266        for i in 0..bpf_intf::consts_CAKE_CSTATE_TABLE as usize {
267            let path = format!("/sys/devices/system/cpu/cpu0/cpuidle/state{i}/latency");
268            if let Ok(s) = std::fs::read_to_string(&path) {
269                let us: u32 = s.trim().parse().unwrap_or(0);
270                rodata.cake_cstate_exit_us[i] = us;
271                deepest_us = deepest_us.max(us);
272            }
273        }
274        if rodata.cake_tog_g51 == 1 && deepest_us == 0 {
275            info!("   g51     no cpuidle driver: depth model inert");
276        }
277        // §G58: the lead covers the deepest exit twice over; a host without
278        // a cpuidle table gets the fixed default. Never a refusal.
279        rodata.cake_prewake_lead_ns = if deepest_us > 0 {
280            u64::from(deepest_us) * 2 * 1000
281        } else {
282            bpf_intf::consts_PREWAKE_LEAD_DEFAULT_NS as u64
283        };
284        if rodata.cake_tog_g58 == 1 {
285            info!(
286                "   g58     pre-wake lead {} us ({})",
287                rodata.cake_prewake_lead_ns / 1000,
288                if deepest_us > 0 {
289                    "2x deepest exit"
290                } else {
291                    "default, no cpuidle"
292                }
293            );
294        }
295        if rodata.cake_tog_g59 == 1 && deepest_us == 0 {
296            info!("   g59     no cpuidle table: depth pick is first fit");
297        }
298        let mut perf_seen = false;
299        for c in 0..*NR_CPU_IDS {
300            let path = format!("/sys/devices/system/cpu/cpu{c}/acpi_cppc/highest_perf");
301            if let Ok(s) = std::fs::read_to_string(&path) {
302                let v: u64 = s.trim().parse().unwrap_or(0);
303                rodata.cpu_perf_rank[c] = v.min(255) as u8;
304                perf_seen = perf_seen || v != 0;
305            }
306        }
307        if rodata.cake_tog_g52 == 1 && !perf_seen {
308            info!("   g52     no CPPC highest_perf: rank tiebreak inert");
309        }
310
311        // §G56 FOLD tables, from RUNTIME topology — never the build host.
312        // cpu -> compact LLC index, per-LLC qmask word (narrow hosts), and
313        // the per-home band order: own LLC first, then foreign LLCs by
314        // descending CPPC rank when g52 is live, id order otherwise. More
315        // LLCs than the table degrades the fold off (bands stay id-order,
316        // the toggle gate's span check keeps the walk), never a refusal.
317        let nr_llcs = topo.all_llcs.len().min(bpf_intf::consts_MAX_LLCS as usize);
318        let mut llc_rank = [0u8; bpf_intf::consts_MAX_LLCS as usize];
319        for (idx, llc) in topo.all_llcs.values().take(nr_llcs).enumerate() {
320            let mut word = 0u64;
321            for cpu in llc.all_cpus.keys().copied() {
322                if cpu < rodata.cake_cpu_llc.len() {
323                    rodata.cake_cpu_llc[cpu] = idx as u8;
324                }
325                if cpu < 64 {
326                    word |= 1u64 << cpu;
327                }
328                if cpu < *NR_CPU_IDS {
329                    llc_rank[idx] = llc_rank[idx].max(rodata.cpu_perf_rank[cpu]);
330                }
331            }
332            rodata.cake_llc_qword[idx] = word;
333        }
334        rodata.cake_nr_llcs = nr_llcs.max(1) as u32;
335        for home in 0..nr_llcs {
336            let mut foreign: Vec<usize> = (0..nr_llcs).filter(|&l| l != home).collect();
337            if rodata.cake_tog_g52 == 1 {
338                foreign.sort_by_key(|&l| std::cmp::Reverse(llc_rank[l]));
339            }
340            rodata.cake_llc_order[home][0] = home as u8;
341            for (b, l) in foreign.iter().enumerate() {
342                rodata.cake_llc_order[home][b + 1] = *l as u8;
343            }
344        }
345        if rodata.cake_tog_g56 == 1 {
346            info!(
347                "   g56     banded steal: {nr_llcs} LLC band(s), order {}",
348                if rodata.cake_tog_g52 == 1 {
349                    "rank"
350                } else {
351                    "id"
352                }
353            );
354        }
355
356        // Hardware-anchored thresholds: measured, never derived from the slice.
357        // Clamped so a probe perturbed by host load cannot mis-tune the
358        // scheduler, and logged so the value used is always on the record.
359        match probe_handoff_hop_ns() {
360            Some(probe) => {
361                // The admission threshold IS the tail of a genuine handoff --
362                // used directly, never a mean times an invented multiplier.
363                // DIAGNOSTIC ONLY -- deliberately does not drive policy yet.
364                //
365                // Measured 2026-07-30: driving cake_handoff_max_ns from this
366                // probe cost mutex-handoff -35.66% (CI[-40.01,-31.31]) with
367                // migrations 2.4-2.7x up, at both `2 * mean` (1240ns) and, by
368                // inspection, `p99` (798ns). The known-good threshold is
369                // 1464ns = 2.34x this probe's median.
370                //
371                // The probe is not wrong about the hardware -- its 625ns
372                // median cross-validates the independent 606ns rdtscp floor
373                // to 3%. It is measuring the wrong DISTRIBUTION: a clean
374                // condvar ping-pong on an idle host, where p99 sits just
375                // 1.28x the median, while real handoffs under contention
376                // carry lock acquisition and cache misses and run far wider.
377                // That is a workload property, and no startup probe can see
378                // it. Making this threshold host-adaptive needs runtime
379                // observation of `used` in ops.stopping, not a boot-time
380                // measurement -- same machinery as G9.7.
381                let (med, p99) = (probe.median, probe.p99);
382                let hm = rodata.cake_handoff_max_ns;
383                // The probe's p99 IS the pick-to-landing horizon the tick
384                // predictor needs (§G36): how long this host takes to land a
385                // wake. Zero (probe failed) leaves the predictor off.
386                rodata.cake_wake_hop_ns = p99;
387                if opts.verbose {
388                    info!("   class   starvation = mean wait > mean burst (no threshold)");
389                    info!(
390                        "   probe   hop median {med}ns p99 {p99}ns (diagnostic) · handoff_max {hm}ns"
391                    );
392                }
393            }
394            None => {
395                if opts.verbose {
396                    log::warn!("   probe   handoff probe failed (diagnostic only)");
397                }
398            }
399        }
400
401        // Interrupt sinks are measured, never guessed — and measured LIVE:
402        // per-CPU handler-time share off the run loop, cut at the
403        // distribution's own widest gap (§G30, §G33, §R.26). Nothing is
404        // sampled at attach — an attach-time window measures whatever the
405        // machine happened to be doing during launch (§G30's observer
406        // effect) — so the scheduler starts sink-free and announces the
407        // first honest set seconds later.
408        info!("   irq     interrupt sinks tracked live by handler-time share");
409
410        let siblings = &mut rodata.cpu_sibling;
411        siblings.fill(-1);
412        for core in topo.all_cores.values() {
413            let cpu_ids: Vec<usize> = core.cpus.keys().copied().collect();
414
415            if cpu_ids.len() < 2 {
416                continue;
417            }
418            for (idx, cpu) in cpu_ids.iter().copied().enumerate() {
419                let sibling = cpu_ids[(idx + 1) % cpu_ids.len()];
420
421                anyhow::ensure!(
422                    cpu < siblings.len() && sibling <= i32::MAX as usize,
423                    "CPU topology id exceeds Cake's compiled sibling map"
424                );
425                siblings[cpu] = sibling as i32;
426            }
427        }
428
429        // Bootstrap each frame word in its SAFE direction. The clock starts
430        // slow so occupant protection is never divided into a zero; the FLOOR
431        // starts at the display-class fast end (2 ms, NOT the engine-band min:
432        // a vote-free host would keep a 250 µs slice cap forever) so the cap
433        // begins tight and relaxes only as evidence arrives (§G18, §G19).
434        // Geometry starts at the fixed slice; only votes move it (§G27).
435        if let Some(bss) = skel.maps.bss_data.as_mut() {
436            bss.cake_frame_ns = bpf_intf::consts_FRAME_PERIOD_MAX_NS as u64;
437            bss.cake_frame_floor_ns = bpf_intf::consts_FRAME_FLOOR_BOOT_NS as u64;
438            bss.cake_frame_slice_ns = bpf_intf::consts_SLICE_NS as u64;
439        }
440
441        // Multi-CCD steal order is a runtime decision, never a build-host
442        // property: one binary must attach on any topology. Hosts wider than
443        // the fixed matrix span keep the generic ring walk.
444        {
445            let rodata = skel
446                .maps
447                .rodata_data
448                .as_mut()
449                .context("BPF rodata unavailable for cache topology")?;
450            let order = &mut rodata.cpu_steal_order;
451            let span = bpf_intf::consts_STEAL_SPAN as usize;
452            let fits = topo
453                .all_cpus
454                .keys()
455                .next_back()
456                .is_none_or(|cpu| *cpu < span);
457            let multi_ccd = topo.all_llcs.len() > 1;
458            rodata.steal_order_live = u8::from(multi_ccd && fits);
459            order.fill(0);
460
461            if multi_ccd && !fits {
462                warn!("   ccd     host wider than steal matrix ({span} CPUs); ring steal only");
463            }
464            if multi_ccd && fits {
465                let llc_cache: BTreeMap<usize, usize> = topo
466                    .all_llcs
467                    .iter()
468                    .map(|(id, llc)| {
469                        (
470                            *id,
471                            llc.all_cpus
472                                .values()
473                                .map(|cpu| cpu.cache_size)
474                                .max()
475                                .unwrap_or(0),
476                        )
477                    })
478                    .collect();
479                let policy = bpf_intf::consts_CCD_STEAL_POLICY;
480                let nr_ids = *NR_CPU_IDS;
481
482                for src in topo.all_cpus.values() {
483                    let mut candidates: Vec<_> = topo
484                        .all_cpus
485                        .values()
486                        .filter(|dst| dst.id != src.id)
487                        .collect();
488                    candidates.sort_by_key(|dst| {
489                        let class = if dst.llc_id == src.llc_id {
490                            0
491                        } else if policy > 1 && llc_cache[&dst.llc_id] == llc_cache[&src.llc_id] {
492                            1
493                        } else {
494                            2
495                        };
496                        (class, (dst.id + nr_ids - src.id) % nr_ids)
497                    });
498                    let base = src.id * span;
499                    for (slot, dst) in candidates.into_iter().enumerate() {
500                        order[base + slot] = dst.id as u16;
501                    }
502                }
503            }
504        }
505
506        // Load and attach.
507        // §G76: the cpuidle mirror tracepoint fires on every idle transition
508        // (2.7M/12 s on a polling-idle host); it is loaded only for its consumers.
509        let want_cpu_idle = skel
510            .maps
511            .rodata_data
512            .as_ref()
513            .map(|r| r.cake_tog_g51 == 1)
514            .unwrap_or(false);
515        skel.progs.cake_cpu_idle.set_autoload(want_cpu_idle);
516        let mut skel = scx_ops_load!(skel, cake_ops, uei)?;
517        let struct_ops = Some(scx_ops_attach!(skel, cake_ops)?);
518
519        // Handler-edge tracepoints feed the in-handler word (§G35). A failed
520        // attach degrades to chronic-only steering, never blocks the
521        // scheduler; the word simply stays zero.
522        let mut irq_links = Vec::with_capacity(4);
523        for (name, res) in [
524            ("irq_enter", skel.progs.cake_irq_enter.attach()),
525            ("irq_leave", skel.progs.cake_irq_leave.attach()),
526            ("softirq_enter", skel.progs.cake_softirq_enter.attach()),
527            ("softirq_leave", skel.progs.cake_softirq_leave.attach()),
528        ] {
529            match res {
530                Ok(link) => irq_links.push(link),
531                Err(e) => warn!("   irq     {name} hook failed ({e}); chronic steering only"),
532            }
533        }
534        if want_cpu_idle {
535            match skel.progs.cake_cpu_idle.attach() {
536                Ok(link) => irq_links.push(link),
537                Err(e) => warn!("   g51     cpu_idle hook failed ({e}); depth mirror off"),
538            }
539        }
540
541        info!("🍰 attached — wakeups queue globally, continuations locally");
542
543        // The file capabilities (cap_bpf,cap_perfmon,cap_sys_nice) are only
544        // needed to load and attach; detach and map access use already-open
545        // fds. Drop them all: while any elevated capability stays in the
546        // permitted set, the kernel's ptrace access check denies
547        // /proc/<pid>/exe to unprivileged observers even with dumpable
548        // restored, which breaks the sudoless bench runner's hash-of-exe
549        // identity verification (and holding dead privileges is bad hygiene).
550        drop_all_capabilities();
551
552        Ok(Self {
553            skel,
554            struct_ops,
555            _irq_links: irq_links,
556            probe_on,
557            frame_bucket: None,
558            frame_period: 0,
559            slow_polls: 0,
560            frame_floor: 0,
561            verbose: opts.verbose,
562            sinks: SinkMonitor::new(*NR_CPU_IDS),
563            sinks_logged: false,
564        })
565    }
566
567    fn exited(&mut self) -> bool {
568        uei_exited!(&self.skel, uei)
569    }
570
571    fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
572        // Frame clock, reported under --verbose on a material change only, so
573        // a moving line there means the observed cadence really moved (§G11).
574        let mut shown: u64 = 0;
575        while !shutdown.load(Ordering::Relaxed) && !self.exited() {
576            std::thread::sleep(Duration::from_secs(1));
577
578            if let Some(set) = self.sinks.tick(*NR_CPU_IDS) {
579                self.publish_sinks(&set);
580            }
581
582            let observed = self.publish_frame_clock();
583            if self.verbose && observed != 0 && observed.abs_diff(shown) > shown / 16 {
584                shown = observed;
585                info!(
586                    "   frame   observed period {}us ({:.1} Hz)",
587                    observed / 1000,
588                    1e9 / observed as f64
589                );
590            }
591        }
592
593        // Diagnostics (--toggle probe=1): black box of placements that waited > 10 ms, then the census.
594        if let (true, Some(bss)) = (self.probe_on, self.skel.maps.bss_data.as_ref()) {
595            let n = bss.cake_blackbox_n;
596            for i in 0..n.min(4) {
597                let b = &bss.cake_blackbox[i as usize];
598                let comm = String::from_utf8_lossy(
599                    &b.comm
600                        .iter()
601                        .map(|c| *c as u8)
602                        .take_while(|c| *c != 0)
603                        .collect::<Vec<u8>>(),
604                )
605                .to_string();
606                info!(
607                    "   BLACKBOX wait {:.2} ms  {} pid {} kind {} target cpu{} caller cpu{} waker {} ran_on cpu{}  seats {:#018x} core_free {:#018x} thread_free {:#018x} idle {:#018x}",
608                    b.wait_ns as f64 / 1e6, comm, b.pid, b.kind, b.target, b.caller, b.waker_pid, b.ran_on, b.seats, b.core_free, b.thread_free, b.idle_word
609                );
610            }
611        }
612        if self.probe_on {
613            const NAMES: [&str; 47] = [
614                "select_calls",
615                "serial",
616                "home_warm",
617                "park_reached",
618                "park_prev",
619                "park_mbox",
620                "opt_reached",
621                "opt_hit",
622                "ranked",
623                "wp_attempt",
624                "wp_tiny",
625                "wp_small",
626                "wp_protect",
627                "wp_vtime",
628                "wp_starved",
629                "wp_fired",
630                "free_pick",
631                "prewake_fire",
632                "reserved_take",
633                "pl_local",
634                "pl_local_on",
635                "pl_cpuq_wake",
636                "pl_cpuq_cont",
637                "pl_global",
638                "h300_local",
639                "h300_local_on",
640                "h300_cpuq_wake",
641                "h300_cpuq_cont",
642                "h300_global",
643                "h1ms_local",
644                "h1ms_local_on",
645                "h1ms_cpuq_wake",
646                "h1ms_cpuq_cont",
647                "h1ms_global",
648                "pl_self",
649                "h300_self",
650                "h1ms_self",
651                "hd_skip",
652                "hd_sync",
653                "hd_starved",
654                "hd_irq",
655                "hd_aff",
656                "hd_contended",
657                "hd_notidle",
658                "home_busy",
659                "home_localq",
660                "h300_home_busy",
661            ];
662            let mut tot = [0u64; NAMES.len()];
663            for (i, t) in tot.iter_mut().enumerate() {
664                let key = (i as u32).to_ne_bytes();
665                if let Ok(Some(percpu)) =
666                    self.skel.maps.cake_stats.lookup_percpu(&key, MapFlags::ANY)
667                {
668                    for cpu in &percpu {
669                        if cpu.len() >= 8 {
670                            *t += u64::from_ne_bytes(cpu[..8].try_into().unwrap());
671                        }
672                    }
673                }
674            }
675            let sel = tot[0].max(1) as f64;
676            for (i, name) in NAMES.iter().enumerate() {
677                info!(
678                    "   arms    {name:<13} {:>12}  {:>6.2}% of select",
679                    tot[i],
680                    tot[i] as f64 * 100.0 / sel
681                );
682            }
683        }
684
685        if let Some(bss) = self.skel.maps.bss_data.as_mut() {
686            let ev = &bss.cake_events;
687            info!(
688                "   events  select_fallback {} keep_last {} enq_skip_exiting {}",
689                ev.SCX_EV_SELECT_CPU_FALLBACK,
690                ev.SCX_EV_DISPATCH_KEEP_LAST,
691                ev.SCX_EV_ENQ_SKIP_EXITING
692            );
693        }
694
695        self.struct_ops.take();
696        info!("🍰 {SCHEDULER_NAME} detached — default scheduler restored");
697        uei_report!(&self.skel, uei)
698    }
699
700    /// Push a changed sink set live: rewrite the per-CPU flags, then bump
701    /// the generation so the next ranked pick rebuilds the nonsink mask
702    /// (§G30). Flags settle before the bump; a torn read costs at most one
703    /// extra rebuild.
704    fn publish_sinks(&mut self, set: &[bool]) {
705        let Some(bss) = self.skel.maps.bss_data.as_mut() else {
706            return;
707        };
708        for (cpu, hot) in set.iter().enumerate() {
709            if cpu < bss.cpu_irq_hot.len() {
710                bss.cpu_irq_hot[cpu] = u8::from(*hot);
711            }
712        }
713        bss.cake_sink_gen = bss.cake_sink_gen.wrapping_add(1);
714
715        if self.verbose || !self.sinks_logged {
716            self.sinks_logged = true;
717            let named: Vec<usize> = set
718                .iter()
719                .enumerate()
720                .filter_map(|(cpu, hot)| hot.then_some(cpu))
721                .collect();
722            info!("   irq     interrupt-sink CPUs {named:?} — steered around");
723        }
724    }
725
726    /// Publish the binding cadence from the vote histogram.
727    ///
728    /// Every crowd near the biggest is a REAL cadence (a game plus the
729    /// desktop coexist legitimately), so no argmax: the FASTEST real crowd
730    /// is published, because every consumer of the clock is a bound and the
731    /// fastest cadence is the one that binds. The clock moves faster the
732    /// moment a faster crowd qualifies; it moves slower only when the fast
733    /// crowd actually fades — one noisy poll cannot drag it slower (§G27.1).
734    /// Buckets are cleared as read; sum/count keeps the value exact.
735    /// Returns the published period, 0 when there are no votes.
736    fn publish_frame_clock(&mut self) -> u64 {
737        const WIDTH: usize = std::mem::size_of::<u64>() * 2;
738        /// A crowd within 2x of the biggest is real, not noise.
739        const QUALIFY: u64 = 2;
740        /// The incumbent stays alive down to a quarter of the biggest crowd.
741        const FADE: u64 = 4;
742
743        let hist = &self.skel.maps.cake_frame_hist;
744        let mut crowds: Vec<(u32, u64, u64)> = Vec::new();
745
746        for idx in 0..bpf_intf::consts_FRAME_BUCKETS {
747            let key = idx.to_ne_bytes();
748            let Ok(Some(percpu)) = hist.lookup_percpu(&key, MapFlags::ANY) else {
749                continue;
750            };
751            let (mut count, mut sum) = (0u64, 0u64);
752            for cpu in &percpu {
753                if cpu.len() < WIDTH {
754                    continue;
755                }
756                count += u64::from_ne_bytes(cpu[..8].try_into().unwrap());
757                sum += u64::from_ne_bytes(cpu[8..16].try_into().unwrap());
758            }
759            if count == 0 {
760                continue;
761            }
762            crowds.push((idx, count, sum));
763            let zeroed = vec![vec![0u8; WIDTH]; percpu.len()];
764            let _ = hist.update_percpu(&key, &zeroed, MapFlags::ANY);
765        }
766
767        let Some(max_count) = crowds.iter().map(|c| c.1).max() else {
768            return 0;
769        };
770        // Fastest qualified crowd; bucket index is monotone in period.
771        let Some((qi, qc, qs)) = crowds
772            .iter()
773            .copied()
774            .filter(|c| c.1 * QUALIFY >= max_count)
775            .min_by_key(|c| c.0)
776        else {
777            return 0;
778        };
779
780        let held = self
781            .frame_bucket
782            .and_then(|hb| crowds.iter().copied().find(|c| c.0 == hb));
783        let (bucket, count, sum) = match held {
784            // The incumbent holds only while at least as fast as the
785            // challenger AND still alive; a faster challenger wins at once.
786            Some((hi, hc, hs)) if hi <= qi && hc * FADE >= max_count => (hi, hc, hs),
787            _ => (qi, qc, qs),
788        };
789        let cand = sum / count;
790
791        // Fast up, slow down: a faster cadence binds immediately, a slower
792        // one must win SLOW_POLLS in a row — an app's own threads form
793        // several fast crowds whose per-second counts wobble, and a one-poll
794        // silence must not publish a slow blip (§G27.1; live 2026-08-17).
795        const SLOW_POLLS: u32 = 3;
796        let (period, bucket) = if self.frame_period == 0 || cand <= self.frame_period {
797            self.slow_polls = 0;
798            (cand, Some(bucket))
799        } else {
800            self.slow_polls += 1;
801            if self.slow_polls >= SLOW_POLLS {
802                self.slow_polls = 0;
803                (cand, Some(bucket))
804            } else {
805                (self.frame_period, self.frame_bucket)
806            }
807        };
808        self.frame_period = period;
809
810        // A bound takes the pessimistic side of a noisy estimate: drop to a new
811        // low at once, climb back over ~16 polls. One 17372us excursion off a
812        // true 3621us previously doubled the slice cap (§G18).
813        self.frame_floor = match self.frame_floor {
814            0 => period,
815            f if period < f => period,
816            f => f + (period - f) / 16,
817        };
818        self.frame_bucket = bucket;
819        if let Some(bss) = self.skel.maps.bss_data.as_mut() {
820            bss.cake_frame_ns = period;
821            bss.cake_frame_floor_ns = self.frame_floor;
822            // Diagnostic only: feeds the --verbose clock line; no policy
823            // consumes these — geometry is per task (§R.28).
824            bss.cake_frame_slice_ns = ((self.frame_floor >> 1) + (self.frame_floor >> 2))
825                .min(bpf_intf::consts_SLICE_NS as u64);
826        }
827        period
828    }
829}
830
831/// Re-execute this image to service a kernel-requested restart.
832///
833/// `drop_all_capabilities()` runs after every successful attach, and a
834/// permitted set cleared through `capset` cannot be regained in-process —
835/// file capabilities are applied only at `execve`. Re-entering
836/// `Scheduler::init` would therefore fail `BPF_PROG_LOAD` with `EPERM` and
837/// kill the scheduler instead of restarting it. Exec'ing ourselves restores
838/// the file capabilities from the bounding set (which `capset` never
839/// touched), keeps the PID and `/proc/<pid>/exe` identity the bench runner
840/// verifies, and re-runs `PR_SET_DUMPABLE` on the way in. The struct_ops link
841/// is already dropped by `run()`, so the scheduler is detached before we
842/// replace the image.
843/// One host's wake+block+switch hop, as a distribution rather than a mean.
844struct HandoffProbe {
845    /// Typical hop — the switch-cost anchor for the preempt-protect window.
846    median: u64,
847    /// Tail of a GENUINE handoff. This is the admission threshold itself, not
848    /// a base to multiply: measured 2026-07-30, using `2 * mean` instead cost
849    /// mutex-handoff -35.66% (CI[-40.01,-31.31]) with migrations 2.4-2.7x up,
850    /// because enough real handoffs land in the tail above the mean that a
851    /// mean-derived cut loses them. The in-tree dose-response wanted a 99.67%
852    /// firing rate on this workload, so the statistic that matches the
853    /// requirement is a high percentile, not an invented multiplier.
854    p99: u64,
855}
856
857/// Per-CPU time spent in interrupt handlers, in kernel ticks, from
858/// `/proc/stat` (irq + softirq columns, CONFIG_IRQ_TIME_ACCOUNTING).
859///
860/// Handler TIME is the harm itself, not a proxy: a wake suffers exactly
861/// when it lands on a CPU whose handler is running, and the time share IS
862/// that probability. Counting interrupt lines mis-priced both directions --
863/// a 1000 Hz mouse with a ~1 µs handler steals no more time than an
864/// unflagged CPU, while a slow line with a heavy handler could never reach
865/// a count bar (§G33). Softirq time is included because NAPI network
866/// processing shadows wakes the same way and never appeared as line counts.
867fn read_irq_ticks(nr_cpus: usize) -> Option<Vec<u64>> {
868    let text = std::fs::read_to_string("/proc/stat").ok()?;
869    let mut ticks = vec![0u64; nr_cpus];
870    let mut seen = false;
871    for line in text.lines() {
872        let mut fields = line.split_whitespace();
873        let Some(cpu) = fields
874            .next()
875            .and_then(|l| l.strip_prefix("cpu"))
876            .and_then(|n| n.parse::<usize>().ok())
877        else {
878            continue;
879        };
880        if cpu >= nr_cpus {
881            continue;
882        }
883        // cpuN user nice system idle iowait irq softirq ...
884        let vals: Vec<u64> = fields.filter_map(|v| v.parse().ok()).collect();
885        if vals.len() < 7 {
886            continue;
887        }
888        ticks[cpu] = vals[5] + vals[6];
889        seen = true;
890    }
891    seen.then_some(ticks)
892}
893
894/// Split the host's own handler-time distribution at its widest gap: sinks
895/// are the CPUs above the cut (§G33, §R.26).
896///
897/// No unit-carrying threshold -- the cut is wherever the sorted per-CPU
898/// tick deltas separate the most, so it adapts to any host, any device,
899/// and any load without claiming to know in advance what "loud" means.
900/// Deltas feed ratios directly, so no clock or tick-rate conversion exists.
901/// A zero delta means "under one tick this window" and enters the ranking
902/// at half a tick -- the midpoint of what the measurement could not
903/// resolve -- keeping every ratio finite so a lone loud CPU still
904/// separates from an otherwise-quiet machine. A flat distribution has no
905/// gap and therefore no sinks; a cut that would condemn half the machine
906/// has measured host-wide load, not pinned interrupt affinity (None).
907fn sinks_by_widest_gap(deltas: &[u64]) -> Option<Vec<bool>> {
908    let mut ranked: Vec<(f64, usize)> = deltas
909        .iter()
910        .enumerate()
911        .map(|(cpu, &d)| ((d as f64).max(0.5), cpu))
912        .collect();
913    ranked.sort_by(|a, b| b.0.total_cmp(&a.0));
914
915    let mut cut = 0;
916    let mut widest = 1.0f64;
917    for i in 1..ranked.len() {
918        let ratio = ranked[i - 1].0 / ranked[i].0;
919        if ratio > widest {
920            widest = ratio;
921            cut = i;
922        }
923    }
924    if cut == 0 {
925        return Some(vec![false; deltas.len()]);
926    }
927    if cut * 2 >= deltas.len() {
928        return None;
929    }
930
931    let mut hot = vec![false; deltas.len()];
932    for &(_, cpu) in &ranked[..cut] {
933        hot[cpu] = true;
934    }
935    Some(hot)
936}
937
938/// Live IRQ-sink tracking riding the 1 s run loop (§G30, §G33, §R.25/§R.26).
939///
940/// Each window the handler-time deltas are split at their widest gap. A CPU
941/// above the cut for FLAG_POLLS consecutive windows is published as a sink
942/// -- a real sink ranks above the cut every window, ranking noise does not
943/// -- and a published sink is released after UNFLAG_POLLS windows below it,
944/// because removal merely returns placement freedom and a loading screen
945/// must not flap the mask. A set that keeps repeating earns a doubled
946/// sampling interval up to INTERVAL_MAX (longer windows accumulate more
947/// ticks, so confidence also buys resolution); any change resets to every
948/// tick. All four constants are agreement counts: every value that steers
949/// comes from the measured distribution alone (§R.26).
950struct SinkMonitor {
951    /// Last accepted read; deltas span the full gap between accepted reads,
952    /// so a slower cadence measures a longer, smoother window.
953    prev: Option<Vec<u64>>,
954    /// The set currently pushed to the scheduler.
955    published: Vec<bool>,
956    /// Consecutive windows each CPU has ranked above the cut.
957    hot_streak: Vec<u32>,
958    /// Consecutive windows each published sink has ranked below the cut.
959    quiet: Vec<u32>,
960    /// Windows since the published set last changed.
961    stable: u32,
962    /// Current sampling interval in run-loop ticks.
963    interval: u32,
964    ticks: u32,
965}
966
967impl SinkMonitor {
968    /// Unchanged windows that earn an interval doubling.
969    const STABLE_POLLS: u32 = 8;
970    /// Sampling never slows past this many ticks.
971    const INTERVAL_MAX: u32 = 16;
972    /// Above-cut windows before a CPU is published.
973    const FLAG_POLLS: u32 = 2;
974    /// Below-cut windows before a published sink is removed.
975    const UNFLAG_POLLS: u32 = 3;
976
977    fn new(nr_cpus: usize) -> Self {
978        Self {
979            prev: None,
980            published: vec![false; nr_cpus],
981            hot_streak: vec![0; nr_cpus],
982            quiet: vec![0; nr_cpus],
983            stable: 0,
984            interval: 1,
985            ticks: 0,
986        }
987    }
988
989    /// One run-loop tick; Some(set) when the published set changed.
990    fn tick(&mut self, nr_cpus: usize) -> Option<Vec<bool>> {
991        self.ticks = self.ticks.wrapping_add(1);
992        if !self.ticks.is_multiple_of(self.interval) {
993            return None;
994        }
995        // A failed read leaves prev in place, so the next sample just spans
996        // a longer window; an untrusted split still advances the baseline.
997        let sample = read_irq_ticks(nr_cpus)?;
998        let before = self.prev.replace(sample)?;
999        let current = self.prev.as_ref()?;
1000        let deltas: Vec<u64> = current
1001            .iter()
1002            .zip(before.iter())
1003            .map(|(c, b)| c.saturating_sub(*b))
1004            .collect();
1005        let hot = sinks_by_widest_gap(&deltas)?;
1006
1007        let mut changed = false;
1008        for (cpu, &is_hot) in hot.iter().enumerate().take(self.published.len()) {
1009            if is_hot {
1010                self.quiet[cpu] = 0;
1011                self.hot_streak[cpu] += 1;
1012                // The cut wanders under load, so flagged CPUs accrete across
1013                // polls — the per-sample half-machine guard cannot see the
1014                // UNION. Bound it here: the published set stays a strict
1015                // minority, so placement always keeps most of the machine.
1016                if !self.published[cpu]
1017                    && self.hot_streak[cpu] >= Self::FLAG_POLLS
1018                    && (self.published.iter().filter(|h| **h).count() + 1) * 2
1019                        < self.published.len()
1020                {
1021                    self.published[cpu] = true;
1022                    changed = true;
1023                }
1024            } else {
1025                self.hot_streak[cpu] = 0;
1026                if self.published[cpu] {
1027                    self.quiet[cpu] += 1;
1028                    if self.quiet[cpu] >= Self::UNFLAG_POLLS {
1029                        self.published[cpu] = false;
1030                        self.quiet[cpu] = 0;
1031                        changed = true;
1032                    }
1033                }
1034            }
1035        }
1036
1037        if changed {
1038            self.stable = 0;
1039            self.interval = 1;
1040            return Some(self.published.clone());
1041        }
1042        self.stable += 1;
1043        if self.stable >= Self::STABLE_POLLS {
1044            self.stable = 0;
1045            self.interval = (self.interval * 2).min(Self::INTERVAL_MAX);
1046        }
1047        None
1048    }
1049}
1050
1051/// Measure one wake + block + switch hop on THIS host.
1052///
1053/// Two threads ping-pong through a condvar, which exercises exactly the path
1054/// `cake_handoff_max_ns` is a proxy for: a blocking handoff between two
1055/// tasks. It used to be a slice divisor (/2048), since removed: that made it
1056/// made it wrong at any other slice value and wrong on any other CPU -- at a
1057/// 1 ms slice it evaluated to 488 ns, below this host's ~606 ns sleep floor, so
1058/// nothing could qualify and the co-location gate silently disabled itself.
1059///
1060/// Runs before the scheduler attaches, so it measures the stock kernel path.
1061/// ~2200 round trips, a few milliseconds of startup. Returns nanoseconds per
1062/// one-way hop, or None if the probe could not complete -- callers keep the
1063/// compiled-in defaults rather than trusting a failed measurement.
1064fn probe_handoff_hop_ns() -> Option<HandoffProbe> {
1065    const WARMUP: u32 = 200;
1066    const ITERS: u32 = 2_000;
1067    let total = WARMUP + ITERS;
1068
1069    let pair = Arc::new((std::sync::Mutex::new(0u32), std::sync::Condvar::new()));
1070    let peer = Arc::clone(&pair);
1071
1072    let responder = std::thread::Builder::new()
1073        .name("cake-probe".into())
1074        .spawn(move || {
1075            let (lock, cv) = &*peer;
1076            let mut turn = lock.lock().ok()?;
1077            for _ in 0..total {
1078                while *turn % 2 == 0 {
1079                    turn = cv.wait(turn).ok()?;
1080                }
1081                *turn = turn.wrapping_add(1);
1082                cv.notify_one();
1083            }
1084            Some(())
1085        })
1086        .ok()?;
1087
1088    let mut hops: Vec<u64> = Vec::with_capacity(ITERS as usize);
1089    {
1090        let (lock, cv) = &*pair;
1091        let mut turn = lock.lock().ok()?;
1092        for i in 0..total {
1093            let t0 = std::time::Instant::now();
1094            *turn = turn.wrapping_add(1);
1095            cv.notify_one();
1096            while *turn % 2 == 1 {
1097                turn = cv.wait(turn).ok()?;
1098            }
1099            if i >= WARMUP {
1100                // Two hops per round trip: our wake of the peer, its wake of us.
1101                hops.push(t0.elapsed().as_nanos() as u64 / 2);
1102            }
1103        }
1104    }
1105    responder.join().ok()?;
1106
1107    if hops.len() < ITERS as usize / 2 {
1108        return None;
1109    }
1110    hops.sort_unstable();
1111    let median = hops[hops.len() / 2];
1112    let p99 = hops[hops.len() * 99 / 100];
1113    if median == 0 || p99 == 0 {
1114        return None;
1115    }
1116    Some(HandoffProbe { median, p99 })
1117}
1118
1119fn reexec_self() -> Result<()> {
1120    use std::os::unix::process::CommandExt;
1121
1122    let exe = std::fs::read_link("/proc/self/exe")
1123        .context("failed to resolve /proc/self/exe for restart")?;
1124    /* exec() only returns on failure. */
1125    let err = std::process::Command::new(exe)
1126        .args(std::env::args_os().skip(1))
1127        .exec();
1128
1129    Err(anyhow::Error::new(err).context("re-exec after kernel restart request failed"))
1130}
1131
1132/// Clear the effective, permitted, and inheritable capability sets.
1133fn drop_all_capabilities() {
1134    #[repr(C)]
1135    struct CapHeader {
1136        version: u32,
1137        pid: i32,
1138    }
1139    #[repr(C)]
1140    #[derive(Clone, Copy)]
1141    struct CapData {
1142        effective: u32,
1143        permitted: u32,
1144        inheritable: u32,
1145    }
1146    const LINUX_CAPABILITY_VERSION_3: u32 = 0x2008_0522;
1147    let hdr = CapHeader {
1148        version: LINUX_CAPABILITY_VERSION_3,
1149        pid: 0,
1150    };
1151    let data = [CapData {
1152        effective: 0,
1153        permitted: 0,
1154        inheritable: 0,
1155    }; 2];
1156    let rc = unsafe { libc::syscall(libc::SYS_capset, &hdr, data.as_ptr()) };
1157    if rc != 0 {
1158        log::warn!("capset drop failed ({})", std::io::Error::last_os_error());
1159    }
1160}
1161
1162fn main() -> Result<()> {
1163    // File capabilities (cap_bpf,cap_perfmon,cap_sys_nice) clear the dumpable
1164    // flag, which makes /proc/self/exe root-only and defeats the sudoless
1165    // bench runner's process-identity verification (hash of /proc/<pid>/exe).
1166    // The binary holds no secrets, so restore normal /proc introspection.
1167    unsafe {
1168        libc::prctl(libc::PR_SET_DUMPABLE, 1, 0, 0, 0);
1169    }
1170
1171    let opts = Opts::parse();
1172
1173    if opts.version {
1174        println!(
1175            "{} {}",
1176            SCHEDULER_NAME,
1177            build_id::full_version(env!("CARGO_PKG_VERSION"))
1178        );
1179        return Ok(());
1180    }
1181
1182    let mut lcfg = simplelog::ConfigBuilder::new();
1183    lcfg.set_time_level(simplelog::LevelFilter::Error)
1184        .set_location_level(simplelog::LevelFilter::Off)
1185        .set_target_level(simplelog::LevelFilter::Off)
1186        .set_thread_level(simplelog::LevelFilter::Off);
1187    simplelog::TermLogger::init(
1188        simplelog::LevelFilter::Info,
1189        lcfg.build(),
1190        simplelog::TerminalMode::Stderr,
1191        simplelog::ColorChoice::Auto,
1192    )?;
1193
1194    let shutdown = Arc::new(AtomicBool::new(false));
1195    let shutdown_clone = shutdown.clone();
1196    ctrlc::set_handler(move || {
1197        shutdown_clone.store(true, Ordering::Relaxed);
1198    })
1199    .context("Error setting Ctrl-C handler")?;
1200
1201    let mut open_object = MaybeUninit::uninit();
1202    let mut sched = Scheduler::init(&opts, &mut open_object)?;
1203
1204    if sched.run(shutdown.clone())?.should_restart() {
1205        info!("🍰 restart requested by the kernel — re-executing");
1206        reexec_self()?;
1207    }
1208
1209    Ok(())
1210}
1211
1212#[cfg(test)]
1213mod tests {
1214    use super::sinks_by_widest_gap;
1215
1216    fn flagged(hot: &[bool]) -> Vec<usize> {
1217        hot.iter()
1218            .enumerate()
1219            .filter_map(|(cpu, h)| h.then_some(cpu))
1220            .collect()
1221    }
1222
1223    #[test]
1224    fn bimodal_host_cuts_above_the_gap() {
1225        // The development host under load: nvidia on 13, network on 5,
1226        // everything else at or under one tick.
1227        let deltas = [0, 1, 0, 0, 0, 20, 0, 1, 0, 0, 0, 0, 0, 32, 0, 0];
1228        let hot = sinks_by_widest_gap(&deltas).unwrap();
1229        assert_eq!(flagged(&hot), vec![5, 13]);
1230    }
1231
1232    #[test]
1233    fn flat_distribution_has_no_sinks() {
1234        assert_eq!(sinks_by_widest_gap(&[3; 16]).unwrap(), vec![false; 16]);
1235        assert_eq!(sinks_by_widest_gap(&[0; 16]).unwrap(), vec![false; 16]);
1236    }
1237
1238    #[test]
1239    fn half_machine_cut_is_untrusted() {
1240        // Eight equally-loud CPUs is host-wide load, not pinned affinity.
1241        let deltas = [50, 50, 50, 50, 50, 50, 50, 50, 0, 0, 0, 0, 0, 0, 0, 0];
1242        assert!(sinks_by_widest_gap(&deltas).is_none());
1243    }
1244
1245    #[test]
1246    fn lone_loud_cpu_separates_from_quiet_machine() {
1247        let mut deltas = [0u64; 16];
1248        deltas[13] = 4;
1249        let hot = sinks_by_widest_gap(&deltas).unwrap();
1250        assert_eq!(flagged(&hot), vec![13]);
1251    }
1252}