Skip to main content

scx_flow/
main.rs

1// SPDX-License-Identifier: GPL-2.0
2//
3// Copyright (c) 2026 Galih Tama <galpt@v.recipes>
4//
5// This software may be used and distributed according to the terms of the GNU
6// General Public License version 2.
7
8mod bpf_skel;
9pub use bpf_skel::types;
10pub use bpf_skel::*;
11pub mod bpf_intf;
12pub use bpf_intf::*;
13
14mod carriage;
15mod stats;
16mod webui;
17use std::mem::MaybeUninit;
18use std::sync::atomic::AtomicBool;
19use std::sync::atomic::Ordering;
20use std::sync::Arc;
21use std::time::Duration;
22
23use anyhow::Result;
24use clap::CommandFactory;
25use clap::Parser;
26use clap_complete::generate;
27use clap_complete::Shell;
28use crossbeam::channel::RecvTimeoutError;
29use libbpf_rs::MapCore;
30use log::info;
31use scx_stats::prelude::*;
32use scx_utils::build_id;
33use scx_utils::compat;
34use scx_utils::libbpf_clap_opts::LibbpfOpts;
35use scx_utils::scx_ops_attach;
36use scx_utils::scx_ops_load;
37use scx_utils::scx_ops_open;
38use scx_utils::try_set_rlimit_infinity;
39use scx_utils::uei_exited;
40use scx_utils::uei_report;
41use scx_utils::UserExitInfo;
42
43use stats::Metrics;
44
45const SCHEDULER_NAME: &str = "scx_flow";
46
47fn full_version() -> String {
48    build_id::full_version(env!("CARGO_PKG_VERSION"))
49}
50
51#[derive(Debug, Parser)]
52#[command(name = SCHEDULER_NAME, version, disable_version_flag = true)]
53struct Opts {
54    #[clap(long)]
55    stats: Option<f64>,
56
57    #[clap(long)]
58    monitor: Option<f64>,
59
60    #[clap(short, long, action = clap::ArgAction::SetTrue)]
61    debug: bool,
62
63    #[clap(short = 'V', long, action = clap::ArgAction::SetTrue)]
64    version: bool,
65
66    #[clap(long = "no-webui", action = clap::ArgAction::SetTrue)]
67    no_webui: bool,
68
69    #[clap(long, action = clap::ArgAction::SetTrue)]
70    no_autotune: bool,
71
72    #[clap(long, value_name = "SHELL", hide = true)]
73    completions: Option<Shell>,
74
75    #[clap(flatten, next_help_heading = "Libbpf Options")]
76    libbpf: LibbpfOpts,
77}
78
79struct Scheduler<'a> {
80    skel: BpfSkel<'a>,
81    struct_ops: Option<libbpf_rs::Link>,
82    stats_server: StatsServer<(), Metrics>,
83    webui_tx: Option<crossbeam::channel::Sender<stats::WebMetrics>>,
84    started_at: std::time::Instant,
85}
86
87#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
88struct CpuPolicyStateAgg {
89    budget_exhaustions: u64,
90    runnable_wakeups: u64,
91    cpu_migrations: u64,
92}
93
94impl<'a> Scheduler<'a> {
95    fn read_cpu_policy_state(&self) -> CpuPolicyStateAgg {
96        let key = 0u32.to_ne_bytes();
97        let mut agg = CpuPolicyStateAgg::default();
98
99        let percpu_vals: Vec<Vec<u8>> = match self
100            .skel
101            .maps
102            .cpu_state
103            .lookup_percpu(&key, libbpf_rs::MapFlags::ANY)
104        {
105            Ok(Some(vals)) => vals,
106            _ => return agg,
107        };
108
109        for cpu_val in percpu_vals.iter() {
110            if cpu_val.len() < std::mem::size_of::<bpf_intf::flow_cpu_state>() {
111                continue;
112            }
113
114            let state = unsafe {
115                std::ptr::read_unaligned(cpu_val.as_ptr() as *const bpf_intf::flow_cpu_state)
116            };
117
118            agg.budget_exhaustions = agg
119                .budget_exhaustions
120                .saturating_add(state.budget_exhaustions);
121            agg.runnable_wakeups = agg.runnable_wakeups.saturating_add(state.runnable_wakeups);
122            agg.cpu_migrations = agg.cpu_migrations.saturating_add(state.cpu_migrations);
123        }
124
125        agg
126    }
127
128    fn init(
129        opts: &'a Opts,
130        open_object: &'a mut MaybeUninit<libbpf_rs::OpenObject>,
131        shutdown: Arc<AtomicBool>,
132    ) -> Result<Self> {
133        try_set_rlimit_infinity();
134
135        let mut skel_builder = BpfSkelBuilder::default();
136        skel_builder.obj_builder.debug(opts.debug);
137
138        let open_opts = opts.libbpf.clone().into_bpf_open_opts();
139        let mut skel = scx_ops_open!(skel_builder, open_object, flow_ops, open_opts)?;
140
141        skel.struct_ops.flow_ops_mut().flags = *compat::SCX_OPS_ENQ_EXITING
142            | *compat::SCX_OPS_ENQ_LAST
143            | *compat::SCX_OPS_ENQ_MIGRATION_DISABLED
144            | *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP;
145
146        let mut skel = scx_ops_load!(skel, flow_ops, uei)?;
147
148        // Write scheduler PID to BSS so BPF can bypass the carriage.
149        {
150            let key: u32 = 0;
151            let mut bss_raw = skel
152                .maps
153                .bss
154                .lookup(&key.to_ne_bytes(), libbpf_rs::MapFlags::ANY)
155                .ok()
156                .flatten()
157                .unwrap_or_default();
158            let pid_offset = std::mem::offset_of!(types::bss, flow_scheduler_pid);
159            let pid_bytes = (std::process::id() as u64).to_ne_bytes();
160            let bss_slice = bss_raw.as_mut_slice();
161            if pid_offset + 8 <= bss_slice.len() {
162                bss_slice[pid_offset..pid_offset + 8].copy_from_slice(&pid_bytes);
163            }
164            let _ = skel
165                .maps
166                .bss
167                .update(&key.to_ne_bytes(), &bss_raw, libbpf_rs::MapFlags::ANY);
168        }
169
170        // Discover topology and write into BSS.
171        carriage::init_topology(&mut skel)?;
172
173        let struct_ops = scx_ops_attach!(skel, flow_ops)?;
174
175        let stats_server = StatsServer::new(stats::server_data()).launch()?;
176
177        let webui_tx: Option<crossbeam::channel::Sender<stats::WebMetrics>> = if !opts.no_webui {
178            let (tx, rx) = crossbeam::channel::unbounded::<stats::WebMetrics>();
179            let shutdown = shutdown.clone();
180            std::thread::spawn(move || {
181                webui::start(rx, shutdown);
182            });
183            Some(tx)
184        } else {
185            None
186        };
187
188        Ok(Self {
189            skel,
190            struct_ops: Some(struct_ops),
191            stats_server,
192            webui_tx,
193            started_at: std::time::Instant::now(),
194        })
195    }
196
197    fn get_metrics(&self) -> Metrics {
198        let bss_data = self
199            .skel
200            .maps
201            .bss_data
202            .as_ref()
203            .expect("bss_data missing — BPF object has no .bss section");
204        let cpu_policy_state = self.read_cpu_policy_state();
205        Metrics {
206            on_cpu: bss_data.on_cpu,
207            total_runtime: bss_data.total_runtime,
208            uptime_ns: self.started_at.elapsed().as_nanos() as u64,
209
210            prio_dispatches: bss_data.prio_dispatches,
211            pinned_dispatches: bss_data.pinned_dispatches,
212
213            carriage_producer: bss_data.carriage_producer,
214
215            budget_exhaustions: bss_data.budget_exhaustions + cpu_policy_state.budget_exhaustions,
216            runnable_wakeups: bss_data.runnable_wakeups + cpu_policy_state.runnable_wakeups,
217            cpu_migrations: bss_data.cpu_migrations + cpu_policy_state.cpu_migrations,
218        }
219    }
220
221    fn get_web_metrics(&self) -> stats::WebMetrics {
222        let metrics = self.get_metrics();
223        let bss_data = self
224            .skel
225            .maps
226            .bss_data
227            .as_ref()
228            .expect("bss_data missing — BPF object has no .bss section");
229
230        let nr_cpus = bss_data.nr_cpu_ids as usize;
231        let mut per_cpu = Vec::with_capacity(nr_cpus);
232        for cpu in 0..nr_cpus {
233            if cpu >= 1024 {
234                break;
235            }
236            per_cpu.push(stats::PerCpuMetrics {
237                id: cpu as u32,
238                freq_khz: bss_data.per_cpu_max_freq_khz[cpu],
239                llc_id: bss_data.per_cpu_llc_id[cpu] as u32,
240                smt: bss_data.per_cpu_is_smt[cpu] != 0,
241            });
242        }
243
244        let closed_slot = (bss_data.carriage_producer.wrapping_sub(1) & 63) as usize;
245        let carriage_filling_count = if closed_slot < 64 {
246            bss_data.carriage_pool[closed_slot].count as u64
247        } else {
248            0
249        };
250
251        stats::WebMetrics {
252            stats: metrics,
253            per_cpu,
254            carriage_filling_count,
255        }
256    }
257
258    fn exited(&self) -> bool {
259        uei_exited!(&self.skel, uei)
260    }
261
262    fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
263        let (res_ch, req_ch) = self.stats_server.channels();
264
265        while !shutdown.load(Ordering::Relaxed) && !self.exited() {
266            match req_ch.recv_timeout(Duration::from_millis(250)) {
267                Ok(()) => {
268                    let m = self.get_metrics();
269                    if let Some(ref tx) = self.webui_tx {
270                        let wm = self.get_web_metrics();
271                        let _ = tx.try_send(wm);
272                    }
273                    res_ch.send(m)?;
274                }
275                Err(RecvTimeoutError::Timeout) => {
276                    if let Some(ref tx) = self.webui_tx {
277                        let wm = self.get_web_metrics();
278                        let _ = tx.try_send(wm);
279                    }
280                }
281                Err(e) => Err(e)?,
282            }
283        }
284
285        let _ = self.struct_ops.take();
286        uei_report!(&self.skel, uei)
287    }
288}
289
290fn main() -> Result<()> {
291    let opts = Opts::parse();
292
293    if let Some(shell) = opts.completions {
294        generate(
295            shell,
296            &mut Opts::command(),
297            SCHEDULER_NAME,
298            &mut std::io::stdout(),
299        );
300        return Ok(());
301    }
302
303    let monitor_only = opts.monitor.is_some();
304
305    if opts.version {
306        println!("{} {}", SCHEDULER_NAME, full_version());
307        return Ok(());
308    }
309
310    if !monitor_only {
311        simplelog::SimpleLogger::init(
312            if opts.debug {
313                simplelog::LevelFilter::Debug
314            } else {
315                simplelog::LevelFilter::Info
316            },
317            simplelog::Config::default(),
318        )?;
319
320        info!("{} {}", SCHEDULER_NAME, full_version());
321        info!("Starting {} scheduler", SCHEDULER_NAME);
322    }
323
324    let shutdown = Arc::new(AtomicBool::new(false));
325    let shutdown_clone = shutdown.clone();
326
327    ctrlc::set_handler(move || {
328        shutdown_clone.store(true, Ordering::Relaxed);
329    })?;
330
331    if let Some(intv) = opts.monitor.or(opts.stats) {
332        let monitor_shutdown = shutdown.clone();
333        let jh = std::thread::spawn(move || {
334            if let Err(err) = stats::monitor(Duration::from_secs_f64(intv), monitor_shutdown) {
335                log::warn!("stats monitor thread finished with error: {err}");
336            }
337        });
338
339        if monitor_only {
340            let _ = jh.join();
341            return Ok(());
342        }
343    }
344
345    let mut open_object = MaybeUninit::<libbpf_rs::OpenObject>::uninit();
346    let mut sched = Scheduler::init(&opts, &mut open_object, shutdown.clone())?;
347    sched.run(shutdown)?;
348
349    info!("Scheduler exited");
350
351    Ok(())
352}