1mod bpf_skel;
11pub use bpf_skel::*;
12pub mod bpf_intf;
13pub use bpf_intf::*;
14#[path = "rust/config.rs"]
15mod config;
16#[path = "rust/snapshot.rs"]
17mod snapshot;
18#[path = "rust/stats.rs"]
19mod stats;
20#[path = "rust/topology.rs"]
21mod topology;
22#[path = "rust/webui.rs"]
23mod webui;
24
25use std::mem::MaybeUninit;
26use std::sync::Arc;
27use std::sync::atomic::AtomicBool;
28use std::sync::atomic::Ordering;
29use std::time::Duration;
30
31use anyhow::Result;
32use clap::CommandFactory;
33use clap::Parser;
34use clap_complete::Shell;
35use clap_complete::generate;
36use crossbeam::channel::RecvTimeoutError;
37use log::info;
38use scx_stats::prelude::*;
39use scx_utils::UserExitInfo;
40use scx_utils::build_id;
41use scx_utils::libbpf_clap_opts::LibbpfOpts;
42use scx_utils::scx_ops_attach;
43use scx_utils::scx_ops_load;
44use scx_utils::scx_ops_open;
45use scx_utils::try_set_rlimit_infinity;
46use scx_utils::uei_exited;
47use scx_utils::uei_report;
48
49use config::Config;
50use stats::Metrics;
51
52const SCHEDULER_NAME: &str = "scx_flow";
54#[cfg(test)]
56const MAX_CPUS: usize = crate::bpf_intf::flow_consts_FLOW_MAX_CPUS as usize;
57
58fn full_version() -> String {
59 build_id::full_version(env!("CARGO_PKG_VERSION"))
60}
61
62fn poll_interval_secs(v: f64) -> Result<Duration> {
66 if !v.is_finite() || v <= 0.0 || v > 3600.0 {
67 anyhow::bail!("interval bad {v}");
68 }
69 Ok(Duration::from_secs_f64(v))
70}
71
72#[derive(Debug, Parser)]
73#[command(name = SCHEDULER_NAME, version, disable_version_flag = true)]
74struct Opts {
75 #[clap(long)]
77 stats: Option<f64>,
78 #[clap(long)]
80 monitor: Option<f64>,
81 #[clap(short = 'd', long, action = clap::ArgAction::SetTrue)]
83 debug: bool,
84 #[clap(short = 'v', long, action = clap::ArgAction::SetTrue)]
86 verbose: bool,
87 #[clap(long, default_value = "1048576")]
89 exit_dump_len: u32,
90 #[clap(short = 'V', long, action = clap::ArgAction::SetTrue)]
92 version: bool,
93 #[clap(long)]
95 help_stats: bool,
96 #[clap(long, value_name = "SHELL", hide = true)]
98 completions: Option<Shell>,
99 #[clap(long = "no-webui", action = clap::ArgAction::SetTrue)]
101 no_webui: bool,
102 #[clap(flatten, next_help_heading = "Libbpf Options")]
103 libbpf: LibbpfOpts,
104}
105
106pub(crate) struct Scheduler<'a> {
112 skel: BpfSkel<'a>,
113 struct_ops: Option<libbpf_rs::Link>,
114 stats_server: StatsServer<(), Metrics>,
115 started_at: std::time::Instant,
116 webui_tx: Option<crossbeam::channel::Sender<stats::WebMetrics>>,
118 online_cpus: Vec<u32>,
120 smt: Vec<bool>,
122 topology: String,
124}
125
126impl<'a> Scheduler<'a> {
127 fn init(
128 opts: &'a Opts,
129 open_object: &'a mut MaybeUninit<libbpf_rs::OpenObject>,
130 shutdown: Arc<AtomicBool>,
131 ) -> Result<Self> {
132 try_set_rlimit_infinity();
133 let mut bld = BpfSkelBuilder::default();
134 bld.obj_builder.debug(opts.debug || opts.verbose);
135 let open_opts = opts.libbpf.clone().into_bpf_open_opts();
136 let mut skel = scx_ops_open!(bld, open_object, flow_ops, open_opts)?;
137 let cfg = Config::default();
139 cfg.validate()?;
140 info!("Config: {}", cfg.describe());
141 skel.struct_ops.flow_ops_mut().exit_dump_len = opts.exit_dump_len;
143 let mut skel = scx_ops_load!(skel, flow_ops, uei)?;
144 let _ = &mut skel;
145 let rows = topology::topo_rows();
148 Self::seed_topo_with(&mut skel, &rows);
149 let struct_ops = scx_ops_attach!(skel, flow_ops)?;
150 let stats_server = StatsServer::new(stats::server_data()).launch()?;
151 let webui_tx = if opts.no_webui {
153 None
154 } else {
155 let (tx, rx) = crossbeam::channel::bounded::<stats::WebMetrics>(16);
156 let sd = shutdown.clone();
157 std::thread::spawn(move || {
158 webui::start(rx, sd);
159 });
160 Some(tx)
161 };
162 let online_cpus: Vec<u32> = rows.iter().map(|(cpu, _, _)| *cpu).collect();
163 let smt: Vec<bool> = rows
165 .iter()
166 .map(|(cpu, sib, _)| topology::is_smt_thread(*cpu, *sib))
167 .collect();
168 let topology = topology::describe_topology(&rows);
169 info!("Topology: {topology}");
170 Ok(Self {
171 skel,
172 struct_ops: Some(struct_ops),
173 stats_server,
174 started_at: std::time::Instant::now(),
175 webui_tx,
176 online_cpus,
177 smt,
178 topology,
179 })
180 }
181
182 fn exited(&self) -> bool {
183 uei_exited!(&self.skel, uei)
184 }
185
186 fn seed_topo_with(skel: &mut BpfSkel<'_>, rows: &[(u32, u32, u32)]) {
190 use libbpf_rs::MapCore;
191 for (cpu, sib, node) in rows {
192 let key = cpu.to_ne_bytes();
193 let mut val = [0u8; 8];
194 val[0..4].copy_from_slice(&sib.to_ne_bytes());
195 val[4..8].copy_from_slice(&node.to_ne_bytes());
196 if let Err(e) = skel
197 .maps
198 .topo_stor
199 .update(&key, &val, libbpf_rs::MapFlags::ANY)
200 {
201 log::warn!("topo seed failed for cpu {cpu}: {e}");
202 }
203 }
204 }
205
206 fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
207 let (res_ch, req_ch) = self.stats_server.channels();
208 while !shutdown.load(Ordering::Relaxed) && !self.exited() {
213 match req_ch.recv_timeout(Duration::from_millis(100)) {
214 Ok(()) => {
215 if let Some(ref tx) = self.webui_tx {
216 if !tx.is_full() {
217 let web = self.get_web_metrics();
218 let stats = web.stats.clone();
219 let _ = tx.try_send(web);
220 res_ch.send(stats)?
221 } else {
222 res_ch.send(self.get_metrics())?
223 }
224 } else {
225 res_ch.send(self.get_metrics())?
226 }
227 }
228 Err(RecvTimeoutError::Timeout) => {
229 if let Some(ref tx) = self.webui_tx
230 && !tx.is_full()
231 {
232 let web = self.get_web_metrics();
233 let _ = tx.try_send(web);
234 }
235 }
236 Err(e) => Err(e)?,
237 }
238 }
239 let m = self.get_metrics();
240 info!(
241 "exit ins={} req={} done={} local={} node={} machine={} kick={} adm={} rej={} miss={} gate={} pkick={} pskip={} redrej={} redrec={} runtime={} oncpu={}",
242 m.inserts,
243 m.requeues,
244 m.completions,
245 m.local_moves,
246 m.node_moves,
247 m.machine_moves,
248 m.kicks,
249 m.admits,
250 m.rejects,
251 m.misses,
252 m.gate_rejects,
253 m.preempt_kicks,
254 m.preempt_skipped,
255 m.red_rejects,
256 m.red_reclaims,
257 m.total_runtime,
258 m.on_cpu,
259 );
260 let _ = self.struct_ops.take();
261 uei_report!(&self.skel, uei)
262 }
263}
264
265fn main() -> Result<()> {
266 let opts = Opts::parse();
267 if let Some(shell) = opts.completions {
268 generate(
269 shell,
270 &mut Opts::command(),
271 SCHEDULER_NAME,
272 &mut std::io::stdout(),
273 );
274 return Ok(());
275 }
276 let only = opts.monitor.is_some();
277 if opts.version {
278 println!("{} {}", SCHEDULER_NAME, full_version());
279 return Ok(());
280 }
281 if opts.help_stats {
282 println!("stats: top");
283 return Ok(());
284 }
285 if !only {
286 simplelog::SimpleLogger::init(
287 if opts.debug {
288 simplelog::LevelFilter::Debug
289 } else {
290 simplelog::LevelFilter::Info
291 },
292 simplelog::Config::default(),
293 )?;
294 info!("{} {}", SCHEDULER_NAME, full_version());
295 info!("Starting {} scheduler", SCHEDULER_NAME);
296 }
297 let shutdown = Arc::new(AtomicBool::new(false));
298 let sd = shutdown.clone();
299 ctrlc::set_handler(move || {
300 sd.store(true, Ordering::Relaxed);
301 })?;
302 if let Some(intv) = opts.monitor.or(opts.stats) {
303 let dur = poll_interval_secs(intv)?;
304 let sd = shutdown.clone();
305 let jh = std::thread::spawn(move || {
306 if let Err(e) = stats::monitor(dur, sd) {
307 log::warn!("monitor failed: {e}");
308 }
309 });
310 if only {
311 let _ = jh.join();
312 return Ok(());
313 }
314 }
315 let mut open_object = MaybeUninit::<libbpf_rs::OpenObject>::uninit();
316 let mut sched = Scheduler::init(&opts, &mut open_object, shutdown.clone())?;
317 sched.run(shutdown)?;
318 info!("Scheduler exited");
319 Ok(())
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn scheduler_name_is_flow() {
328 assert_eq!(SCHEDULER_NAME, "scx_flow");
329 assert!(poll_interval_secs(1.0).is_ok());
330 assert!(poll_interval_secs(0.0).is_err());
331 assert!(poll_interval_secs(-1.0).is_err());
332 assert!(poll_interval_secs(f64::NAN).is_err());
333 assert!(poll_interval_secs(f64::INFINITY).is_err());
334 assert!(poll_interval_secs(3601.0).is_err());
335 }
336
337 #[test]
338 fn max_cpus_matches_header() {
339 assert_eq!(
340 MAX_CPUS,
341 crate::bpf_intf::flow_consts_FLOW_MAX_CPUS as usize
342 );
343 }
344
345 #[test]
346 fn quantum_matches_header() {
347 assert_eq!(
348 crate::config::QUANTUM_NS,
349 crate::bpf_intf::flow_consts_FLOW_QUANTUM_NS as u64
350 );
351 assert_eq!(crate::config::QUANTUM_NS, 1_000_000);
352 assert_eq!(crate::config::WEIGHT_BASE, 128);
353 assert_eq!(crate::config::WEIGHT_MIN, 1);
354 assert_eq!(crate::config::WEIGHT_MAX, 16_384);
355 }
356
357 #[test]
358 fn slot_matches_header() {
359 assert_eq!(crate::bpf_intf::flow_consts_FLOW_MACHINE as u64, 0x5A00);
360 assert_eq!(crate::bpf_intf::flow_consts_FLOW_OVERFLOW as u64, 0x5A01);
361 assert_eq!(crate::bpf_intf::flow_consts_FLOW_MAX_DSQS as u64, 1042);
362 }
363
364 #[test]
365 fn period_matches_header() {
366 assert_eq!(
367 crate::config::PERIOD_NS,
368 crate::bpf_intf::flow_consts_FLOW_PERIOD_NS as u64
369 );
370 assert_eq!(crate::config::PERIOD_NS, 80_000_000);
371 }
372
373 #[test]
374 fn task_size_is_72() {
375 assert_eq!(std::mem::size_of::<crate::bpf_intf::flow_task_ctx>(), 72);
376 }
377
378 #[test]
379 fn cpu_size_is_16() {
380 assert_eq!(std::mem::size_of::<crate::bpf_intf::flow_cpu_state>(), 16);
381 }
382
383 #[test]
384 fn topo_size_is_8() {
385 assert_eq!(std::mem::size_of::<crate::bpf_intf::flow_topo>(), 8);
386 }
387
388 #[test]
389 fn sched_stats_size_is_136() {
390 assert_eq!(
391 std::mem::size_of::<crate::bpf_intf::flow_sched_stats>(),
392 136
393 );
394 }
395
396 #[test]
397 fn fixed_red_matches_header() {
398 assert_eq!(
399 crate::bpf_intf::flow_consts_FLOW_RED_TOL_NS as u64,
400 crate::config::RED_TOL_NS
401 );
402 assert_eq!(crate::config::RED_TOL_NS, 64_000);
403 assert_eq!(
404 crate::bpf_intf::flow_consts_FLOW_RED_EMAX_NS as u64,
405 crate::config::RED_EMAX_NS
406 );
407 assert_eq!(crate::config::RED_EMAX_NS, 256_000);
408 }
409
410 #[test]
411 fn kick_rule_matches_header() {
412 assert_eq!(
413 crate::bpf_intf::flow_consts_FLOW_PREEMPT_MARGIN_NS as u64,
414 crate::config::PREEMPT_MARGIN_NS
415 );
416 assert_eq!(crate::config::PREEMPT_MARGIN_NS, 100_000);
417 assert_eq!(
418 crate::bpf_intf::flow_consts_FLOW_PREEMPT_TAIL_NS as u64,
419 crate::config::PREEMPT_TAIL_NS
420 );
421 assert_eq!(crate::config::PREEMPT_TAIL_NS, 100_000);
422 }
423
424 #[test]
425 fn hint_matches_header() {
426 assert_eq!(
427 crate::config::HINT_MAX,
428 crate::bpf_intf::flow_consts_FLOW_HINT_MAX as u64
429 );
430 assert_eq!(crate::config::HINT_MAX, 8192);
431 }
432}