1mod bpf_skel;
9pub use bpf_skel::*;
10pub mod bpf_intf;
11pub use bpf_intf::*;
12
13mod stats;
14use std::collections::BTreeMap;
15use std::mem::MaybeUninit;
16use std::sync::Arc;
17use std::sync::atomic::AtomicBool;
18use std::sync::atomic::Ordering;
19use std::time::Duration;
20
21use anyhow::Context;
22use anyhow::Result;
23use anyhow::bail;
24use clap::Parser;
25use crossbeam::channel::RecvTimeoutError;
26use libbpf_rs::MapCore;
27use libbpf_rs::OpenObject;
28use libbpf_rs::ProgramInput;
29use libbpf_rs::skel::Skel;
30use log::{debug, info, warn};
31use scx_arena::ArenaLib;
32use scx_stats::prelude::*;
33use scx_utils::CoreType;
34use scx_utils::Cpumask;
35use scx_utils::NR_CPU_IDS;
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::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;
48use stats::Metrics;
49
50const SCHEDULER_NAME: &str = "scx_bpfland";
51
52const MAX_CPUS: usize = 1024;
54
55const QUEUED_BITS: usize = 2048 * 64;
57
58const LINE_BITS: usize = 512;
60
61const MAX_TIERS: u32 = 8;
63
64const CAPACITY_TOLERANCE_PCT: usize = 5;
67
68#[derive(Debug, Parser)]
78struct Opts {
79 #[clap(long, default_value = "0")]
81 exit_dump_len: u32,
82
83 #[clap(short = 's', long, default_value = "1600")]
85 slice_us: u64,
86
87 #[clap(short = 'm', long, value_name = "DOMAIN")]
102 primary_domain: Option<String>,
103
104 #[clap(long)]
107 no_asym_packing: bool,
108
109 #[clap(long)]
112 no_preempt: bool,
113
114 #[clap(long)]
117 no_eligibility: bool,
118
119 #[clap(long)]
122 no_vlag: bool,
123
124 #[clap(short = 'l', long, default_value = "10000")]
127 lag_bound_us: u64,
128
129 #[clap(short = 'g', long, default_value = "100")]
131 min_granularity_us: u64,
132
133 #[clap(long)]
137 no_vtime_borrowing: bool,
138
139 #[clap(long, default_value = "20000")]
141 vtime_borrowing_us: u64,
142
143 #[clap(long, default_value = "0")]
154 rotate_us: u64,
155
156 #[clap(long)]
158 stats: Option<f64>,
159
160 #[clap(long)]
162 monitor: Option<f64>,
163
164 #[clap(short = 'v', long, action = clap::ArgAction::SetTrue)]
166 verbose: bool,
167
168 #[clap(short = 'V', long, action = clap::ArgAction::SetTrue)]
170 version: bool,
171
172 #[clap(long)]
174 help_stats: bool,
175
176 #[clap(flatten, next_help_heading = "Libbpf Options")]
177 pub libbpf: LibbpfOpts,
178}
179
180#[derive(Clone, Copy, Debug, PartialEq)]
182enum Preference {
183 Performance,
184 Powersave,
185 Turbo,
186 Cores,
187}
188
189fn preference(spec: &str, profile: PowerProfile) -> Option<(Preference, bool)> {
192 match spec {
193 "performance" => Some((Preference::Performance, false)),
194 "powersave" => Some((Preference::Powersave, false)),
195 "turbo" => Some((Preference::Turbo, false)),
196 "cores" => Some((Preference::Cores, false)),
197 "auto" => Some((
198 if profile == PowerProfile::Powersave {
199 Preference::Powersave
200 } else {
201 Preference::Performance
202 },
203 true,
204 )),
205 _ => None,
206 }
207}
208
209fn power_profile() -> PowerProfile {
211 let profile = fetch_power_profile(true);
212 if profile == PowerProfile::Unknown {
213 fetch_power_profile(false)
214 } else {
215 profile
216 }
217}
218
219fn primary_domain(spec: &str, topo: &Topology) -> Result<Cpumask> {
220 let mut mask = Cpumask::new();
221 if spec == "all" || preference(spec, PowerProfile::Unknown).is_some() {
222 for &cpu in topo.all_cpus.keys() {
223 mask.set_cpu(cpu)?;
224 }
225 } else {
226 for part in spec.split(',') {
227 let part = part.trim();
228 let (first, last) = match part.split_once('-') {
229 Some((first, last)) => (first.trim().parse::<usize>()?, last.trim().parse()?),
230 None => {
231 let cpu = part.parse::<usize>()?;
232 (cpu, cpu)
233 }
234 };
235 if first > last || last >= MAX_CPUS {
236 bail!("invalid primary CPU range: {part}");
237 }
238 for cpu in first..=last {
239 if !topo.all_cpus.contains_key(&cpu) {
240 bail!("primary CPU {cpu} is not online");
241 }
242 mask.set_cpu(cpu)?;
243 }
244 }
245 }
246 if mask.iter().next().is_none() {
247 bail!("the primary domain must contain an online CPU");
248 }
249 Ok(mask)
250}
251
252struct Domains {
255 of_cpu: Vec<i32>,
256 base: Vec<u32>,
257 len: Vec<u32>,
258 cpus: Vec<u16>,
259}
260
261impl Domains {
262 fn new(topo: &Topology, key: impl Fn(&scx_utils::Cpu) -> usize) -> Self {
264 let mut groups: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
265 for cpu in topo.all_cpus.values() {
266 groups.entry(key(cpu)).or_default().push(cpu.id);
267 }
268 let mut domains = Domains {
269 of_cpu: vec![0; MAX_CPUS],
270 base: vec![0; MAX_CPUS],
271 len: vec![0; MAX_CPUS],
272 cpus: Vec::with_capacity(MAX_CPUS),
273 };
274 for (dom, cpus) in groups.values().enumerate() {
275 domains.base[dom] = domains.cpus.len() as u32;
276 domains.len[dom] = cpus.len() as u32;
277 for &cpu in cpus {
278 domains.of_cpu[cpu] = dom as i32;
279 domains.cpus.push(cpu as u16);
280 }
281 }
282 domains.cpus.resize(MAX_CPUS, 0);
283 domains
284 }
285
286 fn queued_bits(&self) -> Vec<u32> {
289 let mut bits: Vec<u32> = (0..MAX_CPUS as u32).collect();
290 let lens: Vec<usize> = self
291 .len
292 .iter()
293 .take_while(|&&len| len > 0)
294 .map(|&len| len as usize)
295 .collect();
296 let lines: usize = lens.iter().map(|len| len.div_ceil(LINE_BITS)).sum();
297 if lines * LINE_BITS > QUEUED_BITS {
298 return bits;
299 }
300 let mut line = 0;
301 for (dom, &len) in lens.iter().enumerate() {
302 let base = self.base[dom] as usize;
303 for pos in 0..len {
304 bits[self.cpus[base + pos] as usize] = (line * LINE_BITS + pos) as u32;
305 }
306 line += len.div_ceil(LINE_BITS);
307 }
308 bits
309 }
310}
311
312struct Scheduler<'a> {
313 skel: BpfSkel<'a>,
314 struct_ops: Option<libbpf_rs::Link>,
315 stats_server: StatsServer<(), Metrics>,
316 _arenalib: ArenaLib,
317 auto_profile: Option<PowerProfile>,
319 user_restart: bool,
320}
321
322impl<'a> Scheduler<'a> {
323 fn init(opts: &'a Opts, open_object: &'a mut MaybeUninit<OpenObject>) -> Result<Self> {
324 try_set_rlimit_infinity();
325
326 let topo = Topology::new().context("reading the CPU topology")?;
327 let nr_cpus = *NR_CPU_IDS;
328 if nr_cpus > MAX_CPUS {
329 bail!("{} CPUs, at most {} are supported", nr_cpus, MAX_CPUS);
330 }
331
332 let spec = opts.primary_domain.as_deref().unwrap_or("all");
333 let primary = primary_domain(spec, &topo).context("parsing the primary domain")?;
334 let profile = if spec == "auto" {
335 power_profile()
336 } else {
337 PowerProfile::Unknown
338 };
339 let (pref, auto) = preference(spec, profile).unwrap_or((Preference::Performance, false));
340
341 info!(
342 "{} {} {}",
343 SCHEDULER_NAME,
344 build_id::full_version(env!("CARGO_PKG_VERSION")),
345 if topo.smt_enabled {
346 "SMT on"
347 } else {
348 "SMT off"
349 }
350 );
351 info!(
352 "scheduler options: {}",
353 std::env::args().collect::<Vec<_>>().join(" ")
354 );
355
356 let mut skel_builder = BpfSkelBuilder::default();
357 skel_builder.obj_builder.debug(opts.verbose);
358 let open_opts = opts.libbpf.clone().into_bpf_open_opts();
359 let mut skel = scx_ops_open!(skel_builder, open_object, bpfland_ops, open_opts)?;
360
361 skel.struct_ops.bpfland_ops_mut().exit_dump_len = opts.exit_dump_len;
362
363 let rodata = skel.maps.rodata_data.as_mut().unwrap();
364 rodata.slice_ns = opts.slice_us * 1000;
365 rodata.lag_bound_ns = opts.lag_bound_us * 1000;
366 rodata.min_granularity_ns = opts.min_granularity_us * 1000;
367 rodata.vtime_borrowing_ns = opts.vtime_borrowing_us * 1000;
368 rodata.rotate_ns = opts.rotate_us * 1000;
369 rodata.no_asym_packing = opts.no_asym_packing;
370 rodata.no_preempt = opts.no_preempt;
371 rodata.no_eligibility = opts.no_eligibility;
372 rodata.no_vlag = opts.no_vlag;
373 rodata.no_vtime_borrowing = opts.no_vtime_borrowing;
374 rodata.stats_enabled = opts.stats.is_some();
375 rodata.smt_enabled = topo.smt_enabled;
376 rodata.nr_cpus = nr_cpus as u32;
377 rodata.primary_all = primary.iter().count() == topo.all_cpus.len();
378 for cpu in primary.iter() {
379 rodata.primary_cpus[cpu] = true;
380 }
381 if !rodata.primary_all {
382 info!("Primary CPUs: {} (no spillover)", primary.to_cpulist());
383 }
384
385 let llcs = Domains::new(&topo, |cpu| cpu.llc_id);
387 let nodes = Domains::new(&topo, |cpu| cpu.node_id);
388 let cores = Domains::new(&topo, |cpu| cpu.core_id);
389 let queued_bits = llcs.queued_bits();
390 for i in 0..MAX_CPUS {
391 rodata.cpu_llc[i] = llcs.of_cpu[i];
392 rodata.cpu_node[i] = nodes.of_cpu[i];
393 rodata.cpu_core[i] = cores.of_cpu[i];
394 rodata.llc_base[i] = llcs.base[i];
395 rodata.llc_len[i] = llcs.len[i];
396 rodata.node_base[i] = nodes.base[i];
397 rodata.node_len[i] = nodes.len[i];
398 rodata.core_base[i] = cores.base[i];
399 rodata.core_len[i] = cores.len[i];
400 rodata.llc_cpus[i] = llcs.cpus[i];
401 rodata.node_cpus[i] = nodes.cpus[i];
402 rodata.core_cpus[i] = cores.cpus[i];
403 rodata.cpu_queued_bit[i] = queued_bits[i];
404 }
405
406 skel.struct_ops.bpfland_ops_mut().flags = *compat::SCX_OPS_ENQ_LAST
407 | *compat::SCX_OPS_ENQ_MIGRATION_DISABLED
408 | *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP
409 | *compat::SCX_OPS_KEEP_BUILTIN_IDLE
410 | *compat::SCX_OPS_TID_TO_TASK;
411 skel.maps.rodata_data.as_mut().unwrap().tid_lookup = *compat::SCX_OPS_TID_TO_TASK != 0;
414 if *compat::SCX_OPS_TID_TO_TASK != 0 {
417 skel.struct_ops.bpfland_ops_mut().flags |= *compat::SCX_OPS_ENQ_EXITING;
418 }
419 info!(
420 "scheduler flags: {:#x}",
421 skel.struct_ops.bpfland_ops_mut().flags
422 );
423
424 let mut skel = scx_ops_load!(skel, bpfland_ops, uei)?;
425
426 Self::init_place_tiers(&mut skel, &topo, &llcs, pref)?;
427
428 let arenalib =
431 ArenaLib::start(skel.object_mut()).context("starting arena userspace services")?;
432
433 let struct_ops = Some(scx_ops_attach!(skel, bpfland_ops)?);
434 let stats_server = StatsServer::new(stats::server_data()).launch()?;
435
436 info!("{} attached", SCHEDULER_NAME);
437
438 Ok(Self {
439 skel,
440 struct_ops,
441 stats_server,
442 _arenalib: arenalib,
443 auto_profile: auto.then_some(profile),
444 user_restart: false,
445 })
446 }
447
448 fn init_place_tiers(
453 skel: &mut BpfSkel<'_>,
454 topo: &Topology,
455 llcs: &Domains,
456 pref: Preference,
457 ) -> Result<()> {
458 let (mut tiers, source) = match Self::packing_tiers(skel, topo)? {
459 Some(tiers) => (tiers, "packing priority"),
460 None => match Self::capacity_tiers(topo) {
461 Some(tiers) => (tiers, "capacity"),
462 None => (topo.all_cpus.keys().map(|&cpu| (cpu, 0)).collect(), ""),
463 },
464 };
465 let mut nr_tiers = tiers.iter().map(|&(_, tier)| tier + 1).max().unwrap_or(1);
466 match pref {
467 Preference::Performance => {}
468 Preference::Powersave => {
470 for (_, tier) in tiers.iter_mut() {
471 *tier = nr_tiers - 1 - *tier;
472 }
473 }
474 Preference::Turbo if nr_tiers < MAX_TIERS => {
476 let turbo = |cpu: usize| {
477 matches!(
478 topo.all_cpus.get(&cpu).map(|cpu| &cpu.core_type),
479 Some(CoreType::Big { turbo: true })
480 )
481 };
482 if tiers.iter().any(|&(cpu, _)| turbo(cpu)) {
483 for (cpu, tier) in tiers.iter_mut() {
484 if !turbo(*cpu) {
485 *tier += 1;
486 }
487 }
488 }
489 }
490 Preference::Turbo => {}
491 Preference::Cores => {
493 let first = |cpu: usize| {
494 topo.all_cores.values().any(|core| {
495 core.cpus.keys().find(|cpu| topo.all_cpus.contains_key(cpu)) == Some(&cpu)
496 })
497 };
498 for (cpu, tier) in tiers.iter_mut() {
499 *tier = (*tier * 2 + u32::from(!first(*cpu))).min(MAX_TIERS - 1);
500 }
501 }
502 }
503 let mut levels: Vec<u32> = tiers.iter().map(|&(_, tier)| tier).collect();
505 levels.sort_unstable();
506 levels.dedup();
507 for (_, tier) in tiers.iter_mut() {
508 *tier = levels.iter().position(|&level| level == *tier).unwrap_or(0) as u32;
509 }
510 nr_tiers = levels.len().max(1) as u32;
511
512 let bss = skel.maps.bss_data.as_mut().unwrap();
513 bss.cpus_ranked = nr_tiers > 1;
514 bss.nr_place_tiers = nr_tiers;
515 for llc in 0..MAX_CPUS {
516 bss.llc_place_tier[llc] = u32::MAX;
517 }
518 for &(cpu, tier) in &tiers {
519 bss.place_tier[cpu] = tier;
520 let llc = llcs.of_cpu[cpu] as usize;
521 bss.llc_place_tier[llc] = bss.llc_place_tier[llc].min(tier);
522 }
523 if nr_tiers > 1 {
524 info!(
525 "CPU ranking: by {}, {} tiers, {} order",
526 source,
527 nr_tiers,
528 format!("{pref:?}").to_lowercase()
529 );
530 } else {
531 info!("CPU ranking: off");
532 }
533
534 Ok(())
535 }
536
537 fn packing_tiers(skel: &mut BpfSkel<'_>, topo: &Topology) -> Result<Option<Vec<(usize, u32)>>> {
540 let mut priorities = Vec::new();
541 let mut all_pack = true;
542 for cpu in topo.all_cpus.values() {
543 let mut args = cpu_priority_args {
544 cpu: cpu.id as u64,
545 priority: 0,
546 asym_packing: 0,
547 };
548 let input = ProgramInput {
549 context_in: Some(unsafe {
550 std::slice::from_raw_parts_mut(
551 &mut args as *mut _ as *mut u8,
552 std::mem::size_of_val(&args),
553 )
554 }),
555 ..Default::default()
556 };
557 let out = skel
558 .progs
559 .bpfland_cpu_priority
560 .test_run(input)
561 .context("running bpfland_cpu_priority")?;
562 if out.return_value != 0 {
563 bail!("bpfland_cpu_priority failed: {}", out.return_value);
564 }
565 all_pack &= args.asym_packing != 0;
566 priorities.push((cpu.id, args.priority));
567 }
568 let distinct = priorities.windows(2).any(|pair| pair[0].1 != pair[1].1);
569 if !all_pack || !distinct {
570 return Ok(None);
571 }
572
573 let mut levels: Vec<i64> = priorities.iter().map(|&(_, prio)| prio).collect();
575 levels.sort_unstable_by(|a, b| b.cmp(a));
576 levels.dedup();
577 Ok(Some(
578 priorities
579 .iter()
580 .map(|&(cpu, prio)| {
581 let tier = levels.iter().position(|&level| level == prio).unwrap_or(0);
582 (cpu, (tier as u32).min(MAX_TIERS - 1))
583 })
584 .collect(),
585 ))
586 }
587
588 fn capacity_tiers(topo: &Topology) -> Option<Vec<(usize, u32)>> {
592 let mut capacities: Vec<(usize, usize)> = topo
593 .all_cpus
594 .values()
595 .map(|cpu| Some((cpu.id, cpu.kernel_cpu_capacity?)))
596 .collect::<Option<_>>()?;
597 capacities.sort_by_key(|&(cpu, capacity)| (std::cmp::Reverse(capacity), cpu));
598 let mut anchor = capacities.first()?.1;
599 let mut tier = 0;
600 let tiers: Vec<(usize, u32)> = capacities
601 .iter()
602 .map(|&(cpu, capacity)| {
603 if capacity * 100 < anchor * (100 - CAPACITY_TOLERANCE_PCT) {
604 tier = (tier + 1).min(MAX_TIERS - 1);
605 anchor = capacity;
606 }
607 (cpu, tier)
608 })
609 .collect();
610 (tier > 0).then_some(tiers)
611 }
612
613 fn get_metrics(&self) -> Metrics {
614 let mut metrics = Metrics::default();
615 let key = 0_u32.to_ne_bytes();
616 if let Ok(Some(values)) = self
617 .skel
618 .maps
619 .counters_stor
620 .lookup_percpu(&key, libbpf_rs::MapFlags::ANY)
621 {
622 for value in values {
623 if value.len() < std::mem::size_of::<counters>() {
624 continue;
625 }
626 let c: counters =
627 unsafe { std::ptr::read_unaligned(value.as_ptr() as *const counters) };
628 metrics.nr_direct += c.direct;
629 metrics.nr_borrowed += c.borrowed;
630 metrics.nr_moved += c.moved;
631 metrics.nr_missed += c.missed;
632 metrics.nr_preempts += c.preempts;
633 metrics.nr_steals += c.steals;
634 metrics.nr_balanced += c.balanced;
635 metrics.nr_rotated += c.rotated;
636 }
637 }
638 metrics
639 }
640
641 pub fn exited(&mut self) -> bool {
642 uei_exited!(&self.skel, uei)
643 }
644
645 fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
646 let (res_ch, req_ch) = self.stats_server.channels();
647 while !shutdown.load(Ordering::Relaxed) && !self.exited() {
648 if self
650 .auto_profile
651 .is_some_and(|profile| power_profile() != profile)
652 {
653 info!("power profile changed, restarting");
654 self.user_restart = true;
655 break;
656 }
657 match req_ch.recv_timeout(Duration::from_secs(1)) {
658 Ok(()) => res_ch.send(self.get_metrics())?,
659 Err(RecvTimeoutError::Timeout) => {}
660 Err(e) => Err(e)?,
661 }
662 }
663
664 let _ = self.struct_ops.take();
665 uei_report!(&self.skel, uei)
666 }
667}
668
669impl Drop for Scheduler<'_> {
670 fn drop(&mut self) {
671 info!("Unregister {SCHEDULER_NAME} scheduler");
672 }
673}
674
675fn main() -> Result<()> {
676 let opts = Opts::parse();
677
678 if opts.version {
679 println!(
680 "{} {}",
681 SCHEDULER_NAME,
682 build_id::full_version(env!("CARGO_PKG_VERSION"))
683 );
684 return Ok(());
685 }
686
687 if opts.help_stats {
688 stats::server_data().describe_meta(&mut std::io::stdout(), None)?;
689 return Ok(());
690 }
691
692 let release =
693 std::fs::read_to_string("/proc/sys/kernel/osrelease").context("reading kernel release")?;
694 let mut fields = release.trim().split(['.', '-']);
695 let major: u32 = fields
696 .next()
697 .context("missing kernel major version")?
698 .parse()?;
699 let minor: u32 = fields
700 .next()
701 .context("missing kernel minor version")?
702 .parse()?;
703 if (major, minor) < (6, 16) {
704 bail!(
705 "scx_bpfland requires Linux 6.16 or newer (running {})",
706 release.trim()
707 );
708 }
709
710 let loglevel = simplelog::LevelFilter::Info;
711
712 let mut lcfg = simplelog::ConfigBuilder::new();
713 lcfg.set_time_offset_to_local()
714 .expect("Failed to set local time offset")
715 .set_time_level(simplelog::LevelFilter::Error)
716 .set_location_level(simplelog::LevelFilter::Off)
717 .set_target_level(simplelog::LevelFilter::Off)
718 .set_thread_level(simplelog::LevelFilter::Off);
719 simplelog::TermLogger::init(
720 loglevel,
721 lcfg.build(),
722 simplelog::TerminalMode::Stderr,
723 simplelog::ColorChoice::Auto,
724 )?;
725
726 let shutdown = Arc::new(AtomicBool::new(false));
727 let shutdown_clone = shutdown.clone();
728 ctrlc::set_handler(move || {
729 shutdown_clone.store(true, Ordering::Relaxed);
730 })
731 .context("Error setting Ctrl-C handler")?;
732
733 if let Some(intv) = opts.monitor.or(opts.stats) {
734 let shutdown_copy = shutdown.clone();
735 let jh = std::thread::spawn(move || {
736 match stats::monitor(Duration::from_secs_f64(intv), shutdown_copy) {
737 Ok(_) => {
738 debug!("stats monitor thread finished successfully")
739 }
740 Err(error_object) => {
741 warn!(
742 "stats monitor thread finished because of an error {}",
743 error_object
744 )
745 }
746 }
747 });
748 if opts.monitor.is_some() {
749 let _ = jh.join();
750 return Ok(());
751 }
752 }
753
754 let mut open_object = MaybeUninit::uninit();
755 loop {
756 let mut sched = Scheduler::init(&opts, &mut open_object)?;
757 if !sched.run(shutdown.clone())?.should_restart() {
758 if sched.user_restart {
759 continue;
760 }
761 break;
762 }
763 }
764
765 Ok(())
766}