1mod bpf_skel;
9pub use bpf_skel::*;
10pub mod bpf_intf;
11pub use bpf_intf::*;
12
13mod stats;
14use std::ffi::{c_int, c_ulong};
15use std::fmt::Write;
16use std::mem::MaybeUninit;
17use std::sync::Arc;
18use std::sync::atomic::AtomicBool;
19use std::sync::atomic::Ordering;
20use std::time::Duration;
21
22use anyhow::Context;
23use anyhow::Result;
24use anyhow::anyhow;
25use anyhow::bail;
26use clap::Parser;
27use crossbeam::channel::RecvTimeoutError;
28use libbpf_rs::OpenObject;
29use libbpf_rs::ProgramInput;
30use log::warn;
31use log::{debug, info};
32use scx_stats::prelude::*;
33use scx_utils::Cpumask;
34use scx_utils::NR_CPU_IDS;
35use scx_utils::Powermode;
36use scx_utils::Topology;
37use scx_utils::UserExitInfo;
38use scx_utils::autopower::{PowerProfile, fetch_power_profile};
39use scx_utils::build_id;
40use scx_utils::compat;
41use scx_utils::get_primary_cpus;
42use scx_utils::libbpf_clap_opts::LibbpfOpts;
43use scx_utils::pm::{cpu_idle_resume_latency_supported, update_cpu_idle_resume_latency};
44use scx_utils::scx_ops_attach;
45use scx_utils::scx_ops_load;
46use scx_utils::scx_ops_open;
47use scx_utils::try_set_rlimit_infinity;
48use scx_utils::uei_exited;
49use scx_utils::uei_report;
50use stats::Metrics;
51
52const SCHEDULER_NAME: &str = "scx_bpfland";
53
54fn cpus_to_cpumask(cpus: &Vec<usize>) -> String {
56 if cpus.is_empty() {
57 return String::from("none");
58 }
59
60 let max_cpu_id = *cpus.iter().max().unwrap();
62
63 let mut bitmask = vec![0u8; (max_cpu_id + 1).div_ceil(8)];
65
66 for cpu_id in cpus {
68 let byte_index = cpu_id / 8;
69 let bit_index = cpu_id % 8;
70 bitmask[byte_index] |= 1 << bit_index;
71 }
72
73 let hex_str: String = bitmask.iter().rev().fold(String::new(), |mut f, byte| {
75 let _ = write!(&mut f, "{:02x}", byte);
76 f
77 });
78
79 format!("0x{}", hex_str)
80}
81
82#[derive(Debug, Parser)]
90struct Opts {
91 #[clap(long, default_value = "0")]
93 exit_dump_len: u32,
94
95 #[clap(short = 's', long, default_value = "1000")]
97 slice_us: u64,
98
99 #[clap(short = 'L', long, default_value = "0")]
101 slice_min_us: u64,
102
103 #[clap(short = 'l', long, default_value = "40000")]
108 slice_us_lag: u64,
109
110 #[clap(short = 't', long, default_value = "0")]
115 throttle_us: u64,
116
117 #[clap(short = 'I', long, allow_hyphen_values = true, default_value = "-1")]
123 idle_resume_us: i64,
124
125 #[clap(short = 'p', long, action = clap::ArgAction::SetTrue)]
132 local_pcpu: bool,
133
134 #[clap(short = 'k', long, action = clap::ArgAction::SetTrue)]
143 local_kthreads: bool,
144
145 #[clap(short = 'w', long, action = clap::ArgAction::SetTrue)]
152 no_wake_sync: bool,
153
154 #[clap(short = 'S', long, action = clap::ArgAction::SetTrue)]
162 sticky_tasks: bool,
163
164 #[clap(short = 'm', long, default_value = "auto")]
175 primary_domain: String,
176
177 #[clap(short = 'P', long, action = clap::ArgAction::SetTrue)]
182 preferred_idle_scan: bool,
183
184 #[clap(long, action = clap::ArgAction::SetTrue)]
186 disable_smt: bool,
187
188 #[clap(long, action = clap::ArgAction::SetTrue)]
190 disable_numa: bool,
191
192 #[clap(short = 'f', long, action = clap::ArgAction::SetTrue)]
196 cpufreq: bool,
197
198 #[clap(long)]
200 stats: Option<f64>,
201
202 #[clap(long)]
205 monitor: Option<f64>,
206
207 #[clap(short = 'd', long, action = clap::ArgAction::SetTrue)]
209 debug: bool,
210
211 #[clap(short = 'v', long, action = clap::ArgAction::SetTrue)]
213 verbose: bool,
214
215 #[clap(short = 'V', long, action = clap::ArgAction::SetTrue)]
217 version: bool,
218
219 #[clap(long)]
221 help_stats: bool,
222
223 #[clap(flatten, next_help_heading = "Libbpf Options")]
224 pub libbpf: LibbpfOpts,
225}
226
227struct Scheduler<'a> {
228 skel: BpfSkel<'a>,
229 struct_ops: Option<libbpf_rs::Link>,
230 opts: &'a Opts,
231 topo: Topology,
232 power_profile: PowerProfile,
233 stats_server: StatsServer<(), Metrics>,
234 user_restart: bool,
235}
236
237impl<'a> Scheduler<'a> {
238 fn init(opts: &'a Opts, open_object: &'a mut MaybeUninit<OpenObject>) -> Result<Self> {
239 try_set_rlimit_infinity();
240
241 let topo = Topology::new().unwrap();
243
244 let smt_enabled = !opts.disable_smt && topo.smt_enabled;
246
247 let nr_nodes = topo
249 .nodes
250 .values()
251 .filter(|node| !node.all_cpus.is_empty())
252 .count();
253 info!("NUMA nodes: {}", nr_nodes);
254
255 let numa_enabled = !opts.disable_numa && nr_nodes > 1;
257 if !numa_enabled {
258 info!("Disabling NUMA optimizations");
259 }
260
261 let power_profile = Self::power_profile();
263 let domain =
264 Self::resolve_energy_domain(&opts.primary_domain, power_profile).map_err(|err| {
265 anyhow!(
266 "failed to resolve primary domain '{}': {}",
267 opts.primary_domain,
268 err
269 )
270 })?;
271
272 info!(
273 "{} {} {}",
274 SCHEDULER_NAME,
275 build_id::full_version(env!("CARGO_PKG_VERSION")),
276 if smt_enabled { "SMT on" } else { "SMT off" }
277 );
278
279 info!(
281 "scheduler options: {}",
282 std::env::args().collect::<Vec<_>>().join(" ")
283 );
284
285 if opts.idle_resume_us >= 0 {
286 if !cpu_idle_resume_latency_supported() {
287 warn!("idle resume latency not supported");
288 } else {
289 info!("Setting idle QoS to {} us", opts.idle_resume_us);
290 for cpu in topo.all_cpus.values() {
291 update_cpu_idle_resume_latency(
292 cpu.id,
293 opts.idle_resume_us.try_into().unwrap(),
294 )?;
295 }
296 }
297 }
298
299 let mut skel_builder = BpfSkelBuilder::default();
301 skel_builder.obj_builder.debug(opts.verbose);
302 let open_opts = opts.libbpf.clone().into_bpf_open_opts();
303 let mut skel = scx_ops_open!(skel_builder, open_object, bpfland_ops, open_opts)?;
304
305 skel.struct_ops.bpfland_ops_mut().exit_dump_len = opts.exit_dump_len;
306
307 let rodata = skel.maps.rodata_data.as_mut().unwrap();
309 rodata.debug = opts.debug;
310 rodata.smt_enabled = smt_enabled;
311 rodata.numa_enabled = numa_enabled;
312 rodata.local_pcpu = opts.local_pcpu;
313 rodata.no_wake_sync = opts.no_wake_sync;
314 rodata.sticky_tasks = opts.sticky_tasks;
315 rodata.slice_max = opts.slice_us * 1000;
316 rodata.slice_min = opts.slice_min_us * 1000;
317 rodata.slice_lag = opts.slice_us_lag * 1000;
318 rodata.throttle_ns = opts.throttle_us * 1000;
319 rodata.primary_all = domain.weight() == *NR_CPU_IDS;
320
321 let mut cpus: Vec<_> = topo.all_cpus.values().collect();
323 cpus.sort_by_key(|cpu| std::cmp::Reverse(cpu.cpu_capacity));
324 for (i, cpu) in cpus.iter().enumerate() {
325 rodata.cpu_capacity[cpu.id] = cpu.cpu_capacity as c_ulong;
326 rodata.preferred_cpus[i] = cpu.id as u64;
327 }
328 if opts.preferred_idle_scan {
329 info!(
330 "Preferred CPUs: {:?}",
331 &rodata.preferred_cpus[0..cpus.len()]
332 );
333 }
334 rodata.preferred_idle_scan = opts.preferred_idle_scan;
335
336 rodata.local_kthreads = opts.local_kthreads || opts.throttle_us > 0;
339
340 skel.struct_ops.bpfland_ops_mut().flags = *compat::SCX_OPS_ENQ_EXITING
342 | *compat::SCX_OPS_ENQ_LAST
343 | *compat::SCX_OPS_ENQ_MIGRATION_DISABLED
344 | *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP
345 | if numa_enabled {
346 *compat::SCX_OPS_BUILTIN_IDLE_PER_NODE
347 } else {
348 0
349 };
350 info!(
351 "scheduler flags: {:#x}",
352 skel.struct_ops.bpfland_ops_mut().flags
353 );
354
355 let mut skel = scx_ops_load!(skel, bpfland_ops, uei)?;
357
358 Self::init_energy_domain(&mut skel, &domain).map_err(|err| {
360 anyhow!(
361 "failed to initialize primary domain 0x{:x}: {}",
362 domain,
363 err
364 )
365 })?;
366
367 if let Err(err) = Self::init_cpufreq_perf(&mut skel, &opts.primary_domain, opts.cpufreq) {
369 bail!(
370 "failed to initialize cpufreq performance level: error {}",
371 err
372 );
373 }
374
375 if smt_enabled {
377 Self::init_smt_domains(&mut skel, &topo)?;
378 }
379
380 let struct_ops = Some(scx_ops_attach!(skel, bpfland_ops)?);
382 let stats_server = StatsServer::new(stats::server_data()).launch()?;
383
384 Ok(Self {
385 skel,
386 struct_ops,
387 opts,
388 topo,
389 power_profile,
390 stats_server,
391 user_restart: false,
392 })
393 }
394
395 fn enable_primary_cpu(skel: &mut BpfSkel<'_>, cpu: i32) -> Result<(), u32> {
396 let prog = &mut skel.progs.enable_primary_cpu;
397 let mut args = cpu_arg {
398 cpu_id: cpu as c_int,
399 };
400 let input = ProgramInput {
401 context_in: Some(unsafe {
402 std::slice::from_raw_parts_mut(
403 &mut args as *mut _ as *mut u8,
404 std::mem::size_of_val(&args),
405 )
406 }),
407 ..Default::default()
408 };
409 let out = prog.test_run(input).unwrap();
410 if out.return_value != 0 {
411 return Err(out.return_value);
412 }
413
414 Ok(())
415 }
416
417 fn epp_to_cpumask(profile: Powermode) -> Result<Cpumask> {
418 let mut cpus = get_primary_cpus(profile).unwrap_or_default();
419 if cpus.is_empty() {
420 cpus = get_primary_cpus(Powermode::Any).unwrap_or_default();
421 }
422 Cpumask::from_str(&cpus_to_cpumask(&cpus))
423 }
424
425 fn resolve_energy_domain(primary_domain: &str, power_profile: PowerProfile) -> Result<Cpumask> {
426 let domain = match primary_domain {
427 "powersave" => Self::epp_to_cpumask(Powermode::Powersave)?,
428 "performance" => Self::epp_to_cpumask(Powermode::Performance)?,
429 "turbo" => Self::epp_to_cpumask(Powermode::Turbo)?,
430 "auto" => match power_profile {
431 PowerProfile::Powersave => Self::epp_to_cpumask(Powermode::Powersave)?,
432 PowerProfile::Balanced { .. }
433 | PowerProfile::Performance
434 | PowerProfile::Unknown => Self::epp_to_cpumask(Powermode::Any)?,
435 },
436 "all" => Self::epp_to_cpumask(Powermode::Any)?,
437 &_ => Cpumask::from_str(primary_domain)?,
438 };
439
440 Ok(domain)
441 }
442
443 fn init_energy_domain(skel: &mut BpfSkel<'_>, domain: &Cpumask) -> Result<()> {
444 info!("primary CPU domain = 0x{:x}", domain);
445
446 if let Err(err) = Self::enable_primary_cpu(skel, -1) {
448 bail!("failed to reset primary domain: error {}", err);
449 }
450
451 for cpu in 0..*NR_CPU_IDS {
453 if domain.test_cpu(cpu)
454 && let Err(err) = Self::enable_primary_cpu(skel, cpu as i32)
455 {
456 bail!("failed to add CPU {} to primary domain: error {}", cpu, err);
457 }
458 }
459
460 Ok(())
461 }
462
463 fn init_cpufreq_perf(
465 skel: &mut BpfSkel<'_>,
466 primary_domain: &String,
467 auto: bool,
468 ) -> Result<()> {
469 let perf_lvl: i64 = match primary_domain.as_str() {
472 "powersave" => 0,
473 _ if auto => -1,
474 _ => 1024,
475 };
476 info!(
477 "cpufreq performance level: {}",
478 match perf_lvl {
479 1024 => "max".into(),
480 0 => "min".into(),
481 n if n < 0 => "auto".into(),
482 _ => perf_lvl.to_string(),
483 }
484 );
485 skel.maps.bss_data.as_mut().unwrap().cpufreq_perf_lvl = perf_lvl;
486
487 Ok(())
488 }
489
490 fn power_profile() -> PowerProfile {
491 let profile = fetch_power_profile(true);
492 if profile == PowerProfile::Unknown {
493 fetch_power_profile(false)
494 } else {
495 profile
496 }
497 }
498
499 fn refresh_sched_domain(&mut self) -> bool {
500 if self.power_profile != PowerProfile::Unknown {
501 let power_profile = Self::power_profile();
502 if power_profile != self.power_profile {
503 self.power_profile = power_profile;
504
505 if self.opts.primary_domain == "auto" {
506 return true;
507 }
508 if let Err(err) = Self::init_cpufreq_perf(
509 &mut self.skel,
510 &self.opts.primary_domain,
511 self.opts.cpufreq,
512 ) {
513 warn!("failed to refresh cpufreq performance level: error {}", err);
514 }
515 }
516 }
517
518 false
519 }
520
521 fn enable_sibling_cpu(
522 skel: &mut BpfSkel<'_>,
523 cpu: usize,
524 sibling_cpu: usize,
525 ) -> Result<(), u32> {
526 let prog = &mut skel.progs.enable_sibling_cpu;
527 let mut args = domain_arg {
528 cpu_id: cpu as c_int,
529 sibling_cpu_id: sibling_cpu as c_int,
530 };
531 let input = ProgramInput {
532 context_in: Some(unsafe {
533 std::slice::from_raw_parts_mut(
534 &mut args as *mut _ as *mut u8,
535 std::mem::size_of_val(&args),
536 )
537 }),
538 ..Default::default()
539 };
540 let out = prog.test_run(input).unwrap();
541 if out.return_value != 0 {
542 return Err(out.return_value);
543 }
544
545 Ok(())
546 }
547
548 fn init_smt_domains(skel: &mut BpfSkel<'_>, topo: &Topology) -> Result<(), std::io::Error> {
549 let smt_siblings = topo.sibling_cpus();
550
551 info!("SMT sibling CPUs: {:?}", smt_siblings);
552 for (cpu, sibling_cpu) in smt_siblings.iter().enumerate() {
553 Self::enable_sibling_cpu(skel, cpu, *sibling_cpu as usize).unwrap();
554 }
555
556 Ok(())
557 }
558
559 fn get_metrics(&self) -> Metrics {
560 let bss_data = self.skel.maps.bss_data.as_ref().unwrap();
561 Metrics {
562 nr_running: bss_data.nr_running,
563 nr_cpus: bss_data.nr_online_cpus,
564 nr_kthread_dispatches: bss_data.nr_kthread_dispatches,
565 nr_direct_dispatches: bss_data.nr_direct_dispatches,
566 nr_shared_dispatches: bss_data.nr_shared_dispatches,
567 nr_idle_select_path_picks: bss_data.nr_idle_select_path_picks,
568 nr_idle_enqueue_path_picks: bss_data.nr_idle_enqueue_path_picks,
569 nr_idle_prev_cpu_picks: bss_data.nr_idle_prev_cpu_picks,
570 nr_idle_primary_picks: bss_data.nr_idle_primary_picks,
571 nr_idle_spill_picks: bss_data.nr_idle_spill_picks,
572 nr_idle_pick_failures: bss_data.nr_idle_pick_failures,
573 nr_idle_primary_domain_misses: bss_data.nr_idle_primary_domain_misses,
574 nr_idle_global_misses: bss_data.nr_idle_global_misses,
575 nr_waker_cpu_biases: bss_data.nr_waker_cpu_biases,
576 nr_keep_running_reuses: bss_data.nr_keep_running_reuses,
577 nr_keep_running_queue_empty: bss_data.nr_keep_running_queue_empty,
578 nr_keep_running_smt_blocked: bss_data.nr_keep_running_smt_blocked,
579 nr_keep_running_queued_work: bss_data.nr_keep_running_queued_work,
580 nr_dispatch_cpu_dsq_consumes: bss_data.nr_dispatch_cpu_dsq_consumes,
581 nr_dispatch_node_dsq_consumes: bss_data.nr_dispatch_node_dsq_consumes,
582 }
583 }
584
585 pub fn exited(&mut self) -> bool {
586 uei_exited!(&self.skel, uei)
587 }
588
589 fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
590 let (res_ch, req_ch) = self.stats_server.channels();
591 while !shutdown.load(Ordering::Relaxed) && !self.exited() {
592 if self.refresh_sched_domain() {
593 self.user_restart = true;
594 break;
595 }
596 match req_ch.recv_timeout(Duration::from_secs(1)) {
597 Ok(()) => res_ch.send(self.get_metrics())?,
598 Err(RecvTimeoutError::Timeout) => {}
599 Err(e) => Err(e)?,
600 }
601 }
602
603 let _ = self.struct_ops.take();
604 uei_report!(&self.skel, uei)
605 }
606}
607
608impl Drop for Scheduler<'_> {
609 fn drop(&mut self) {
610 info!("Unregister {SCHEDULER_NAME} scheduler");
611
612 if self.opts.idle_resume_us >= 0 && cpu_idle_resume_latency_supported() {
614 for cpu in self.topo.all_cpus.values() {
615 update_cpu_idle_resume_latency(cpu.id, cpu.pm_qos_resume_latency_us as i32)
616 .unwrap();
617 }
618 }
619 }
620}
621
622fn main() -> Result<()> {
623 let opts = Opts::parse();
624
625 if opts.version {
626 println!(
627 "{} {}",
628 SCHEDULER_NAME,
629 build_id::full_version(env!("CARGO_PKG_VERSION"))
630 );
631 return Ok(());
632 }
633
634 if opts.help_stats {
635 stats::server_data().describe_meta(&mut std::io::stdout(), None)?;
636 return Ok(());
637 }
638
639 let loglevel = simplelog::LevelFilter::Info;
640
641 let mut lcfg = simplelog::ConfigBuilder::new();
642 lcfg.set_time_offset_to_local()
643 .expect("Failed to set local time offset")
644 .set_time_level(simplelog::LevelFilter::Error)
645 .set_location_level(simplelog::LevelFilter::Off)
646 .set_target_level(simplelog::LevelFilter::Off)
647 .set_thread_level(simplelog::LevelFilter::Off);
648 simplelog::TermLogger::init(
649 loglevel,
650 lcfg.build(),
651 simplelog::TerminalMode::Stderr,
652 simplelog::ColorChoice::Auto,
653 )?;
654
655 let shutdown = Arc::new(AtomicBool::new(false));
656 let shutdown_clone = shutdown.clone();
657 ctrlc::set_handler(move || {
658 shutdown_clone.store(true, Ordering::Relaxed);
659 })
660 .context("Error setting Ctrl-C handler")?;
661
662 if let Some(intv) = opts.monitor.or(opts.stats) {
663 let shutdown_copy = shutdown.clone();
664 let jh = std::thread::spawn(move || {
665 match stats::monitor(Duration::from_secs_f64(intv), shutdown_copy) {
666 Ok(_) => {
667 debug!("stats monitor thread finished successfully")
668 }
669 Err(error_object) => {
670 warn!(
671 "stats monitor thread finished because of an error {}",
672 error_object
673 )
674 }
675 }
676 });
677 if opts.monitor.is_some() {
678 let _ = jh.join();
679 return Ok(());
680 }
681 }
682
683 let mut open_object = MaybeUninit::uninit();
684 loop {
685 let mut sched = Scheduler::init(&opts, &mut open_object)?;
686 if !sched.run(shutdown.clone())?.should_restart() {
687 if sched.user_restart {
688 continue;
689 }
690 break;
691 }
692 }
693
694 Ok(())
695}