Skip to main content

scx_pandemonium/
main.rs

1// PANDEMONIUM -- SCHED_EXT KERNEL SCHEDULER
2// ADAPTIVE DESKTOP SCHEDULING FOR LINUX
3//
4// SCHEDULING DECISIONS HAPPEN IN BPF (ZERO KERNEL-USERSPACE ROUND TRIPS)
5// RUST USERSPACE HANDLES: ADAPTIVE CONTROL LOOP, MONITORING, BENCHMARKING
6
7#[allow(non_upper_case_globals)]
8#[allow(non_camel_case_types)]
9#[allow(non_snake_case)]
10mod bpf_skel;
11
12mod bpf_intf;
13
14#[macro_use]
15mod log;
16mod adaptive;
17mod chaos;
18mod cli;
19mod scheduler;
20mod topology;
21mod tuning;
22mod watchdog;
23
24use std::mem::MaybeUninit;
25use std::sync::atomic::{AtomicBool, Ordering};
26use std::time::Duration;
27
28use anyhow::Result;
29use clap::{Parser, Subcommand};
30
31use scheduler::Scheduler;
32use scx_utils::build_id;
33
34static SHUTDOWN: AtomicBool = AtomicBool::new(false);
35
36#[derive(Parser)]
37#[command(name = "scx_pandemonium")]
38#[command(
39    version,
40    disable_version_flag = true,
41    about = "PANDEMONIUM -- ADAPTIVE LINUX SCHEDULER"
42)]
43struct Cli {
44    #[command(subcommand)]
45    command: Option<SubCmd>,
46
47    #[arg(short, long)]
48    verbose: bool,
49
50    /// Print scheduler version and exit.
51    #[arg(long)]
52    version: bool,
53
54    /// Internal: dump in-memory ring log on shutdown
55    #[arg(long, hide = true)]
56    dump_log: bool,
57
58    /// Internal: override CPU count for scaling formulas (test harness use)
59    #[arg(long, hide = true)]
60    nr_cpus: Option<u64>,
61
62    /// Run BPF scheduler only, disable Rust adaptive control loop
63    #[arg(long)]
64    no_adaptive: bool,
65
66    /// Override the topology-derived Phi distance scale (phi_dist_scale_q16).
67    /// 0 disables the Phi steal-resist (flat CoDel target); omit for the
68    /// topology value. Test/bench use -- the override holds across both the
69    /// adaptive and --no-adaptive paths.
70    #[arg(long)]
71    phi_scale: Option<u64>,
72}
73
74#[derive(Subcommand)]
75enum SubCmd {
76    /// Internal: interactive wakeup probe (Python test harness use)
77    #[command(hide = true)]
78    Probe,
79
80    /// Internal: CPU-pinned stress worker (Python test harness use)
81    #[command(hide = true)]
82    StressWorker(StressWorkerArgs),
83}
84
85#[derive(Parser)]
86struct StressWorkerArgs {
87    /// CPU to pin the stress worker to
88    #[arg(long)]
89    cpu: u32,
90}
91
92fn main() -> Result<()> {
93    let cli = Cli::parse();
94
95    let verbose = cli.verbose;
96    let dump_log = cli.dump_log;
97    let nr_cpus = cli.nr_cpus;
98    let no_adaptive = cli.no_adaptive;
99    let phi_scale = cli.phi_scale;
100
101    if cli.version {
102        println!(
103            "scx_pandemonium {}",
104            build_id::full_version(env!("CARGO_PKG_VERSION"))
105        );
106        return Ok(());
107    }
108
109    match cli.command {
110        None => run_scheduler(verbose, dump_log, nr_cpus, no_adaptive, phi_scale),
111        Some(SubCmd::Probe) => {
112            cli::probe::run_probe();
113            Ok(())
114        }
115        Some(SubCmd::StressWorker(args)) => {
116            cli::stress::run_stress_worker(args.cpu);
117            Ok(())
118        }
119    }
120}
121
122fn run_scheduler(
123    verbose: bool,
124    dump_log: bool,
125    nr_cpus: Option<u64>,
126    no_adaptive: bool,
127    phi_scale: Option<u64>,
128) -> Result<()> {
129    ctrlc::set_handler(move || {
130        SHUTDOWN.store(true, Ordering::Relaxed);
131    })?;
132
133    // WATCHDOG: ABORTS IF THE CONTROL LOOP STALLS FOR MORE THAN 10 SECONDS.
134    // LIBBPF MAP OPERATIONS CAN HANG ON KERNEL STALL / VERIFIER RELOAD /
135    // PERCPU CONTENTION; WITHOUT THIS, TELEMETRY AND KNOB WRITES STOP SILENTLY.
136    watchdog::spawn(&SHUTDOWN, Duration::from_secs(10));
137
138    let nr_cpus_display =
139        nr_cpus.unwrap_or_else(|| libbpf_rs::num_possible_cpus().unwrap_or(1) as u64);
140    let governor = std::fs::read_to_string("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor")
141        .unwrap_or_default()
142        .trim()
143        .to_string();
144
145    let smt_on = std::fs::read_to_string("/sys/devices/system/cpu/smt/active")
146        .map(|s| s.trim() == "1")
147        .unwrap_or(false);
148
149    log_info!(
150        "scx_pandemonium {} SMT {}",
151        build_id::full_version(env!("CARGO_PKG_VERSION")),
152        if smt_on { "on" } else { "off" }
153    );
154    log_info!(
155        "CPUS: {} (governor: {})",
156        nr_cpus_display,
157        if governor.is_empty() {
158            "unknown"
159        } else {
160            &governor
161        }
162    );
163    log_info!("VERBOSE: {}", verbose);
164
165    let mut is_restart = false;
166    loop {
167        // ON RESTART, WAIT FOR KERNEL STRUCT_OPS CLEANUP.
168        // DETACH IS ASYNCHRONOUS -- UNDER HEAVY LOAD (12C SATURATED),
169        // THE KERNEL NEEDS TIME TO FULLY UNREGISTER THE OLD SCHEDULER.
170        if is_restart {
171            std::thread::sleep(Duration::from_secs(2));
172        }
173
174        let mut open_object = MaybeUninit::uninit();
175        let mut sched = Scheduler::init(&mut open_object, nr_cpus)?;
176
177        // POPULATE CACHE TOPOLOGY AT STARTUP -- the one detect-and-populate
178        // sequence (topology.rs owns it: all computes before any map write,
179        // tuning knobs written last as the "go" signal). The SAME sequence
180        // re-runs on hotplug via CpuTopology::poll_hotplug from both control
181        // loops, so a CPU broken at boot self-corrects and the R_eff/phi/
182        // domain tables track the live width.
183        let mut last_online = topology::CpuTopology::online_cpu_count();
184        if let Err(e) = topology::CpuTopology::detect_and_populate(
185            &mut sched,
186            nr_cpus_display as usize,
187            phi_scale,
188        ) {
189            log_warn!("CACHE TOPOLOGY DETECT FAILED: {}", e);
190        }
191
192        let should_restart = if no_adaptive {
193            // BPF-ONLY MODE: SCHEDULER RUNS WITH DEFAULT KNOBS, NO RUST TUNING
194            // STILL PRINTS STATS SO BENCHMARKS GET TELEMETRY FOR BOTH PHASES
195            log_info!("PANDEMONIUM IS ACTIVE (BPF ONLY, CTRL+C TO EXIT)");
196            let mut prev = scheduler::PandemoniumStats::default();
197            while !SHUTDOWN.load(Ordering::Relaxed) && !sched.exited() {
198                watchdog::LOOP_HEARTBEAT.fetch_add(1, Ordering::Relaxed);
199                std::thread::sleep(Duration::from_secs(1));
200
201                // HOTPLUG: re-derive topology when the online set changes
202                // (BPF-only mode has no adaptive loop to carry the poll).
203                topology::CpuTopology::poll_hotplug(
204                    &mut sched,
205                    nr_cpus_display as usize,
206                    phi_scale,
207                    &mut last_online,
208                );
209
210                let stats = sched.read_stats();
211
212                let delta_d = stats.nr_dispatches.wrapping_sub(prev.nr_dispatches);
213                let delta_idle = stats.nr_idle_hits.wrapping_sub(prev.nr_idle_hits);
214                let delta_shared = stats.nr_shared.wrapping_sub(prev.nr_shared);
215                let delta_preempt = stats.nr_preempt.wrapping_sub(prev.nr_preempt);
216                let delta_keep = stats.nr_keep_running.wrapping_sub(prev.nr_keep_running);
217                let delta_parks = stats.nr_osc_park.wrapping_sub(prev.nr_osc_park);
218                let delta_wake_sum = stats.wake_lat_sum.wrapping_sub(prev.wake_lat_sum);
219                let delta_wake_samples = stats.wake_lat_samples.wrapping_sub(prev.wake_lat_samples);
220                let delta_hard = stats.nr_hard_kicks.wrapping_sub(prev.nr_hard_kicks);
221                let delta_soft = stats.nr_soft_kicks.wrapping_sub(prev.nr_soft_kicks);
222                let delta_steal = stats.nr_steal.wrapping_sub(prev.nr_steal);
223                let delta_enq_wake = stats.nr_enq_wakeup.wrapping_sub(prev.nr_enq_wakeup);
224                let delta_enq_requeue = stats.nr_enq_requeue.wrapping_sub(prev.nr_enq_requeue);
225                let wake_avg_us = if delta_wake_samples > 0 {
226                    delta_wake_sum / delta_wake_samples / 1000
227                } else {
228                    0
229                };
230
231                let d_idle_sum = stats.wake_lat_idle_sum.wrapping_sub(prev.wake_lat_idle_sum);
232                let d_idle_cnt = stats.wake_lat_idle_cnt.wrapping_sub(prev.wake_lat_idle_cnt);
233                let d_kick_sum = stats.wake_lat_kick_sum.wrapping_sub(prev.wake_lat_kick_sum);
234                let d_kick_cnt = stats.wake_lat_kick_cnt.wrapping_sub(prev.wake_lat_kick_cnt);
235                let lat_idle_us = if d_idle_cnt > 0 {
236                    d_idle_sum / d_idle_cnt / 1000
237                } else {
238                    0
239                };
240                let lat_kick_us = if d_kick_cnt > 0 {
241                    d_kick_sum / d_kick_cnt / 1000
242                } else {
243                    0
244                };
245                let delta_reenq = stats.nr_reenqueue.wrapping_sub(prev.nr_reenqueue);
246
247                // L2 CACHE AFFINITY DELTAS
248                let dl2_hb = stats.nr_l2_hit_batch.wrapping_sub(prev.nr_l2_hit_batch);
249                let dl2_mb = stats.nr_l2_miss_batch.wrapping_sub(prev.nr_l2_miss_batch);
250                let dl2_hi = stats
251                    .nr_l2_hit_interactive
252                    .wrapping_sub(prev.nr_l2_hit_interactive);
253                let dl2_mi = stats
254                    .nr_l2_miss_interactive
255                    .wrapping_sub(prev.nr_l2_miss_interactive);
256                let l2_pct_b = if dl2_hb + dl2_mb > 0 {
257                    dl2_hb * 100 / (dl2_hb + dl2_mb)
258                } else {
259                    0
260                };
261                let l2_pct_i = if dl2_hi + dl2_mi > 0 {
262                    dl2_hi * 100 / (dl2_hi + dl2_mi)
263                } else {
264                    0
265                };
266
267                let idle_pct = if delta_d > 0 {
268                    delta_idle * 100 / delta_d
269                } else {
270                    0
271                };
272
273                let sojourn_ms = stats.batch_sojourn_ns / 1_000_000;
274                let longrun_label = if stats.longrun_mode_active > 0 {
275                    " LONGRUN"
276                } else {
277                    ""
278                };
279
280                if verbose {
281                    println!(
282                        "d/s: {:<8} idle: {}% shared: {:<6} preempt: {:<4} keep: {:<4} kick: H={:<4} S={:<4} enq: W={:<4} R={:<4} wake: {}us lat_idle: {}us lat_kick: {}us reenq: {} sjrn: {}ms l2: B={}% I={}% [BPF{}]",
283                        delta_d, idle_pct, delta_shared, delta_preempt, delta_keep,
284                        delta_hard, delta_soft, delta_enq_wake, delta_enq_requeue,
285                        wake_avg_us, lat_idle_us, lat_kick_us,
286                        delta_reenq, sojourn_ms, l2_pct_b, l2_pct_i,
287                        longrun_label,
288                    );
289                }
290
291                sched.log.snapshot(
292                    delta_d,
293                    delta_idle,
294                    delta_shared,
295                    delta_preempt,
296                    delta_keep,
297                    delta_parks,
298                    wake_avg_us,
299                    delta_hard,
300                    delta_soft,
301                    lat_idle_us,
302                    lat_kick_us,
303                    delta_steal,
304                );
305
306                prev = stats;
307            }
308
309            // KNOBS SUMMARY: CAPTURED BY TEST HARNESS FOR ARCHIVE
310            let knobs = sched.read_tuning_knobs();
311            let final_stats = sched.read_stats();
312            let l2_total_b = final_stats.nr_l2_hit_batch + final_stats.nr_l2_miss_batch;
313            let l2_total_i = final_stats.nr_l2_hit_interactive + final_stats.nr_l2_miss_interactive;
314            let l2_cum_b = if l2_total_b > 0 {
315                final_stats.nr_l2_hit_batch * 100 / l2_total_b
316            } else {
317                0
318            };
319            let l2_cum_i = if l2_total_i > 0 {
320                final_stats.nr_l2_hit_interactive * 100 / l2_total_i
321            } else {
322                0
323            };
324            // CROSS-DOMAIN SCATTER ATTRIBUTION (PER XDOM_* PATH), ON THE [KNOBS]
325            // LINE SO THE BENCH SUITE CAPTURES IT UNIFORMLY ACROSS BPF/ADAPTIVE
326            // (LETS THE SUITE COMPARE SCATTER BETWEEN MODES). scatter_pct IS THE
327            // PLACEMENT-SIDE FRACTION (idx 0..6).
328            let x = &final_stats.nr_cross_domain;
329            let x_scatter: u64 = x[0..6].iter().sum();
330            let x_scatter_pct = if final_stats.nr_dispatches > 0 {
331                x_scatter * 100 / final_stats.nr_dispatches
332            } else {
333                0
334            };
335            println!(
336                "[KNOBS] regime=BPF slice_ns={} batch_ns={} preempt_ns={} l2_hit=B:{}%/I:{}% cross_domain_scatter_pct={} cross_domain_sel_tight={} cross_domain_sel_sync={} cross_domain_sel_normal={} cross_domain_sel_dfl={} cross_domain_enq_t1={} cross_domain_enq_t2={} cross_domain_steal={} cross_domain_step5={}",
337                knobs.slice_ns, knobs.batch_slice_ns,
338                knobs.preempt_thresh_ns,
339                l2_cum_b, l2_cum_i,
340                x_scatter_pct, x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7],
341            );
342
343            sched.read_exit_info()
344        } else {
345            // ADAPTIVE MODE: BPF + SINGLE-THREAD MONITOR LOOP
346            log_info!("PANDEMONIUM IS ACTIVE (CTRL+C TO EXIT)");
347            adaptive::monitor_loop(&mut sched, &SHUTDOWN, verbose, nr_cpus_display, phi_scale)?
348        };
349
350        log_info!("PANDEMONIUM IS SHUTTING DOWN");
351
352        if dump_log {
353            sched.log.dump();
354        }
355        sched.log.summary();
356
357        if !should_restart || SHUTDOWN.load(Ordering::Relaxed) {
358            break;
359        }
360
361        // RESET SHUTDOWN FOR RESTART
362        SHUTDOWN.store(false, Ordering::Relaxed);
363        log_info!("RESTARTING PANDEMONIUM...");
364        is_restart = true;
365    }
366
367    log_info!("Shutdown complete");
368    Ok(())
369}