1#[allow(clippy::unwrap_used)]
7mod bpf_skel;
8pub use bpf_skel::*;
9pub mod bpf_intf;
10mod cell_manager;
11mod stats;
12mod topology;
13mod undefok_flags;
14
15use cell_manager::{CellManager, CpuAssignment, CpuRecipient};
16
17use std::cmp::max;
18use std::collections::{HashMap, HashSet};
19use std::fmt;
20use std::fmt::Display;
21use std::mem::MaybeUninit;
22use std::os::fd::AsFd;
23use std::sync::atomic::AtomicBool;
24use std::sync::atomic::AtomicU32;
25use std::sync::atomic::Ordering;
26use std::sync::Arc;
27use std::time::Duration;
28use std::time::Instant;
29
30use anyhow::anyhow;
31use anyhow::bail;
32use anyhow::Context;
33use anyhow::Result;
34use clap::Parser;
35use libbpf_rs::MapCore as _;
36use libbpf_rs::OpenObject;
37use libbpf_rs::ProgramInput;
38use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags, EpollTimeout};
39use nix::sys::eventfd::EventFd;
40use scx_stats::prelude::*;
41use scx_utils::build_id;
42use scx_utils::compat;
43use scx_utils::init_libbpf_logging;
44use scx_utils::libbpf_clap_opts::LibbpfOpts;
45use scx_utils::scx_enums;
46use scx_utils::scx_ops_attach;
47use scx_utils::scx_ops_load;
48use scx_utils::scx_ops_open;
49use scx_utils::uei_exited;
50use scx_utils::uei_report;
51use scx_utils::Cpumask;
52use scx_utils::Topology;
53use scx_utils::TopologyArgs;
54use scx_utils::UserExitInfo;
55use scx_utils::NR_CPUS_POSSIBLE;
56use tracing::{debug, info, trace, warn};
57use tracing_subscriber::filter::EnvFilter;
58
59use stats::CellMetrics;
60use stats::Metrics;
61use topology::MitosisTopology;
62
63const SCHEDULER_NAME: &str = "scx_mitosis";
64const MAX_CELLS: usize = bpf_intf::consts_MAX_CELLS as usize;
65const MAX_SUBCELLS_PER_CELL: usize = bpf_intf::consts_MAX_SUBCELLS_PER_CELL as usize;
66const NR_CSTATS: usize = bpf_intf::cell_stat_idx_NR_CSTATS as usize;
67const INOTIFY_TOKEN: u64 = 1;
69const STATS_TOKEN: u64 = 2;
71
72fn parse_ewma_factor(s: &str) -> Result<f64, String> {
73 let v: f64 = s.parse().map_err(|e| format!("{e}"))?;
74 if !(0.0..=1.0).contains(&v) {
75 return Err(format!("value {v} not in range 0.0..=1.0"));
76 }
77 Ok(v)
78}
79
80#[derive(Debug, Parser)]
88#[command(after_long_help = r#"VIRTUAL LLC CONFIGURATION:
89 --virt-llc requires --enable-llc-awareness to affect DSQ creation.
90 MIN-MAX specifies the target range of physical cores per virtual LLC.
91 For each physical LLC and core type, the smallest exact divisor in the
92 range is preferred; otherwise, the size with the smallest remainder is
93 used, favoring smaller sizes. Remaining cores join the last partition.
94
95 Example: On a 32-core physical LLC, --virt-llc=8-8 creates four virtual
96 LLCs, and therefore four LLC DSQs per cell."#)]
97struct Opts {
98 #[clap(short = 'v', long, action = clap::ArgAction::Count)]
100 verbose: u8,
101
102 #[clap(long, default_value = "info")]
105 log_level: String,
106
107 #[clap(long, default_value = "0")]
109 exit_dump_len: u32,
110
111 #[clap(long, default_value = "1")]
113 monitor_interval_s: u64,
114
115 #[clap(long)]
118 monitor: Option<f64>,
119
120 #[clap(short = 'V', long, action = clap::ArgAction::SetTrue)]
122 version: bool,
123
124 #[clap(long)]
126 run_id: Option<u64>,
127
128 #[clap(long, value_delimiter = ',')]
132 undefok: Vec<String>,
133
134 #[clap(long, default_value = "true", action = clap::ArgAction::Set)]
138 exiting_task_workaround: bool,
139
140 #[clap(long, action = clap::ArgAction::SetTrue)]
143 cpu_controller_disabled: bool,
144
145 #[clap(long, action = clap::ArgAction::SetTrue)]
148 reject_multicpu_pinning: bool,
149
150 #[clap(long, action = clap::ArgAction::SetTrue)]
153 enable_llc_awareness: bool,
154
155 #[clap(long, action = clap::ArgAction::SetTrue)]
157 enable_work_stealing: bool,
158
159 #[clap(long, required_unless_present_any = ["monitor", "version"])]
163 cell_parent_cgroup: Option<String>,
164
165 #[clap(long)]
170 cell_exclude: Vec<String>,
171
172 #[clap(long, default_value_t = 0)]
179 cell0_min_cpus: usize,
180
181 #[clap(long, action = clap::ArgAction::SetTrue)]
184 enable_borrowing: bool,
185
186 #[clap(long, action = clap::ArgAction::SetTrue)]
188 use_lockless_peek: bool,
189
190 #[clap(long, action = clap::ArgAction::SetTrue)]
192 enable_rebalancing: bool,
193
194 #[clap(long, default_value = "20.0")]
196 rebalance_threshold: f64,
197
198 #[clap(long, default_value = "5")]
200 rebalance_cooldown_s: u64,
201
202 #[clap(long, default_value = "0.3", value_parser = parse_ewma_factor)]
204 demand_smoothing: f64,
205
206 #[clap(long, action = clap::ArgAction::SetTrue)]
210 dynamic_affinity_cpu_selection: bool,
211
212 #[clap(long, action = clap::ArgAction::SetTrue)]
215 enable_slice_shrinking: bool,
216
217 #[clap(long, default_value = "4000")]
220 slice_shrink_max_us: u64,
221
222 #[clap(long, default_value = "500")]
226 slice_shrink_min_us: u64,
227
228 #[clap(flatten, next_help_heading = "Topology Options")]
229 topology: TopologyArgs,
230
231 #[clap(flatten, next_help_heading = "Libbpf Options")]
232 pub libbpf: LibbpfOpts,
233}
234
235const QUEUE_STATS_IDX: [bpf_intf::cell_stat_idx; 4] = [
240 bpf_intf::cell_stat_idx_CSTAT_LOCAL,
241 bpf_intf::cell_stat_idx_CSTAT_CPU_DSQ,
242 bpf_intf::cell_stat_idx_CSTAT_CELL_DSQ,
243 bpf_intf::cell_stat_idx_CSTAT_BORROWED,
244];
245
246#[derive(Debug)]
248struct Cell {
249 cpus: Cpumask,
250 subcells: Vec<Subcell>,
251}
252
253#[derive(Debug, Clone, PartialEq, Eq)]
254struct Subcell {
255 id: u32,
256 primary: Cpumask,
257 borrowable: Option<Cpumask>,
258}
259
260impl Cell {
261 fn new() -> Self {
262 Self {
263 cpus: Cpumask::new(),
264 subcells: vec![Subcell::new(0)],
265 }
266 }
267}
268
269impl Subcell {
270 fn new(id: u32) -> Self {
271 Self {
272 id,
273 primary: Cpumask::new(),
274 borrowable: None,
275 }
276 }
277}
278
279struct Scheduler<'a> {
280 skel: BpfSkel<'a>,
281 monitor_interval: Duration,
282 cells: HashMap<u32, Cell>,
283 prev_cell_stats: [[u64; NR_CSTATS]; MAX_CELLS],
286 prev_cell_running_ns: [u64; MAX_CELLS],
288 prev_cell_own_ns: [u64; MAX_CELLS],
289 prev_cell_lent_ns: [u64; MAX_CELLS],
290 metrics: Metrics,
291 stats_server: Option<StatsServer<(), Metrics>>,
292 last_configuration_seq: Option<u32>,
293 last_cpuset_seq: u32,
295 cell_manager: CellManager,
297 enable_borrowing: bool,
299 enable_rebalancing: bool,
301 rebalance_threshold: f64,
303 rebalance_cooldown: Duration,
305 demand_smoothing: f64,
307 smoothed_util: [f64; MAX_CELLS],
309 last_rebalance: Instant,
311 rebalance_count: u64,
313 epoll: Epoll,
315 stats_waker: EventFd,
317}
318
319struct DistributionStats {
320 total_decisions: u64,
321 share_of_decisions_pct: f64,
322 local_q_pct: f64,
323 cpu_q_pct: f64,
324 cell_q_pct: f64,
325 borrowed_pct: f64,
326 affn_viol_pct: f64,
327 steal_pct: f64,
328 pin_skip_pct: f64,
329
330 global_queue_decisions: u64,
332}
333
334impl Display for DistributionStats {
335 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336 const MIN_DECISIONS_WIDTH: usize = 5;
340 let descisions_width = if self.global_queue_decisions > 0 {
341 max(
342 MIN_DECISIONS_WIDTH,
343 (self.global_queue_decisions as f64).log10().ceil() as usize,
344 )
345 } else {
346 MIN_DECISIONS_WIDTH
347 };
348 write!(
349 f,
350 "{:width$} {:5.1}% | Local:{:4.1}% From: CPU:{:4.1}% Cell:{:4.1}% Borrow:{:4.1}% | V:{:4.1}% S:{:4.1}% PS:{:4.1}%",
351 self.total_decisions,
352 self.share_of_decisions_pct,
353 self.local_q_pct,
354 self.cpu_q_pct,
355 self.cell_q_pct,
356 self.borrowed_pct,
357 self.affn_viol_pct,
358 self.steal_pct,
359 self.pin_skip_pct,
360 width = descisions_width,
361 )
362 }
363}
364
365impl<'a> Scheduler<'a> {
366 fn managed_cell_parent<'b>(opts: &'b Opts) -> Result<&'b str> {
367 opts.cell_parent_cgroup
368 .as_deref()
369 .ok_or_else(|| anyhow!("--cell-parent-cgroup is required to run the scheduler"))
370 }
371
372 fn validate_args(_opts: &Opts) -> Result<()> {
373 Ok(())
374 }
375
376 fn init(opts: &Opts, open_object: &'a mut MaybeUninit<OpenObject>) -> Result<Self> {
377 Self::validate_args(opts).context("validating scheduler options")?;
378
379 let topology = Topology::with_args(&opts.topology).context("detecting system topology")?;
380
381 let nr_llc = topology.all_llcs.len().max(1);
382
383 let mut skel_builder = BpfSkelBuilder::default();
384 skel_builder
385 .obj_builder
386 .debug(opts.log_level.contains("trace"));
387 init_libbpf_logging(None);
388 info!(
389 "Running scx_mitosis (build ID: {})",
390 build_id::full_version(env!("CARGO_PKG_VERSION"))
391 );
392
393 let open_opts = opts.libbpf.clone().into_bpf_open_opts();
394 let mut skel = scx_ops_open!(skel_builder, open_object, mitosis, open_opts)
395 .context("opening BPF skeleton")?;
396
397 skel.struct_ops.mitosis_mut().exit_dump_len = opts.exit_dump_len;
398
399 let rodata = skel
400 .maps
401 .rodata_data
402 .as_mut()
403 .expect("BUG: rodata_data missing after skel open");
404
405 rodata.slice_ns = scx_enums.SCX_SLICE_DFL;
406 rodata.exiting_task_workaround_enabled = opts.exiting_task_workaround;
407 rodata.cpu_controller_disabled = opts.cpu_controller_disabled;
408 rodata.dynamic_affinity_cpu_selection = opts.dynamic_affinity_cpu_selection;
409
410 if opts.slice_shrink_min_us >= opts.slice_shrink_max_us {
412 bail!(
413 "--slice-shrink-min-us ({}) must be less than --slice-shrink-max-us ({})",
414 opts.slice_shrink_min_us,
415 opts.slice_shrink_max_us
416 );
417 }
418 rodata.enable_slice_shrinking = opts.enable_slice_shrinking;
419 rodata.slice_shrink_max_ns = opts.slice_shrink_max_us * 1_000;
420 rodata.slice_shrink_multiplier = 2;
422 rodata.slice_shrink_min_ns = opts.slice_shrink_min_us * 1_000;
423
424 rodata.nr_possible_cpus = *NR_CPUS_POSSIBLE as u32;
425 for cpu in topology.all_cpus.keys() {
426 rodata.all_cpus[cpu / 8] |= 1 << (cpu % 8);
427 }
428
429 rodata.reject_multicpu_pinning = opts.reject_multicpu_pinning;
430
431 rodata.nr_llc = nr_llc as u32;
433 rodata.enable_llc_awareness = opts.enable_llc_awareness;
434
435 rodata.enable_borrowing = opts.enable_borrowing;
436 rodata.use_lockless_peek = opts.use_lockless_peek;
437
438 match *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP {
439 0 => info!("Kernel does not support queued wakeup optimization."),
440 v => skel.struct_ops.mitosis_mut().flags |= v,
441 }
442
443 let mitosis_topology = MitosisTopology::new(&topology);
444 mitosis_topology
445 .apply(&mut skel)
446 .context("mitosis_topology.apply")?;
447
448 let skel = scx_ops_load!(skel, mitosis, uei).context("loading BPF skeleton")?;
449
450 let stats_server = StatsServer::new(stats::server_data())
451 .launch()
452 .context("launching stats server")?;
453
454 let parent_cgroup = Self::managed_cell_parent(opts)?;
455 let exclude: HashSet<String> = opts.cell_exclude.iter().cloned().collect();
456 let cell_manager = CellManager::new(
457 parent_cgroup,
458 MAX_CELLS as u32,
459 topology.span.clone(),
460 exclude,
461 opts.cell0_min_cpus,
462 mitosis_topology.cpu_to_llc.into_iter().collect(),
463 )
464 .with_context(|| format!("initializing cell manager for cgroup {}", parent_cgroup))?;
465
466 let epoll = Epoll::new(EpollCreateFlags::empty()).context("creating epoll instance")?;
468
469 let stats_waker = EventFd::from_value_and_flags(
471 0,
472 nix::sys::eventfd::EfdFlags::EFD_NONBLOCK | nix::sys::eventfd::EfdFlags::EFD_SEMAPHORE,
473 )
474 .context("creating stats-waker eventfd")?;
475
476 epoll
478 .add(
479 &stats_waker,
480 EpollEvent::new(EpollFlags::EPOLLIN, STATS_TOKEN),
481 )
482 .context("registering stats-waker with epoll")?;
483
484 epoll
485 .add(
486 &cell_manager,
487 EpollEvent::new(EpollFlags::EPOLLIN, INOTIFY_TOKEN),
488 )
489 .context("registering cell manager inotify with epoll")?;
490
491 Ok(Self {
492 skel,
493 monitor_interval: Duration::from_secs(opts.monitor_interval_s),
494 cells: HashMap::new(),
495 prev_cell_stats: [[0; NR_CSTATS]; MAX_CELLS],
496 prev_cell_running_ns: [0; MAX_CELLS],
497 prev_cell_own_ns: [0; MAX_CELLS],
498 prev_cell_lent_ns: [0; MAX_CELLS],
499 metrics: Metrics::default(),
500 stats_server: Some(stats_server),
501 last_configuration_seq: None,
502 last_cpuset_seq: 0,
503 cell_manager,
504 enable_borrowing: opts.enable_borrowing,
505 enable_rebalancing: opts.enable_rebalancing,
506 rebalance_threshold: opts.rebalance_threshold,
507 rebalance_cooldown: Duration::from_secs(opts.rebalance_cooldown_s),
508 demand_smoothing: opts.demand_smoothing,
509 smoothed_util: [0.0; MAX_CELLS],
510 last_rebalance: Instant::now(),
511 rebalance_count: 0,
512 epoll,
513 stats_waker,
514 })
515 }
516
517 fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
518 let struct_ops = scx_ops_attach!(self.skel, mitosis).context("attaching BPF scheduler")?;
519
520 info!("Mitosis Scheduler Attached. Run `scx_mitosis --monitor` for metrics.");
521
522 self.apply_initial_cells()
524 .context("applying initial cell configuration")?;
525
526 let (res_ch, req_ch) = self
527 .stats_server
528 .as_ref()
529 .expect("BUG: stats_server missing after init")
530 .channels();
531
532 let stats_waker_fd = self
536 .stats_waker
537 .as_fd()
538 .try_clone_to_owned()
539 .context("cloning stats-waker fd for bridge thread")?;
540 let stats_waker = unsafe { EventFd::from_owned_fd(stats_waker_fd) };
541 let stats_bridge = std::thread::spawn(move || {
542 while req_ch.recv().is_ok() {
543 let _ = stats_waker.write(1);
545 }
546 });
547
548 while !shutdown.load(Ordering::Relaxed) && !uei_exited!(&self.skel, uei) {
549 let mut events = [EpollEvent::empty(); 1];
550 let timeout = EpollTimeout::try_from(self.monitor_interval).with_context(|| {
551 format!(
552 "monitor_interval {:?} exceeds maximum epoll timeout",
553 self.monitor_interval,
554 )
555 })?;
556
557 match self.epoll.wait(&mut events, timeout) {
558 Ok(n) => {
559 for event in &events[..n] {
560 match event.data() {
561 INOTIFY_TOKEN => {
562 self.process_cell_events()
564 .context("processing cell manager events")?;
565 }
566 STATS_TOKEN => {
567 let _ = self.stats_waker.read();
569 res_ch
570 .send(self.get_metrics())
571 .context("sending metrics response")?;
572 }
573 _ => {}
574 }
575 }
576 }
577 Err(nix::errno::Errno::EINTR) => continue,
578 Err(e) => return Err(e.into()),
579 }
580
581 self.refresh_bpf_cells()
583 .context("refreshing BPF cell state")?;
584 self.check_cpuset_changes()
585 .context("checking cpuset changes")?;
586 self.collect_metrics().context("collecting metrics")?;
587
588 if self.enable_rebalancing {
589 self.maybe_rebalance().context("running rebalance check")?;
590 }
591 }
592
593 drop(struct_ops);
594 drop(self.stats_server.take());
596 let _ = stats_bridge.join();
597 info!("Unregister {SCHEDULER_NAME} scheduler");
598 uei_report!(&self.skel, uei)
599 }
600
601 fn apply_initial_cells(&mut self) -> Result<()> {
603 let cpu_assignments = self
604 .compute_and_apply_cell_config(&[])
605 .context("computing initial cell configuration")?;
606
607 info!(
608 "Applied initial cell configuration: {}",
609 self.cell_manager.format_cell_config(&cpu_assignments)
610 );
611
612 Ok(())
613 }
614
615 fn process_cell_events(&mut self) -> Result<()> {
617 let (num_new, num_destroyed, new_cell_ids, destroyed_cell_ids) = {
618 let (new_cells, destroyed_cells) = self
619 .cell_manager
620 .process_events()
621 .context("processing inotify events")?;
622
623 if new_cells.is_empty() && destroyed_cells.is_empty() {
624 return Ok(());
625 }
626
627 let new_ids: Vec<u32> = new_cells.iter().map(|(_, cell_id)| *cell_id).collect();
628 (
629 new_cells.len(),
630 destroyed_cells.len(),
631 new_ids,
632 destroyed_cells,
633 )
634 };
635
636 for &cell_id in &destroyed_cell_ids {
639 self.smoothed_util[cell_id as usize] = 0.0;
640 }
641
642 let cpu_assignments = self
643 .compute_and_apply_cell_config(&new_cell_ids)
644 .context("recomputing cell configuration for new cgroups")?;
645
646 info!(
647 "Cell config updated ({} new, {} destroyed): {}",
648 num_new,
649 num_destroyed,
650 self.cell_manager.format_cell_config(&cpu_assignments)
651 );
652
653 Ok(())
654 }
655
656 fn compute_and_apply_cell_config(
665 &mut self,
666 new_cell_ids: &[u32],
667 ) -> Result<Vec<CpuAssignment>> {
668 let (cell_assignments, cpu_assignments, subcell_assignments) = {
669 let active_cell_ids: Vec<u32> = self
670 .cell_manager
671 .get_cell_assignments()
672 .iter()
673 .map(|(_, cell_id)| *cell_id)
674 .collect();
675 let all_cell_ids: Vec<u32> = std::iter::once(0)
677 .chain(active_cell_ids.iter().copied())
678 .collect();
679
680 let cpu_assignments = if self.enable_rebalancing {
681 let new_set: HashSet<u32> = new_cell_ids.iter().copied().collect();
683 let existing_utils: Vec<f64> = all_cell_ids
684 .iter()
685 .filter(|id| !new_set.contains(id))
686 .map(|&id| self.smoothed_util[id as usize])
687 .collect();
688
689 let has_data = existing_utils.iter().any(|&u| u > 0.0);
690
691 if has_data {
692 let avg_util: f64 =
694 existing_utils.iter().sum::<f64>() / existing_utils.len().max(1) as f64;
695 for &id in new_cell_ids {
696 self.smoothed_util[id as usize] = avg_util;
697 info!(
698 "Seeded new cell {} smoothed_util to average {:.1}%",
699 id, avg_util
700 );
701 }
702
703 let cell_demands: HashMap<u32, f64> = all_cell_ids
705 .iter()
706 .map(|&id| (id, self.smoothed_util[id as usize]))
707 .collect();
708
709 self.cell_manager
710 .compute_demand_cpu_assignments(&cell_demands, self.enable_borrowing)
711 .context("computing demand-weighted CPU assignments")?
712 } else {
713 self.cell_manager
715 .compute_cpu_assignments(self.enable_borrowing)
716 .context("computing equal-weight CPU assignments (no utilization data)")?
717 }
718 } else {
719 self.cell_manager
720 .compute_cpu_assignments(self.enable_borrowing)
721 .context("computing equal-weight CPU assignments (rebalancing disabled)")?
722 };
723
724 let subcell_assignments = self.compute_subcell_assignments(&cpu_assignments)?;
727
728 (
729 self.cell_manager.get_cell_assignments(),
730 cpu_assignments,
731 subcell_assignments,
732 )
733 };
734
735 self.apply_cell_config(&cell_assignments, &cpu_assignments, &subcell_assignments)
736 .context("applying cell configuration to BPF")?;
737
738 Ok(cpu_assignments)
739 }
740
741 fn maybe_rebalance(&mut self) -> Result<()> {
743 if self.last_rebalance.elapsed() < self.rebalance_cooldown {
745 return Ok(());
746 }
747
748 let active_cells: Vec<u32> = self.cells.keys().copied().collect();
750 if active_cells.len() < 2 {
751 return Ok(());
752 }
753
754 let mut min_util = f64::MAX;
755 let mut max_util = f64::MIN;
756 for &cell_id in &active_cells {
757 let util = self.smoothed_util[cell_id as usize];
758 if util < min_util {
759 min_util = util;
760 }
761 if util > max_util {
762 max_util = util;
763 }
764 }
765
766 let spread = max_util - min_util;
767 if spread < self.rebalance_threshold {
768 return Ok(());
769 }
770
771 let cell_demands: HashMap<u32, f64> = active_cells
773 .iter()
774 .map(|&cell_id| (cell_id, self.smoothed_util[cell_id as usize]))
775 .collect();
776
777 let (cell_assignments, cpu_assignments, subcell_assignments) = {
779 let cpu_assignments = self
780 .cell_manager
781 .compute_demand_cpu_assignments(&cell_demands, self.enable_borrowing)
782 .context("computing demand-weighted CPU assignments for rebalance")?;
783
784 let changed = cpu_assignments.iter().any(|a| {
785 self.cells
786 .get(&a.id)
787 .map_or(true, |cell| cell.cpus != a.primary)
788 });
789
790 if !changed {
794 return Ok(());
795 }
796
797 let subcell_assignments = self.compute_subcell_assignments(&cpu_assignments)?;
798
799 (
800 self.cell_manager.get_cell_assignments(),
801 cpu_assignments,
802 subcell_assignments,
803 )
804 };
805
806 self.apply_cell_config(&cell_assignments, &cpu_assignments, &subcell_assignments)
807 .context("applying rebalanced cell configuration to BPF")?;
808
809 self.last_rebalance = Instant::now();
810 self.rebalance_count += 1;
811 self.metrics.rebalance_count = self.rebalance_count;
812
813 info!(
814 "Rebalanced CPUs (spread={:.1}%, count={}): {}",
815 spread,
816 self.rebalance_count,
817 self.cell_manager.format_cell_config(&cpu_assignments)
818 );
819
820 Ok(())
821 }
822
823 fn compute_subcell_assignments(
824 &self,
825 cell_cpu_assignments: &[CpuAssignment],
826 ) -> Result<Vec<Vec<CpuAssignment>>> {
827 cell_cpu_assignments
828 .iter()
829 .map(|cell_assignment| {
830 let recipients: Vec<CpuRecipient> = self
831 .cells
832 .get(&cell_assignment.id)
833 .map(|cell| {
834 cell.subcells
835 .iter()
836 .map(|subcell| {
837 CpuRecipient::unpinned(subcell.id, 1.0, &cell_assignment.primary)
838 })
839 .collect()
840 })
841 .unwrap_or_else(|| {
842 vec![CpuRecipient::unpinned(0, 1.0, &cell_assignment.primary)]
843 });
844
845 CellManager::compute_subcell_cpu_assignments(
846 &cell_assignment.primary,
847 &recipients,
848 self.enable_borrowing,
849 )
850 })
851 .collect()
852 }
853
854 fn refresh_bpf_subcells(&mut self, active_cells: &HashSet<u32>) -> Result<()> {
855 for cell_id in active_cells {
856 let bpf_cell = read_bpf_cell(&self.skel, *cell_id)?;
857 let subcells: Vec<Subcell> = bpf_cell
858 .subcells
859 .iter()
860 .filter(|subcell| subcell.in_use != 0)
861 .map(|subcell| {
862 Ok(Subcell {
863 id: subcell.id,
864 primary: read_cpumask_from_bytes(&subcell.primary.mask)?,
865 borrowable: if self.enable_borrowing {
866 Some(read_cpumask_from_bytes(&subcell.borrowable.mask)?)
867 } else {
868 None
869 },
870 })
871 })
872 .collect::<Result<_>>()?;
873
874 if subcells.is_empty() {
875 bail!("Cell {} has no active subcells in BPF state", cell_id);
876 }
877
878 let cell = self.cells.get_mut(cell_id).ok_or_else(|| {
879 anyhow::anyhow!("Cell {} missing during subcell refresh", cell_id)
880 })?;
881 cell.subcells = subcells;
882 }
883
884 Ok(())
885 }
886
887 fn apply_cell_config(
892 &mut self,
893 cell_assignments: &[(u64, u32)],
894 cpu_assignments: &[CpuAssignment],
895 subcell_assignments: &[Vec<CpuAssignment>],
896 ) -> Result<()> {
897 let bss_data = self
898 .skel
899 .maps
900 .bss_data
901 .as_mut()
902 .expect("bss_data must be available after scheduler load");
903
904 let config = &mut bss_data.cell_config;
905
906 unsafe {
914 std::ptr::write_bytes(
915 config as *mut _ as *mut u8,
916 0,
917 std::mem::size_of_val(config),
918 );
919 }
920
921 if subcell_assignments.len() != cpu_assignments.len() {
922 bail!(
923 "Mismatched subcell assignments: {} cell assignments vs {} subcell groups",
924 cpu_assignments.len(),
925 subcell_assignments.len()
926 );
927 }
928
929 if cell_assignments.len() > bpf_intf::consts_MAX_CELLS as usize {
930 bail!(
931 "Too many cell assignments: {} > MAX_CELLS ({})",
932 cell_assignments.len(),
933 bpf_intf::consts_MAX_CELLS
934 );
935 }
936 config.num_cell_assignments = cell_assignments.len() as u32;
937
938 for cell_id in 0..MAX_CELLS {
939 for subcell_id in 0..MAX_SUBCELLS_PER_CELL {
940 config.subcells[cell_id][subcell_id].id = subcell_id as u32;
941 }
942 }
943
944 for (i, (cgid, cell_id)) in cell_assignments.iter().enumerate() {
945 config.assignments[i].cgid = *cgid;
946 config.assignments[i].cell_id = *cell_id;
947 }
948
949 let mut max_cell_id: u32 = 0;
951 for (a, cell_subcells) in cpu_assignments.iter().zip(subcell_assignments.iter()) {
952 validate_subcell_assignments(a, cell_subcells, self.enable_borrowing)?;
953
954 if a.id >= bpf_intf::consts_MAX_CELLS {
955 bail!(
956 "Cell ID {} exceeds MAX_CELLS ({})",
957 a.id,
958 bpf_intf::consts_MAX_CELLS
959 );
960 }
961 max_cell_id = max_cell_id.max(a.id + 1);
962
963 write_cpumask_to_config(&a.primary, &mut config.cpumasks[a.id as usize].mask);
964
965 if let Some(ref borrowable) = a.borrowable {
966 write_cpumask_to_config(
967 borrowable,
968 &mut config.borrowable_cpumasks[a.id as usize].mask,
969 );
970 }
971
972 for subcell in cell_subcells {
973 let slot = subcell.id as usize;
974 config.subcells[a.id as usize][slot].id = subcell.id;
975 config.subcells[a.id as usize][slot].in_use = 1;
976 write_cpumask_to_config(
977 &subcell.primary,
978 &mut config.subcells[a.id as usize][slot].primary.mask,
979 );
980 if let Some(ref borrowable) = subcell.borrowable {
981 write_cpumask_to_config(
982 borrowable,
983 &mut config.subcells[a.id as usize][slot].borrowable.mask,
984 );
985 }
986 }
987 }
988 config.num_cells = max_cell_id;
989
990 let prog = &mut self.skel.progs.apply_cell_config;
992 let out = prog
993 .test_run(ProgramInput::default())
994 .context("Failed to run apply_cell_config BPF program")?;
995 if out.return_value != 0 {
996 bail!(
997 "apply_cell_config BPF program returned error {} (num_assignments={}, num_cells={})",
998 out.return_value as i32,
999 cell_assignments.len(),
1000 cpu_assignments.len()
1001 );
1002 }
1003
1004 Ok(())
1005 }
1006
1007 fn get_metrics(&self) -> Metrics {
1008 self.metrics.clone()
1009 }
1010
1011 fn calculate_distribution_stats(
1012 &self,
1013 queue_counts: &[u64; QUEUE_STATS_IDX.len()],
1014 global_queue_decisions: u64,
1015 scope_queue_decisions: u64,
1016 scope_affn_viols: u64,
1017 scope_steals: u64,
1018 scope_pin_skips: u64,
1019 ) -> Result<DistributionStats> {
1020 let share_of_global =
1023 100.0 * (scope_queue_decisions as f64) / (global_queue_decisions as f64);
1024
1025 let queue_pct = if scope_queue_decisions == 0 {
1027 debug!("No queue decisions in scope, zeroing out queue distribution");
1028 [0.0; QUEUE_STATS_IDX.len()]
1029 } else {
1030 core::array::from_fn(|i| {
1031 100.0 * (queue_counts[i] as f64) / (scope_queue_decisions as f64)
1032 })
1033 };
1034
1035 let affinity_violations_percent = if scope_queue_decisions == 0 {
1037 debug!("No queue decisions in scope, zeroing out affinity violations");
1038 0.0
1039 } else {
1040 100.0 * (scope_affn_viols as f64) / (scope_queue_decisions as f64)
1041 };
1042
1043 let steal_pct = if scope_queue_decisions == 0 {
1044 0.0
1045 } else {
1046 100.0 * (scope_steals as f64) / (scope_queue_decisions as f64)
1047 };
1048
1049 let pin_skip_pct = if scope_queue_decisions == 0 {
1050 0.0
1051 } else {
1052 100.0 * (scope_pin_skips as f64) / (scope_queue_decisions as f64)
1053 };
1054
1055 const EXPECTED_QUEUES: usize = 4;
1056 if queue_pct.len() != EXPECTED_QUEUES {
1057 bail!(
1058 "Expected {} queues, got {}",
1059 EXPECTED_QUEUES,
1060 queue_pct.len()
1061 );
1062 }
1063
1064 return Ok(DistributionStats {
1065 total_decisions: scope_queue_decisions,
1066 share_of_decisions_pct: share_of_global,
1067 local_q_pct: queue_pct[0],
1068 cpu_q_pct: queue_pct[1],
1069 cell_q_pct: queue_pct[2],
1070 borrowed_pct: queue_pct[3],
1071 affn_viol_pct: affinity_violations_percent,
1072 steal_pct,
1073 pin_skip_pct,
1074 global_queue_decisions,
1075 });
1076 }
1077
1078 fn update_and_log_global_queue_stats(
1080 &mut self,
1081 global_queue_decisions: u64,
1082 cell_stats_delta: &[[u64; NR_CSTATS]; MAX_CELLS],
1083 ) -> Result<()> {
1084 let mut queue_counts = [0; QUEUE_STATS_IDX.len()];
1086 for cells in 0..MAX_CELLS {
1087 for (i, stat) in QUEUE_STATS_IDX.iter().enumerate() {
1088 queue_counts[i] += cell_stats_delta[cells][*stat as usize];
1089 }
1090 }
1091
1092 let prefix = "Total Decisions:";
1093
1094 let scope_affn_viols: u64 = cell_stats_delta
1096 .iter()
1097 .map(|&cell| cell[bpf_intf::cell_stat_idx_CSTAT_AFFN_VIOL as usize])
1098 .sum::<u64>();
1099
1100 let scope_steals: u64 = cell_stats_delta
1102 .iter()
1103 .map(|&cell| cell[bpf_intf::cell_stat_idx_CSTAT_STEAL as usize])
1104 .sum::<u64>();
1105
1106 let scope_pin_skips: u64 = cell_stats_delta
1108 .iter()
1109 .map(|&cell| cell[bpf_intf::cell_stat_idx_CSTAT_PIN_SKIP as usize])
1110 .sum::<u64>();
1111
1112 let stats = self
1114 .calculate_distribution_stats(
1115 &queue_counts,
1116 global_queue_decisions,
1117 global_queue_decisions,
1118 scope_affn_viols,
1119 scope_steals,
1120 scope_pin_skips,
1121 )
1122 .context("calculating global queue distribution stats")?;
1123
1124 self.metrics.update(&stats);
1125
1126 let sum = |idx: usize| -> u64 { cell_stats_delta.iter().map(|c| c[idx]).sum() };
1128 self.metrics.drain_cnt = sum(bpf_intf::cell_stat_idx_CSTAT_DRAIN_CNT as usize);
1129 self.metrics.drain_affn_cnt = sum(bpf_intf::cell_stat_idx_CSTAT_DRAIN_AFFN_CNT as usize);
1130 self.metrics.slice_shrink_max =
1131 sum(bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_MAX as usize);
1132 self.metrics.slice_shrink_proportional =
1133 sum(bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_PROPORTIONAL as usize);
1134 self.metrics.slice_shrink_min =
1135 sum(bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_MIN as usize);
1136 self.metrics.slice_shrink = self.metrics.slice_shrink_max
1137 + self.metrics.slice_shrink_proportional
1138 + self.metrics.slice_shrink_min;
1139
1140 trace!("{} {}", prefix, stats);
1141
1142 Ok(())
1143 }
1144
1145 fn update_and_log_cell_queue_stats(
1147 &mut self,
1148 global_queue_decisions: u64,
1149 cell_stats_delta: &[[u64; NR_CSTATS]; MAX_CELLS],
1150 ) -> Result<()> {
1151 for cell in 0..MAX_CELLS {
1152 let cell_queue_decisions = QUEUE_STATS_IDX
1153 .iter()
1154 .map(|&stat| cell_stats_delta[cell][stat as usize])
1155 .sum::<u64>();
1156
1157 if cell_queue_decisions == 0 {
1159 continue;
1160 }
1161
1162 let mut queue_counts = [0; QUEUE_STATS_IDX.len()];
1163 for (i, &stat) in QUEUE_STATS_IDX.iter().enumerate() {
1164 queue_counts[i] = cell_stats_delta[cell][stat as usize];
1165 }
1166
1167 const MIN_CELL_WIDTH: usize = 2;
1168 let cell_width: usize = max(MIN_CELL_WIDTH, (MAX_CELLS as f64).log10().ceil() as usize);
1169
1170 let prefix = format!(" Cell {:width$}:", cell, width = cell_width);
1171
1172 let scope_affn_viols: u64 =
1174 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_AFFN_VIOL as usize];
1175
1176 let scope_steals: u64 =
1178 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_STEAL as usize];
1179
1180 let scope_pin_skips: u64 =
1182 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_PIN_SKIP as usize];
1183
1184 let stats = self
1185 .calculate_distribution_stats(
1186 &queue_counts,
1187 global_queue_decisions,
1188 cell_queue_decisions,
1189 scope_affn_viols,
1190 scope_steals,
1191 scope_pin_skips,
1192 )
1193 .with_context(|| {
1194 format!("calculating queue distribution stats for cell {}", cell)
1195 })?;
1196
1197 let cell_metrics = self.metrics.cells.entry(cell as u32).or_default();
1198 cell_metrics.update(&stats);
1199
1200 cell_metrics.drain_cnt =
1202 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_DRAIN_CNT as usize];
1203 cell_metrics.drain_affn_cnt =
1204 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_DRAIN_AFFN_CNT as usize];
1205 cell_metrics.slice_shrink_max =
1206 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_MAX as usize];
1207 cell_metrics.slice_shrink_proportional = cell_stats_delta[cell]
1208 [bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_PROPORTIONAL as usize];
1209 cell_metrics.slice_shrink_min =
1210 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_MIN as usize];
1211 cell_metrics.slice_shrink = cell_metrics.slice_shrink_max
1212 + cell_metrics.slice_shrink_proportional
1213 + cell_metrics.slice_shrink_min;
1214
1215 trace!("{} {}", prefix, stats);
1216 }
1217 Ok(())
1218 }
1219
1220 fn update_drain_metrics(&mut self, cell_stats_delta: &[[u64; NR_CSTATS]; MAX_CELLS]) {
1221 let mut total = 0;
1222 let mut affn_total = 0;
1223
1224 for cell in 0..MAX_CELLS {
1225 let drain_cnt =
1226 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_DRAIN_CNT as usize];
1227 let drain_affn_cnt =
1228 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_DRAIN_AFFN_CNT as usize];
1229 total += drain_cnt;
1230 affn_total += drain_affn_cnt;
1231
1232 if let Some(cell_metrics) = self.metrics.cells.get_mut(&(cell as u32)) {
1233 cell_metrics.drain_cnt = drain_cnt;
1234 cell_metrics.drain_affn_cnt = drain_affn_cnt;
1235 } else if drain_cnt > 0 || drain_affn_cnt > 0 {
1236 let cell_metrics = self.metrics.cells.entry(cell as u32).or_default();
1237 cell_metrics.drain_cnt = drain_cnt;
1238 cell_metrics.drain_affn_cnt = drain_affn_cnt;
1239 }
1240 }
1241
1242 self.metrics.drain_cnt = total;
1243 self.metrics.drain_affn_cnt = affn_total;
1244 }
1245
1246 fn log_all_queue_stats(
1247 &mut self,
1248 cell_stats_delta: &[[u64; NR_CSTATS]; MAX_CELLS],
1249 ) -> Result<()> {
1250 let global_queue_decisions: u64 = cell_stats_delta
1252 .iter()
1253 .flat_map(|cell| QUEUE_STATS_IDX.iter().map(|&idx| cell[idx as usize]))
1254 .sum();
1255
1256 self.update_drain_metrics(cell_stats_delta);
1257
1258 if global_queue_decisions == 0 {
1259 if self.metrics.drain_cnt == 0 {
1260 warn!("No queueing decisions made globally");
1261 }
1262 return Ok(());
1263 }
1264
1265 self.update_and_log_global_queue_stats(global_queue_decisions, &cell_stats_delta)
1266 .context("updating global queue stats")?;
1267
1268 self.update_and_log_cell_queue_stats(global_queue_decisions, &cell_stats_delta)
1269 .context("updating per-cell queue stats")?;
1270
1271 Ok(())
1272 }
1273
1274 fn calculate_cell_stat_delta(
1275 &mut self,
1276 cpu_ctxs: &[bpf_intf::cpu_ctx],
1277 ) -> Result<[[u64; NR_CSTATS]; MAX_CELLS]> {
1278 let mut cell_stats_delta = [[0 as u64; NR_CSTATS]; MAX_CELLS];
1279
1280 for cell in 0..MAX_CELLS {
1283 for stat in 0..NR_CSTATS {
1284 let mut cur_cell_stat = 0;
1285
1286 for cpu_ctx in cpu_ctxs.iter() {
1288 cur_cell_stat += cpu_ctx.cstats[cell][stat];
1289 }
1290
1291 cell_stats_delta[cell][stat] = cur_cell_stat - self.prev_cell_stats[cell][stat];
1293 self.prev_cell_stats[cell][stat] = cur_cell_stat;
1294 }
1295 }
1296 Ok(cell_stats_delta)
1297 }
1298
1299 fn collect_metrics(&mut self) -> Result<()> {
1301 let cpu_ctxs = read_cpu_ctxs(&self.skel).context("reading per-CPU contexts for metrics")?;
1302
1303 let cell_stats_delta = self
1304 .calculate_cell_stat_delta(&cpu_ctxs)
1305 .context("calculating cell stat deltas")?;
1306
1307 self.log_all_queue_stats(&cell_stats_delta)
1308 .context("logging queue stats")?;
1309
1310 self.metrics.enforced_holdout = self.cell_manager.enforced_holdout() as u64;
1313
1314 self.collect_demand_metrics(&cpu_ctxs)
1315 .context("collecting demand metrics")?;
1316
1317 for (cell_id, cell) in &self.cells {
1318 trace!("CELL[{}]: {}", cell_id, cell.cpus);
1319 }
1320
1321 for (cell_id, cell) in self.cells.iter() {
1322 self.metrics
1324 .cells
1325 .entry(*cell_id)
1326 .and_modify(|cell_metrics| {
1327 cell_metrics.num_cpus = cell.cpus.weight() as u32;
1328 cell_metrics.cgroup_path = self.cell_manager.cgroup_path_for_cell(*cell_id);
1329 });
1330 }
1331 self.metrics.num_cells = self.cells.len() as u32;
1332
1333 Ok(())
1334 }
1335
1336 fn collect_demand_metrics(&mut self, cpu_ctxs: &[bpf_intf::cpu_ctx]) -> Result<()> {
1338 let mut total_running_ns = [0u64; MAX_CELLS];
1343 let mut on_own_ns = [0u64; MAX_CELLS];
1344 let mut lent_ns = [0u64; MAX_CELLS];
1345
1346 for cpu_ctx in cpu_ctxs.iter() {
1347 let owner = cpu_ctx.cell as usize;
1348 for cell in 0..MAX_CELLS {
1349 let ns = cpu_ctx.running_ns[cell];
1350 total_running_ns[cell] += ns;
1351 if owner == cell {
1352 on_own_ns[cell] += ns;
1353 }
1354 }
1355 if owner >= MAX_CELLS {
1356 bail!(
1357 "CPU has invalid cell assignment {} (MAX_CELLS={})",
1358 owner,
1359 MAX_CELLS
1360 );
1361 }
1362 let total_on_cpu: u64 = cpu_ctx.running_ns.iter().sum();
1364 let owner_on_cpu = cpu_ctx.running_ns[owner];
1365 lent_ns[owner] += total_on_cpu.saturating_sub(owner_on_cpu);
1366 }
1367
1368 let interval_ns = self.monitor_interval.as_nanos() as u64;
1370
1371 let mut global_running_delta = 0u64;
1372 let mut global_borrowed_delta = 0u64;
1373 let mut global_lent_delta = 0u64;
1374 let mut global_capacity = 0u64;
1375
1376 for cell in 0..MAX_CELLS {
1377 let delta_running =
1378 total_running_ns[cell].saturating_sub(self.prev_cell_running_ns[cell]);
1379 let delta_on_own = on_own_ns[cell].saturating_sub(self.prev_cell_own_ns[cell]);
1380 let delta_lent = lent_ns[cell].saturating_sub(self.prev_cell_lent_ns[cell]);
1381
1382 self.prev_cell_running_ns[cell] = total_running_ns[cell];
1383 self.prev_cell_own_ns[cell] = on_own_ns[cell];
1384 self.prev_cell_lent_ns[cell] = lent_ns[cell];
1385
1386 if delta_running == 0 && delta_lent == 0 {
1387 continue;
1388 }
1389
1390 let delta_borrowed = delta_running.saturating_sub(delta_on_own);
1392
1393 let Some(cell_info) = self.cells.get(&(cell as u32)) else {
1396 continue;
1397 };
1398
1399 let nr_cpus = cell_info.cpus.weight() as u64;
1400 if nr_cpus == 0 {
1401 bail!("Cell {} has 0 CPUs assigned", cell);
1402 }
1403
1404 let capacity = nr_cpus * interval_ns;
1406 let util_pct = 100.0 * (delta_running as f64) / (capacity as f64);
1408 let demand_borrow_pct = if delta_running > 0 {
1410 100.0 * (delta_borrowed as f64) / (delta_running as f64)
1411 } else {
1412 0.0
1413 };
1414 let lent_pct = 100.0 * (delta_lent as f64) / (capacity as f64);
1416
1417 if self.enable_rebalancing {
1419 self.smoothed_util[cell] = self.demand_smoothing * util_pct
1420 + (1.0 - self.demand_smoothing) * self.smoothed_util[cell];
1421 }
1422
1423 self.metrics
1424 .cells
1425 .entry(cell as u32)
1426 .or_default()
1427 .update_demand(util_pct, demand_borrow_pct, lent_pct);
1428
1429 if self.enable_rebalancing {
1431 self.metrics
1432 .cells
1433 .entry(cell as u32)
1434 .or_default()
1435 .smoothed_util_pct = self.smoothed_util[cell];
1436 }
1437
1438 global_running_delta = global_running_delta.saturating_add(delta_running);
1439 global_borrowed_delta = global_borrowed_delta.saturating_add(delta_borrowed);
1440 global_lent_delta = global_lent_delta.saturating_add(delta_lent);
1441 global_capacity = global_capacity.saturating_add(capacity);
1442 }
1443
1444 let global_util_pct = if global_capacity > 0 {
1445 100.0 * (global_running_delta as f64) / (global_capacity as f64)
1446 } else {
1447 0.0
1448 };
1449 let global_borrow_pct = if global_running_delta > 0 {
1450 100.0 * (global_borrowed_delta as f64) / (global_running_delta as f64)
1451 } else {
1452 0.0
1453 };
1454 let global_lent_pct = if global_capacity > 0 {
1455 100.0 * (global_lent_delta as f64) / (global_capacity as f64)
1456 } else {
1457 0.0
1458 };
1459
1460 self.metrics
1461 .update_demand(global_util_pct, global_borrow_pct, global_lent_pct);
1462
1463 Ok(())
1464 }
1465
1466 fn update_applied_cpuset_seq(&mut self) {
1468 unsafe {
1469 let ptr = &mut self
1470 .skel
1471 .maps
1472 .bss_data
1473 .as_mut()
1474 .expect("BUG: bss_data missing after scheduler load")
1475 .applied_cpuset_seq as *mut u32;
1476 std::ptr::write_volatile(ptr, self.last_cpuset_seq);
1477 }
1478 }
1479
1480 fn check_cpuset_changes(&mut self) -> Result<()> {
1482 let current_seq = unsafe {
1483 let ptr = &self
1484 .skel
1485 .maps
1486 .bss_data
1487 .as_ref()
1488 .expect("BUG: bss_data missing after scheduler load")
1489 .cpuset_seq as *const u32;
1490 (ptr as *const AtomicU32)
1491 .as_ref()
1492 .expect("BUG: cpuset_seq pointer cast yielded null")
1493 .load(Ordering::Acquire)
1494 };
1495
1496 if current_seq == self.last_cpuset_seq {
1497 return Ok(());
1498 }
1499 self.last_cpuset_seq = current_seq;
1500
1501 if !self
1502 .cell_manager
1503 .refresh_cpusets()
1504 .context("refreshing cell cpusets")?
1505 {
1506 self.update_applied_cpuset_seq();
1508 return Ok(());
1509 }
1510
1511 let cpu_assignments = self
1512 .compute_and_apply_cell_config(&[])
1513 .context("recomputing cell configuration after cpuset change")?;
1514 self.update_applied_cpuset_seq();
1515 info!(
1516 "Cpuset change detected, recomputed config: {}",
1517 self.cell_manager.format_cell_config(&cpu_assignments)
1518 );
1519 Ok(())
1520 }
1521
1522 fn refresh_bpf_cells(&mut self) -> Result<()> {
1523 let applied_configuration = unsafe {
1524 let ptr = &self
1525 .skel
1526 .maps
1527 .bss_data
1528 .as_ref()
1529 .expect("BUG: bss_data missing after scheduler load")
1530 .applied_configuration_seq as *const u32;
1531 (ptr as *const std::sync::atomic::AtomicU32)
1532 .as_ref()
1533 .expect("BUG: applied_configuration_seq pointer cast yielded null")
1534 .load(std::sync::atomic::Ordering::Acquire)
1535 };
1536 if self
1537 .last_configuration_seq
1538 .is_some_and(|seq| applied_configuration == seq)
1539 {
1540 return Ok(());
1541 }
1542 let mut cell_to_cpus: HashMap<u32, Cpumask> = HashMap::new();
1544 let cpu_ctxs =
1545 read_cpu_ctxs(&self.skel).context("reading per-CPU contexts for BPF cell refresh")?;
1546 for (i, cpu_ctx) in cpu_ctxs.iter().enumerate() {
1547 cell_to_cpus
1548 .entry(cpu_ctx.cell)
1549 .or_insert_with(|| Cpumask::new())
1550 .set_cpu(i)
1551 .expect("set cpu in existing mask");
1552 }
1553
1554 let cells_with_cpus: HashSet<u32> = cell_to_cpus.keys().copied().collect();
1565 let mut active_cells = cells_with_cpus.clone();
1566 active_cells.insert(0);
1567
1568 for cell_idx in &active_cells {
1569 let cpus = cell_to_cpus
1570 .get(cell_idx)
1571 .cloned()
1572 .unwrap_or_else(|| Cpumask::new());
1573 self.cells.entry(*cell_idx).or_insert_with(Cell::new).cpus = cpus;
1574 self.metrics.cells.insert(*cell_idx, CellMetrics::default());
1575 }
1576
1577 self.cells.retain(|&k, _| active_cells.contains(&k));
1579 self.metrics.cells.retain(|&k, _| active_cells.contains(&k));
1580
1581 self.refresh_bpf_subcells(&active_cells)?;
1582
1583 self.last_configuration_seq = Some(applied_configuration);
1584
1585 Ok(())
1586 }
1587}
1588
1589fn validate_subcell_assignments(
1590 cell: &CpuAssignment,
1591 subcells: &[CpuAssignment],
1592 enable_borrowing: bool,
1593) -> Result<()> {
1594 if subcells.is_empty() {
1595 bail!("Cell {} has no subcell assignments", cell.id);
1596 }
1597 if subcells.len() > MAX_SUBCELLS_PER_CELL {
1598 bail!(
1599 "Cell {} has too many subcells: {} > MAX_SUBCELLS_PER_CELL ({})",
1600 cell.id,
1601 subcells.len(),
1602 MAX_SUBCELLS_PER_CELL
1603 );
1604 }
1605
1606 let mut seen = [false; MAX_SUBCELLS_PER_CELL];
1607 let mut primary_union = Cpumask::new();
1608
1609 for subcell in subcells {
1610 if subcell.id >= bpf_intf::consts_MAX_SUBCELLS_PER_CELL {
1611 bail!(
1612 "Subcell ID {} for cell {} exceeds MAX_SUBCELLS_PER_CELL ({})",
1613 subcell.id,
1614 cell.id,
1615 bpf_intf::consts_MAX_SUBCELLS_PER_CELL
1616 );
1617 }
1618
1619 let slot = subcell.id as usize;
1620 if seen[slot] {
1621 bail!("Duplicate subcell ID {} for cell {}", subcell.id, cell.id);
1622 }
1623 seen[slot] = true;
1624
1625 if subcell.primary.weight() == 0 {
1626 bail!(
1627 "Subcell {} in cell {} has no primary CPUs",
1628 subcell.id,
1629 cell.id
1630 );
1631 }
1632 if subcell.primary.and(&cell.primary.not()).weight() != 0 {
1633 bail!(
1634 "Subcell {} primary CPUs are outside cell {}",
1635 subcell.id,
1636 cell.id
1637 );
1638 }
1639 if primary_union.and(&subcell.primary).weight() != 0 {
1640 bail!(
1641 "Subcell {} overlaps another subcell in cell {}",
1642 subcell.id,
1643 cell.id
1644 );
1645 }
1646
1647 if enable_borrowing {
1648 let expected = cell.primary.and(&subcell.primary.not());
1649 let borrowable = subcell.borrowable.as_ref().ok_or_else(|| {
1650 anyhow::anyhow!(
1651 "Subcell {} in cell {} is missing its borrowable mask",
1652 subcell.id,
1653 cell.id
1654 )
1655 })?;
1656 if borrowable != &expected {
1657 bail!(
1658 "Subcell {} in cell {} has an invalid borrowable mask",
1659 subcell.id,
1660 cell.id
1661 );
1662 }
1663 } else if subcell.borrowable.is_some() {
1664 bail!(
1665 "Subcell {} in cell {} has borrowing disabled but a borrowable mask",
1666 subcell.id,
1667 cell.id
1668 );
1669 }
1670
1671 primary_union = primary_union.or(&subcell.primary);
1672 }
1673
1674 if primary_union != cell.primary {
1675 bail!("Subcell primary CPUs do not cover cell {}", cell.id);
1676 }
1677
1678 Ok(())
1679}
1680
1681fn write_cpumask_to_config(cpumask: &Cpumask, dest: &mut [u8]) {
1682 let raw_slice = cpumask.as_raw_slice();
1683 for (word_idx, word) in raw_slice.iter().enumerate() {
1684 let byte_start = word_idx * 8;
1685 let bytes = word.to_le_bytes();
1686 for (j, byte) in bytes.iter().enumerate() {
1687 let idx = byte_start + j;
1688 if idx < dest.len() {
1689 dest[idx] = *byte;
1690 }
1691 }
1692 }
1693}
1694
1695fn read_cpumask_from_bytes(src: &[u8]) -> Result<Cpumask> {
1696 let mut cpumask = Cpumask::new();
1697
1698 for (byte_idx, byte) in src.iter().copied().enumerate() {
1699 let mut bits = byte;
1700 while bits != 0 {
1701 let bit = bits.trailing_zeros() as usize;
1702 bits &= !(1u8 << bit);
1703 cpumask.set_cpu((byte_idx * 8) + bit)?;
1704 }
1705 }
1706
1707 Ok(cpumask)
1708}
1709
1710fn read_bpf_cell(skel: &BpfSkel, cell_id: u32) -> Result<bpf_intf::cell> {
1711 let raw = skel
1712 .maps
1713 .cells
1714 .lookup(&cell_id.to_ne_bytes(), libbpf_rs::MapFlags::ANY)
1715 .with_context(|| format!("Failed to lookup cell {}", cell_id))?
1716 .ok_or_else(|| anyhow::anyhow!("Cell {} missing from BPF map", cell_id))?;
1717
1718 if raw.len() != std::mem::size_of::<bpf_intf::cell>() {
1719 bail!(
1720 "Unexpected cell {} value size: {} != {}",
1721 cell_id,
1722 raw.len(),
1723 std::mem::size_of::<bpf_intf::cell>()
1724 );
1725 }
1726
1727 Ok(unsafe { std::ptr::read_unaligned(raw.as_ptr() as *const bpf_intf::cell) })
1728}
1729
1730fn read_cpu_ctxs(skel: &BpfSkel) -> Result<Vec<bpf_intf::cpu_ctx>> {
1731 let mut cpu_ctxs = vec![];
1732 let cpu_ctxs_vec = skel
1733 .maps
1734 .cpu_ctxs
1735 .lookup_percpu(&0u32.to_ne_bytes(), libbpf_rs::MapFlags::ANY)
1736 .context("Failed to lookup cpu_ctx")?
1737 .expect("BUG: cpu_ctxs lookup_percpu returned None for key 0");
1738 if cpu_ctxs_vec.len() < *NR_CPUS_POSSIBLE {
1739 bail!(
1740 "Percpu map returned {} entries but expected {}",
1741 cpu_ctxs_vec.len(),
1742 *NR_CPUS_POSSIBLE
1743 );
1744 }
1745 for cpu in 0..*NR_CPUS_POSSIBLE {
1746 cpu_ctxs.push(*unsafe {
1747 &*(cpu_ctxs_vec[cpu].as_slice().as_ptr() as *const bpf_intf::cpu_ctx)
1748 });
1749 }
1750 Ok(cpu_ctxs)
1751}
1752
1753fn run(opts: Opts) -> Result<()> {
1754 if opts.version {
1755 println!(
1756 "scx_mitosis {}",
1757 build_id::full_version(env!("CARGO_PKG_VERSION"))
1758 );
1759 return Ok(());
1760 }
1761
1762 let env_filter = EnvFilter::try_from_default_env()
1763 .or_else(|_| match EnvFilter::try_new(&opts.log_level) {
1764 Ok(filter) => Ok(filter),
1765 Err(e) => {
1766 eprintln!(
1767 "invalid log envvar: {}, using info, err is: {}",
1768 opts.log_level, e
1769 );
1770 EnvFilter::try_new("info")
1771 }
1772 })
1773 .unwrap_or_else(|_| EnvFilter::new("info"));
1774
1775 match tracing_subscriber::fmt()
1776 .with_env_filter(env_filter)
1777 .with_target(true)
1778 .with_thread_ids(true)
1779 .with_file(true)
1780 .with_line_number(true)
1781 .try_init()
1782 {
1783 Ok(()) => {}
1784 Err(e) => eprintln!("failed to init logger: {}", e),
1785 }
1786
1787 if opts.verbose > 0 {
1788 warn!("Setting verbose via -v is deprecated and will be an error in future releases.");
1789 }
1790
1791 debug!("opts={:?}", &opts);
1792
1793 if let Some(run_id) = opts.run_id {
1794 info!("scx_mitosis run_id: {}", run_id);
1795 }
1796
1797 let shutdown = Arc::new(AtomicBool::new(false));
1798 let shutdown_clone = shutdown.clone();
1799 ctrlc::set_handler(move || {
1800 shutdown_clone.store(true, Ordering::Relaxed);
1801 })
1802 .context("Error setting Ctrl-C handler")?;
1803
1804 if let Some(intv) = opts.monitor {
1805 let shutdown_clone = shutdown.clone();
1806 let jh = std::thread::spawn(move || {
1807 match stats::monitor(Duration::from_secs_f64(intv), shutdown_clone) {
1808 Ok(_) => {
1809 debug!("stats monitor thread finished successfully")
1810 }
1811 Err(error_object) => {
1812 warn!(
1813 "stats monitor thread finished because of an error {}",
1814 error_object
1815 )
1816 }
1817 }
1818 });
1819 if opts.monitor.is_some() {
1820 let _ = jh.join();
1821 return Ok(());
1822 }
1823 }
1824
1825 let mut open_object = MaybeUninit::uninit();
1826 loop {
1827 let mut sched =
1828 Scheduler::init(&opts, &mut open_object).context("initializing scheduler")?;
1829 if !sched
1830 .run(shutdown.clone())
1831 .context("running scheduler main loop")?
1832 .should_restart()
1833 {
1834 break;
1835 }
1836 }
1837
1838 Ok(())
1839}
1840
1841fn main() -> Result<()> {
1842 let parsed = undefok_flags::parse_args::<Opts>()?;
1843 for undefok in &parsed.ignored_undefok_flags {
1845 eprintln!("warning: ignoring undefok flag --{}", undefok.long);
1846 }
1847 run(parsed.opts)
1848}
1849
1850#[cfg(test)]
1851mod tests {
1852 use super::{validate_subcell_assignments, CpuAssignment, Cpumask, Opts};
1853 use clap::Parser;
1854
1855 fn cpumask(cpus: &[usize]) -> Cpumask {
1856 let mut mask = Cpumask::new();
1857 for &cpu in cpus {
1858 mask.set_cpu(cpu).unwrap();
1859 }
1860 mask
1861 }
1862
1863 fn assignment(id: u32, primary: &[usize], borrowable: Option<&[usize]>) -> CpuAssignment {
1864 CpuAssignment {
1865 id,
1866 primary: cpumask(primary),
1867 borrowable: borrowable.map(cpumask),
1868 }
1869 }
1870
1871 #[test]
1872 fn requires_cell_parent_cgroup_for_scheduler_mode() {
1873 assert!(Opts::try_parse_from(["scx_mitosis"]).is_err());
1874 }
1875
1876 #[test]
1877 fn allows_monitor_without_cell_parent_cgroup() {
1878 assert!(Opts::try_parse_from(["scx_mitosis", "--monitor", "1"]).is_ok());
1879 }
1880
1881 #[test]
1882 fn allows_version_without_cell_parent_cgroup() {
1883 assert!(Opts::try_parse_from(["scx_mitosis", "--version"]).is_ok());
1884 }
1885
1886 #[test]
1887 fn accepts_complete_unpinned_subcell_partition() {
1888 let cell = assignment(3, &[0, 1, 2, 3], None);
1889 let subcells = [
1890 assignment(0, &[0, 1], Some(&[2, 3])),
1891 assignment(1, &[2, 3], Some(&[0, 1])),
1892 ];
1893
1894 validate_subcell_assignments(&cell, &subcells, true).unwrap();
1895 }
1896
1897 #[test]
1898 fn rejects_overlapping_subcell_primaries() {
1899 let cell = assignment(3, &[0, 1, 2, 3], None);
1900 let subcells = [
1901 assignment(0, &[0, 1, 2], None),
1902 assignment(1, &[2, 3], None),
1903 ];
1904
1905 assert!(validate_subcell_assignments(&cell, &subcells, false).is_err());
1906 }
1907
1908 #[test]
1909 fn rejects_incomplete_subcell_partition() {
1910 let cell = assignment(3, &[0, 1, 2, 3], None);
1911 let subcells = [assignment(0, &[0, 1, 2], None)];
1912
1913 assert!(validate_subcell_assignments(&cell, &subcells, false).is_err());
1914 }
1915}