1mod bpf_skel;
9pub use bpf_skel::*;
10pub mod bpf_intf;
11pub use bpf_intf::*;
12
13mod stats;
14use std::collections::HashSet;
15use std::ffi::{c_int, c_ulong};
16use std::mem::MaybeUninit;
17use std::sync::atomic::AtomicBool;
18use std::sync::atomic::Ordering;
19use std::sync::Arc;
20use std::time::Duration;
21
22use anyhow::bail;
23use anyhow::Context;
24use anyhow::Result;
25use clap::Parser;
26use crossbeam::channel::RecvTimeoutError;
27use libbpf_rs::OpenObject;
28use libbpf_rs::ProgramInput;
29use log::{debug, info, warn};
30use scx_stats::prelude::*;
31use scx_utils::build_id;
32use scx_utils::compat;
33use scx_utils::get_primary_cpus;
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::Powermode;
42use scx_utils::Topology;
43use scx_utils::UserExitInfo;
44use scx_utils::NR_CPU_IDS;
45use stats::Metrics;
46
47const SCHEDULER_NAME: &str = "scx_beerland";
48
49#[derive(Debug, clap::Parser)]
50#[command(
51 name = "scx_beerland",
52 version,
53 disable_version_flag = true,
54 about = "Scheduler designed to prioritize locality and scalability."
55)]
56struct Opts {
57 #[clap(long, default_value = "0")]
59 exit_dump_len: u32,
60
61 #[clap(short = 's', long, default_value = "1000")]
63 slice_us: u64,
64
65 #[clap(short = 'l', long, default_value = "20000")]
70 slice_us_lag: u64,
71
72 #[clap(short = 'c', long, default_value = "0")]
77 cpu_busy_thresh: u64,
78
79 #[clap(short = 'm', long)]
91 primary_domain: Option<String>,
92
93 #[clap(short = 'n', long, action = clap::ArgAction::SetTrue)]
95 disable_numa: bool,
96
97 #[clap(long)]
99 stats: Option<f64>,
100
101 #[clap(long)]
104 monitor: Option<f64>,
105
106 #[clap(short = 'v', long, action = clap::ArgAction::SetTrue)]
108 verbose: bool,
109
110 #[clap(short = 'V', long, action = clap::ArgAction::SetTrue)]
112 version: bool,
113
114 #[clap(long)]
116 help_stats: bool,
117
118 #[clap(flatten, next_help_heading = "Libbpf Options")]
119 pub libbpf: LibbpfOpts,
120}
121
122pub fn parse_cpu_list(optarg: &str) -> Result<Vec<usize>, String> {
123 let mut cpus = Vec::new();
124 let mut seen = HashSet::new();
125
126 if let Some(mode) = match optarg {
128 "powersave" => Some(Powermode::Powersave),
129 "performance" => Some(Powermode::Performance),
130 "turbo" => Some(Powermode::Turbo),
131 "all" => Some(Powermode::Any),
132 _ => None,
133 } {
134 return get_primary_cpus(mode).map_err(|e| e.to_string());
135 }
136
137 if optarg
139 .chars()
140 .any(|c| !c.is_ascii_digit() && c != '-' && c != ',' && !c.is_whitespace())
141 {
142 return Err("Invalid character in CPU list".to_string());
143 }
144
145 let cleaned = optarg.replace(' ', "\t");
147
148 for token in cleaned.split(',') {
149 let token = token.trim_matches(|c: char| c.is_whitespace());
150
151 if token.is_empty() {
152 continue;
153 }
154
155 if let Some((start_str, end_str)) = token.split_once('-') {
156 let start = start_str
157 .trim()
158 .parse::<usize>()
159 .map_err(|_| "Invalid range start")?;
160 let end = end_str
161 .trim()
162 .parse::<usize>()
163 .map_err(|_| "Invalid range end")?;
164
165 if start > end {
166 return Err(format!("Invalid CPU range: {}-{}", start, end));
167 }
168
169 for i in start..=end {
170 if cpus.len() >= *NR_CPU_IDS {
171 return Err(format!("Too many CPUs specified (max {})", *NR_CPU_IDS));
172 }
173 if seen.insert(i) {
174 cpus.push(i);
175 }
176 }
177 } else {
178 let cpu = token
179 .parse::<usize>()
180 .map_err(|_| format!("Invalid CPU: {}", token))?;
181 if cpus.len() >= *NR_CPU_IDS {
182 return Err(format!("Too many CPUs specified (max {})", *NR_CPU_IDS));
183 }
184 if seen.insert(cpu) {
185 cpus.push(cpu);
186 }
187 }
188 }
189
190 Ok(cpus)
191}
192
193struct Scheduler<'a> {
194 skel: BpfSkel<'a>,
195 struct_ops: Option<libbpf_rs::Link>,
196 stats_server: StatsServer<(), Metrics>,
197}
198
199impl<'a> Scheduler<'a> {
200 fn init(opts: &'a Opts, open_object: &'a mut MaybeUninit<OpenObject>) -> Result<Self> {
201 try_set_rlimit_infinity();
202
203 let topo = Topology::new().unwrap();
205
206 let smt_enabled = topo.smt_enabled;
208
209 let nr_nodes = topo
211 .nodes
212 .values()
213 .filter(|node| !node.all_cpus.is_empty())
214 .count();
215 info!("NUMA nodes: {}", nr_nodes);
216
217 let numa_enabled = !opts.disable_numa && nr_nodes > 1;
219 if !numa_enabled {
220 info!("Disabling NUMA optimizations");
221 }
222
223 info!(
224 "{} {} {}",
225 SCHEDULER_NAME,
226 build_id::full_version(env!("CARGO_PKG_VERSION")),
227 if smt_enabled { "SMT on" } else { "SMT off" }
228 );
229
230 info!(
232 "scheduler options: {}",
233 std::env::args().collect::<Vec<_>>().join(" ")
234 );
235
236 let mut skel_builder = BpfSkelBuilder::default();
238 skel_builder.obj_builder.debug(opts.verbose);
239 let open_opts = opts.libbpf.clone().into_bpf_open_opts();
240 let mut skel = scx_ops_open!(skel_builder, open_object, beerland_ops, open_opts)?;
241
242 skel.struct_ops.beerland_ops_mut().exit_dump_len = opts.exit_dump_len;
243
244 let rodata = skel.maps.rodata_data.as_mut().unwrap();
246 rodata.slice_ns = opts.slice_us * 1000;
247 rodata.slice_lag = opts.slice_us_lag * 1000;
248 rodata.smt_enabled = smt_enabled;
249 rodata.numa_enabled = numa_enabled;
250 let cpu_busy_thresh = opts.cpu_busy_thresh.min(100);
251 let cpu_busy_filter = cpu_busy_thresh > 0;
252 let busy_threshold = if cpu_busy_filter {
253 cpu_busy_thresh * 1024 / 100
254 } else {
255 0
256 };
257 rodata.busy_threshold = busy_threshold;
258 if cpu_busy_filter {
259 info!(
260 "BPF accounted-time CPU steal filter: threshold {}%",
261 cpu_busy_thresh
262 );
263 } else {
264 info!("BPF accounted-time CPU steal filter disabled");
265 }
266
267 let primary_cpus = if let Some(ref domain) = opts.primary_domain {
269 match parse_cpu_list(domain) {
270 Ok(cpus) => cpus,
271 Err(e) => bail!("Error parsing primary domain: {}", e),
272 }
273 } else {
274 (0..*NR_CPU_IDS).collect()
275 };
276 if primary_cpus.len() < *NR_CPU_IDS {
277 info!("Primary CPUs: {:?}", primary_cpus);
278 rodata.primary_all = false;
279 } else {
280 rodata.primary_all = true;
281 }
282
283 for cpu in topo.all_cpus.values() {
285 rodata.cpu_capacity[cpu.id] = cpu.cpu_capacity as c_ulong;
286 }
287
288 skel.struct_ops.beerland_ops_mut().flags = *compat::SCX_OPS_ENQ_EXITING
290 | *compat::SCX_OPS_ENQ_LAST
291 | *compat::SCX_OPS_ENQ_MIGRATION_DISABLED
292 | *compat::SCX_OPS_ALWAYS_ENQ_IMMED
293 | *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP
294 | if numa_enabled {
295 *compat::SCX_OPS_BUILTIN_IDLE_PER_NODE
296 } else {
297 0
298 };
299 info!(
300 "scheduler flags: {:#x}",
301 skel.struct_ops.beerland_ops_mut().flags
302 );
303
304 let mut skel = scx_ops_load!(skel, beerland_ops, uei)?;
306
307 if smt_enabled {
309 Self::init_smt_domains(&mut skel, &topo)?;
310 }
311
312 if primary_cpus.len() < *NR_CPU_IDS {
314 for cpu in primary_cpus {
315 if let Err(err) = Self::enable_primary_cpu(&mut skel, cpu as i32) {
316 bail!("failed to add CPU {} to primary domain: error {}", cpu, err);
317 }
318 }
319 }
320
321 let struct_ops = Some(scx_ops_attach!(skel, beerland_ops)?);
323 let stats_server = StatsServer::new(stats::server_data()).launch()?;
324
325 Ok(Self {
326 skel,
327 struct_ops,
328 stats_server,
329 })
330 }
331
332 fn enable_sibling_cpu(
333 skel: &mut BpfSkel<'_>,
334 cpu: usize,
335 sibling_cpu: usize,
336 ) -> Result<(), u32> {
337 let prog = &mut skel.progs.enable_sibling_cpu;
338 let mut args = domain_arg {
339 cpu_id: cpu as c_int,
340 sibling_cpu_id: sibling_cpu as c_int,
341 };
342 let input = ProgramInput {
343 context_in: Some(unsafe {
344 std::slice::from_raw_parts_mut(
345 &mut args as *mut _ as *mut u8,
346 std::mem::size_of_val(&args),
347 )
348 }),
349 ..Default::default()
350 };
351 let out = prog.test_run(input).unwrap();
352 if out.return_value != 0 {
353 return Err(out.return_value);
354 }
355
356 Ok(())
357 }
358
359 fn enable_primary_cpu(skel: &mut BpfSkel<'_>, cpu: i32) -> Result<(), u32> {
360 let prog = &mut skel.progs.enable_primary_cpu;
361 let mut args = cpu_arg {
362 cpu_id: cpu as c_int,
363 };
364 let input = ProgramInput {
365 context_in: Some(unsafe {
366 std::slice::from_raw_parts_mut(
367 &mut args as *mut _ as *mut u8,
368 std::mem::size_of_val(&args),
369 )
370 }),
371 ..Default::default()
372 };
373 let out = prog.test_run(input).unwrap();
374 if out.return_value != 0 {
375 return Err(out.return_value);
376 }
377
378 Ok(())
379 }
380
381 fn init_smt_domains(skel: &mut BpfSkel<'_>, topo: &Topology) -> Result<(), std::io::Error> {
382 let smt_siblings = topo.sibling_cpus();
383
384 info!("SMT sibling CPUs: {:?}", smt_siblings);
385 for (cpu, sibling_cpu) in smt_siblings.iter().enumerate() {
386 Self::enable_sibling_cpu(skel, cpu, *sibling_cpu as usize).unwrap();
387 }
388
389 Ok(())
390 }
391
392 fn get_metrics(&mut self) -> Metrics {
393 let bss_data = self.skel.maps.bss_data.as_ref().unwrap();
394 Metrics {
395 nr_local_dispatch: bss_data.nr_local_dispatch,
396 nr_remote_dispatch: bss_data.nr_remote_dispatch,
397 nr_keep_running: bss_data.nr_keep_running,
398 }
399 }
400
401 pub fn exited(&mut self) -> bool {
402 uei_exited!(&self.skel, uei)
403 }
404
405 fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
406 let (res_ch, req_ch) = self.stats_server.channels();
407
408 while !shutdown.load(Ordering::Relaxed) && !self.exited() {
409 match req_ch.recv_timeout(Duration::from_secs(1)) {
411 Ok(()) => res_ch.send(self.get_metrics())?,
412 Err(RecvTimeoutError::Timeout) => {}
413 Err(e) => Err(e)?,
414 }
415 }
416
417 let _ = self.struct_ops.take();
418 uei_report!(&self.skel, uei)
419 }
420}
421
422impl Drop for Scheduler<'_> {
423 fn drop(&mut self) {
424 info!("Unregister {SCHEDULER_NAME} scheduler");
425 }
426}
427
428fn main() -> Result<()> {
429 let opts = Opts::parse();
430
431 if opts.version {
432 println!(
433 "{} {}",
434 SCHEDULER_NAME,
435 build_id::full_version(env!("CARGO_PKG_VERSION"))
436 );
437 return Ok(());
438 }
439
440 if opts.help_stats {
441 stats::server_data().describe_meta(&mut std::io::stdout(), None)?;
442 return Ok(());
443 }
444
445 let loglevel = simplelog::LevelFilter::Info;
446
447 let mut lcfg = simplelog::ConfigBuilder::new();
448 lcfg.set_time_offset_to_local()
449 .expect("Failed to set local time offset")
450 .set_time_level(simplelog::LevelFilter::Error)
451 .set_location_level(simplelog::LevelFilter::Off)
452 .set_target_level(simplelog::LevelFilter::Off)
453 .set_thread_level(simplelog::LevelFilter::Off);
454 simplelog::TermLogger::init(
455 loglevel,
456 lcfg.build(),
457 simplelog::TerminalMode::Stderr,
458 simplelog::ColorChoice::Auto,
459 )?;
460
461 let shutdown = Arc::new(AtomicBool::new(false));
462 let shutdown_clone = shutdown.clone();
463 ctrlc::set_handler(move || {
464 shutdown_clone.store(true, Ordering::Relaxed);
465 })
466 .context("Error setting Ctrl-C handler")?;
467
468 if let Some(intv) = opts.monitor.or(opts.stats) {
469 let shutdown_copy = shutdown.clone();
470 let jh = std::thread::spawn(move || {
471 match stats::monitor(Duration::from_secs_f64(intv), shutdown_copy) {
472 Ok(_) => {
473 debug!("stats monitor thread finished successfully")
474 }
475 Err(error_object) => {
476 warn!(
477 "stats monitor thread finished because of an error {}",
478 error_object
479 )
480 }
481 }
482 });
483 if opts.monitor.is_some() {
484 let _ = jh.join();
485 return Ok(());
486 }
487 }
488
489 let mut open_object = MaybeUninit::uninit();
490 loop {
491 let mut sched = Scheduler::init(&opts, &mut open_object)?;
492 if !sched.run(shutdown.clone())?.should_restart() {
493 break;
494 }
495 }
496
497 Ok(())
498}