1#[allow(clippy::unwrap_used)]
7mod bpf_skel;
8pub use bpf_skel::*;
9pub mod bpf_intf;
10mod cell_manager;
11mod config;
12mod stats;
13mod topology;
14mod undefok_flags;
15
16use cell_manager::{CellManager, CpuAssignment, CpuRecipient};
17use config::{ConfiguredCells, SubcellMatch};
18
19use std::cmp::max;
20use std::collections::{HashMap, HashSet};
21use std::fmt;
22use std::fmt::Display;
23use std::mem::MaybeUninit;
24use std::os::fd::AsFd;
25use std::path::PathBuf;
26use std::sync::Arc;
27use std::sync::atomic::AtomicBool;
28use std::sync::atomic::AtomicU32;
29use std::sync::atomic::Ordering;
30use std::time::Duration;
31use std::time::Instant;
32
33use anyhow::Context;
34use anyhow::Result;
35use anyhow::bail;
36use clap::Parser;
37use libbpf_rs::MapCore as _;
38use libbpf_rs::OpenObject;
39use libbpf_rs::ProgramInput;
40use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags, EpollTimeout};
41use nix::sys::eventfd::EventFd;
42use scx_stats::prelude::*;
43use scx_utils::Cpumask;
44use scx_utils::NR_CPUS_POSSIBLE;
45use scx_utils::Topology;
46use scx_utils::TopologyArgs;
47use scx_utils::UserExitInfo;
48use scx_utils::build_id;
49use scx_utils::compat;
50use scx_utils::init_libbpf_logging;
51use scx_utils::libbpf_clap_opts::LibbpfOpts;
52use scx_utils::scx_enums;
53use scx_utils::scx_ops_attach;
54use scx_utils::scx_ops_load;
55use scx_utils::scx_ops_open;
56use scx_utils::uei_exited;
57use scx_utils::uei_report;
58use tracing::{debug, info, trace, warn};
59use tracing_subscriber::filter::EnvFilter;
60
61use stats::CellMetrics;
62use stats::Metrics;
63use topology::MitosisTopology;
64
65const SCHEDULER_NAME: &str = "scx_mitosis";
66const MAX_CELLS: usize = bpf_intf::consts_MAX_CELLS as usize;
67const MAX_SUBCELLS_PER_CELL: usize = bpf_intf::consts_MAX_SUBCELLS_PER_CELL as usize;
68const NR_CSTATS: usize = bpf_intf::cell_stat_idx_NR_CSTATS as usize;
69const SHARE_SIBLING_SUBCELL_CPUS: bool = true;
70const DEMAND_ONE: f64 = (1u64 << bpf_intf::consts_DEMAND_SHIFT) as f64;
72const INOTIFY_TOKEN: u64 = 1;
74const STATS_TOKEN: u64 = 2;
76
77fn parse_ewma_factor(s: &str) -> Result<f64, String> {
78 let v: f64 = s.parse().map_err(|e| format!("{e}"))?;
79 if !(0.0..=1.0).contains(&v) {
80 return Err(format!("value {v} not in range 0.0..=1.0"));
81 }
82 Ok(v)
83}
84
85#[derive(Debug, Parser)]
93#[command(after_long_help = r#"VIRTUAL LLC CONFIGURATION:
94 --virt-llc requires --enable-llc-awareness to affect DSQ creation.
95 MIN-MAX specifies the target range of physical cores per virtual LLC.
96 For each physical LLC and core type, the smallest exact divisor in the
97 range is preferred; otherwise, the size with the smallest remainder is
98 used, favoring smaller sizes. Remaining cores join the last partition.
99
100 Example: On a 32-core physical LLC, --virt-llc=8-8 creates four virtual
101 LLCs, and therefore four LLC DSQs per cell."#)]
102struct Opts {
103 #[clap(short = 'v', long, action = clap::ArgAction::Count)]
105 verbose: u8,
106
107 #[clap(long, default_value = "info")]
110 log_level: String,
111
112 #[clap(long, default_value = "0")]
114 exit_dump_len: u32,
115
116 #[clap(long, default_value = "1")]
118 monitor_interval_s: u64,
119
120 #[clap(long, conflicts_with = "monitor")]
123 monitor_sync: Option<f64>,
124
125 #[clap(long, conflicts_with = "monitor_sync")]
128 monitor: Option<f64>,
129
130 #[clap(short = 'V', long, action = clap::ArgAction::SetTrue)]
132 version: bool,
133
134 #[clap(long)]
136 run_id: Option<u64>,
137
138 #[clap(long, value_delimiter = ',')]
142 undefok: Vec<String>,
143
144 #[clap(long, default_value = "true", action = clap::ArgAction::Set)]
148 exiting_task_workaround: bool,
149
150 #[allow(dead_code)]
152 #[clap(long, action = clap::ArgAction::SetTrue)]
153 cpu_controller_disabled: bool,
154
155 #[clap(long, action = clap::ArgAction::SetTrue)]
158 reject_multicpu_pinning: bool,
159
160 #[clap(long, action = clap::ArgAction::SetTrue)]
163 enable_llc_awareness: bool,
164
165 #[clap(long, action = clap::ArgAction::SetTrue)]
167 enable_work_stealing: bool,
168
169 #[clap(
173 long,
174 required_unless_present_any = ["monitor", "version", "cell_config"],
175 conflicts_with = "cell_config"
176 )]
177 cell_parent_cgroup: Option<String>,
178
179 #[clap(long, value_name = "PATH")]
182 cell_config: Option<PathBuf>,
183
184 #[clap(long, default_value_t = 10)]
186 reconfiguration_interval_s: u64,
187
188 #[clap(long)]
193 cell_exclude: Vec<String>,
194
195 #[clap(long, default_value_t = 0)]
202 cell0_min_cpus: usize,
203
204 #[clap(long, action = clap::ArgAction::SetTrue)]
207 enable_borrowing: bool,
208
209 #[clap(long, action = clap::ArgAction::SetTrue)]
211 use_lockless_peek: bool,
212
213 #[clap(long, action = clap::ArgAction::SetTrue)]
215 enable_rebalancing: bool,
216
217 #[clap(long, default_value = "20.0")]
219 rebalance_threshold: f64,
220
221 #[clap(long, default_value = "5")]
223 rebalance_cooldown_s: u64,
224
225 #[clap(long, default_value = "0.3", value_parser = parse_ewma_factor)]
227 demand_smoothing: f64,
228
229 #[clap(long, action = clap::ArgAction::SetTrue)]
233 dynamic_affinity_cpu_selection: bool,
234
235 #[clap(long, action = clap::ArgAction::SetTrue)]
238 enable_slice_shrinking: bool,
239
240 #[clap(long, default_value = "4000")]
243 slice_shrink_max_us: u64,
244
245 #[clap(long, default_value = "500")]
249 slice_shrink_min_us: u64,
250
251 #[clap(flatten, next_help_heading = "Topology Options")]
252 topology: TopologyArgs,
253
254 #[clap(flatten, next_help_heading = "Libbpf Options")]
255 pub libbpf: LibbpfOpts,
256}
257
258const QUEUE_STATS_IDX: [bpf_intf::cell_stat_idx; 4] = [
263 bpf_intf::cell_stat_idx_CSTAT_LOCAL,
264 bpf_intf::cell_stat_idx_CSTAT_CPU_DSQ,
265 bpf_intf::cell_stat_idx_CSTAT_CELL_DSQ,
266 bpf_intf::cell_stat_idx_CSTAT_BORROWED,
267];
268
269#[derive(Debug)]
271struct Cell {
272 cpus: Cpumask,
273 subcells: Vec<Subcell>,
274}
275
276#[derive(Debug, Clone, PartialEq, Eq)]
277struct Subcell {
278 id: u32,
279 name: String,
280 primary: Cpumask,
281 borrowable: Option<Cpumask>,
282 matches: Vec<Vec<SubcellMatch>>,
283}
284
285impl Cell {
286 fn new() -> Self {
287 Self {
288 cpus: Cpumask::new(),
289 subcells: vec![Subcell::new(0)],
290 }
291 }
292}
293
294impl Subcell {
295 fn new(id: u32) -> Self {
296 Self {
297 id,
298 name: default_subcell_name(id),
299 primary: Cpumask::new(),
300 borrowable: None,
301 matches: Vec::new(),
302 }
303 }
304}
305
306enum CellSource {
308 Parent(CellManager),
311 Configured(ConfiguredCells),
313}
314
315struct Scheduler<'a> {
316 skel: BpfSkel<'a>,
317 monitor_interval: Duration,
318 cells: HashMap<u32, Cell>,
319 prev_cell_stats: [[u64; NR_CSTATS]; MAX_CELLS],
322 prev_cell_running_ns: [u64; MAX_CELLS],
324 prev_cell_own_ns: [u64; MAX_CELLS],
325 prev_cell_lent_ns: [u64; MAX_CELLS],
326 demand_sampled_at: Option<Instant>,
328 prev_subcell_running_ns: [[u64; MAX_SUBCELLS_PER_CELL]; MAX_CELLS],
329 prev_subcell_own_ns: [[u64; MAX_SUBCELLS_PER_CELL]; MAX_CELLS],
330 prev_subcell_lent_ns: [[u64; MAX_SUBCELLS_PER_CELL]; MAX_CELLS],
331 prev_subcell_queued_ns: [[u64; MAX_SUBCELLS_PER_CELL]; MAX_CELLS],
332 prev_subcell_demand_sum: [[u64; MAX_SUBCELLS_PER_CELL]; MAX_CELLS],
333 metrics: Metrics,
334 stats_server: Option<StatsServer<(), Metrics>>,
335 last_configuration_seq: Option<u32>,
336 last_cpuset_seq: u32,
338 cell_source: CellSource,
340 cpu_to_llc: HashMap<usize, usize>,
342 cell0_min_cpus: usize,
344 configured_enforced_holdout: bool,
346 enable_borrowing: bool,
348 enable_rebalancing: bool,
350 rebalance_threshold: f64,
352 rebalance_cooldown: Duration,
354 demand_smoothing: f64,
356 smoothed_util: [f64; MAX_CELLS],
358 smoothed_subcell_util: [[f64; MAX_SUBCELLS_PER_CELL]; MAX_CELLS],
360 smoothed_subcell_load: [[f64; MAX_SUBCELLS_PER_CELL]; MAX_CELLS],
362 smoothed_subcell_demand: [[f64; MAX_SUBCELLS_PER_CELL]; MAX_CELLS],
364 last_rebalance: Instant,
366 rebalance_count: u64,
368 epoll: Epoll,
370 stats_waker: EventFd,
372 reconfiguration_interval: Duration,
374 last_config_refresh: Instant,
376}
377
378struct DistributionStats {
379 total_decisions: u64,
380 share_of_decisions_pct: f64,
381 local_q_pct: f64,
382 cpu_q_pct: f64,
383 cell_q_pct: f64,
384 borrowed_pct: f64,
385 affn_viol_pct: f64,
386 steal_pct: f64,
387 pin_skip_pct: f64,
388
389 global_queue_decisions: u64,
391}
392
393impl Display for DistributionStats {
394 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
395 const MIN_DECISIONS_WIDTH: usize = 5;
399 let descisions_width = if self.global_queue_decisions > 0 {
400 max(
401 MIN_DECISIONS_WIDTH,
402 (self.global_queue_decisions as f64).log10().ceil() as usize,
403 )
404 } else {
405 MIN_DECISIONS_WIDTH
406 };
407 write!(
408 f,
409 "{: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}%",
410 self.total_decisions,
411 self.share_of_decisions_pct,
412 self.local_q_pct,
413 self.cpu_q_pct,
414 self.cell_q_pct,
415 self.borrowed_pct,
416 self.affn_viol_pct,
417 self.steal_pct,
418 self.pin_skip_pct,
419 width = descisions_width,
420 )
421 }
422}
423
424impl<'a> Scheduler<'a> {
425 fn validate_args(opts: &Opts) -> Result<()> {
426 if opts.cell_parent_cgroup.is_some() && opts.cell_config.is_some() {
427 bail!("--cell-parent-cgroup and --cell-config are mutually exclusive");
428 }
429 if opts.cell_parent_cgroup.is_none() && opts.cell_config.is_none() {
430 bail!("either --cell-parent-cgroup or --cell-config is required");
431 }
432 if !opts.cell_exclude.is_empty() && opts.cell_parent_cgroup.is_none() {
433 bail!("--cell-exclude requires --cell-parent-cgroup");
434 }
435 Ok(())
436 }
437
438 fn init(opts: &Opts, open_object: &'a mut MaybeUninit<OpenObject>) -> Result<Self> {
439 Self::validate_args(opts).context("validating scheduler options")?;
440
441 let topology = Topology::with_args(&opts.topology).context("detecting system topology")?;
442
443 let nr_llc = topology.all_llcs.len().max(1);
444
445 let mut skel_builder = BpfSkelBuilder::default();
446 skel_builder
447 .obj_builder
448 .debug(opts.log_level.contains("trace"));
449 init_libbpf_logging(None);
450 info!(
451 "Running scx_mitosis (build ID: {})",
452 build_id::full_version(env!("CARGO_PKG_VERSION"))
453 );
454
455 let open_opts = opts.libbpf.clone().into_bpf_open_opts();
456 let mut skel = scx_ops_open!(skel_builder, open_object, mitosis, open_opts)
457 .context("opening BPF skeleton")?;
458
459 skel.struct_ops.mitosis_mut().exit_dump_len = opts.exit_dump_len;
460
461 let rodata = skel
462 .maps
463 .rodata_data
464 .as_mut()
465 .expect("BUG: rodata_data missing after skel open");
466
467 rodata.slice_ns = scx_enums.SCX_SLICE_DFL;
468 rodata.exiting_task_workaround_enabled = opts.exiting_task_workaround;
469 rodata.dynamic_affinity_cpu_selection = opts.dynamic_affinity_cpu_selection;
470
471 if opts.slice_shrink_min_us >= opts.slice_shrink_max_us {
473 bail!(
474 "--slice-shrink-min-us ({}) must be less than --slice-shrink-max-us ({})",
475 opts.slice_shrink_min_us,
476 opts.slice_shrink_max_us
477 );
478 }
479 rodata.enable_slice_shrinking = opts.enable_slice_shrinking;
480 rodata.slice_shrink_max_ns = opts.slice_shrink_max_us * 1_000;
481 rodata.slice_shrink_multiplier = 2;
483 rodata.slice_shrink_min_ns = opts.slice_shrink_min_us * 1_000;
484
485 rodata.nr_possible_cpus = *NR_CPUS_POSSIBLE as u32;
486 for cpu in topology.all_cpus.keys() {
487 rodata.all_cpus[cpu / 8] |= 1 << (cpu % 8);
488 }
489
490 rodata.reject_multicpu_pinning = opts.reject_multicpu_pinning;
491
492 rodata.nr_llc = nr_llc as u32;
494 rodata.enable_llc_awareness = opts.enable_llc_awareness;
495
496 rodata.enable_borrowing = opts.enable_borrowing;
497 rodata.use_lockless_peek = opts.use_lockless_peek;
498
499 match *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP {
500 0 => info!("Kernel does not support queued wakeup optimization."),
501 v => skel.struct_ops.mitosis_mut().flags |= v,
502 }
503
504 let mitosis_topology = MitosisTopology::new(&topology);
505 mitosis_topology
506 .apply(&mut skel)
507 .context("mitosis_topology.apply")?;
508
509 let skel = scx_ops_load!(skel, mitosis, uei).context("loading BPF skeleton")?;
510
511 let stats_server = StatsServer::new(stats::server_data())
512 .launch()
513 .context("launching stats server")?;
514
515 let cpu_to_llc: HashMap<usize, usize> = mitosis_topology.cpu_to_llc.into_iter().collect();
516 let cell_source = if let Some(ref parent_cgroup) = opts.cell_parent_cgroup {
517 let exclude: HashSet<String> = opts.cell_exclude.iter().cloned().collect();
518 CellSource::Parent(
519 CellManager::new(
520 parent_cgroup,
521 MAX_CELLS as u32,
522 topology.span.clone(),
523 exclude,
524 opts.cell0_min_cpus,
525 cpu_to_llc.clone(),
526 )
527 .with_context(|| {
528 format!("initializing cell manager for cgroup {}", parent_cgroup)
529 })?,
530 )
531 } else if let Some(ref cell_config) = opts.cell_config {
532 CellSource::Configured(
533 ConfiguredCells::load(cell_config, MAX_CELLS as u32, topology.span.clone())
534 .with_context(|| {
535 format!(
536 "initializing configured cells from {}",
537 cell_config.display()
538 )
539 })?,
540 )
541 } else {
542 bail!("either --cell-parent-cgroup or --cell-config is required");
543 };
544
545 let epoll = Epoll::new(EpollCreateFlags::empty()).context("creating epoll instance")?;
547
548 let stats_waker = EventFd::from_value_and_flags(
550 0,
551 nix::sys::eventfd::EfdFlags::EFD_NONBLOCK | nix::sys::eventfd::EfdFlags::EFD_SEMAPHORE,
552 )
553 .context("creating stats-waker eventfd")?;
554
555 epoll
557 .add(
558 &stats_waker,
559 EpollEvent::new(EpollFlags::EPOLLIN, STATS_TOKEN),
560 )
561 .context("registering stats-waker with epoll")?;
562
563 match &cell_source {
564 CellSource::Parent(cell_manager) => {
565 epoll
566 .add(
567 cell_manager,
568 EpollEvent::new(EpollFlags::EPOLLIN, INOTIFY_TOKEN),
569 )
570 .context("registering cell manager inotify with epoll")?;
571 }
572 CellSource::Configured(_) => {}
573 }
574
575 Ok(Self {
576 skel,
577 monitor_interval: Duration::from_secs(opts.monitor_interval_s),
578 cells: HashMap::new(),
579 prev_cell_stats: [[0; NR_CSTATS]; MAX_CELLS],
580 prev_cell_running_ns: [0; MAX_CELLS],
581 prev_cell_own_ns: [0; MAX_CELLS],
582 prev_cell_lent_ns: [0; MAX_CELLS],
583 demand_sampled_at: None,
584 prev_subcell_running_ns: [[0; MAX_SUBCELLS_PER_CELL]; MAX_CELLS],
585 prev_subcell_own_ns: [[0; MAX_SUBCELLS_PER_CELL]; MAX_CELLS],
586 prev_subcell_lent_ns: [[0; MAX_SUBCELLS_PER_CELL]; MAX_CELLS],
587 prev_subcell_queued_ns: [[0; MAX_SUBCELLS_PER_CELL]; MAX_CELLS],
588 prev_subcell_demand_sum: [[0; MAX_SUBCELLS_PER_CELL]; MAX_CELLS],
589 metrics: Metrics::default(),
590 stats_server: Some(stats_server),
591 last_configuration_seq: None,
592 last_cpuset_seq: 0,
593 cell_source,
594 cpu_to_llc,
595 cell0_min_cpus: opts.cell0_min_cpus,
596 configured_enforced_holdout: false,
597 enable_borrowing: opts.enable_borrowing,
598 enable_rebalancing: opts.enable_rebalancing,
599 rebalance_threshold: opts.rebalance_threshold,
600 rebalance_cooldown: Duration::from_secs(opts.rebalance_cooldown_s),
601 demand_smoothing: opts.demand_smoothing,
602 smoothed_util: [0.0; MAX_CELLS],
603 smoothed_subcell_util: [[0.0; MAX_SUBCELLS_PER_CELL]; MAX_CELLS],
604 smoothed_subcell_load: [[0.0; MAX_SUBCELLS_PER_CELL]; MAX_CELLS],
605 smoothed_subcell_demand: [[0.0; MAX_SUBCELLS_PER_CELL]; MAX_CELLS],
606 last_rebalance: Instant::now(),
607 rebalance_count: 0,
608 epoll,
609 stats_waker,
610 reconfiguration_interval: Duration::from_secs(opts.reconfiguration_interval_s),
611 last_config_refresh: Instant::now(),
612 })
613 }
614
615 fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
616 let struct_ops = scx_ops_attach!(self.skel, mitosis).context("attaching BPF scheduler")?;
617
618 info!("Mitosis Scheduler Attached. Run `scx_mitosis --monitor` for metrics.");
619
620 self.apply_initial_cells()
622 .context("applying initial cell configuration")?;
623
624 let (res_ch, req_ch) = self
625 .stats_server
626 .as_ref()
627 .expect("BUG: stats_server missing after init")
628 .channels();
629
630 let stats_waker_fd = self
634 .stats_waker
635 .as_fd()
636 .try_clone_to_owned()
637 .context("cloning stats-waker fd for bridge thread")?;
638 let stats_waker = unsafe { EventFd::from_owned_fd(stats_waker_fd) };
639 let stats_bridge = std::thread::spawn(move || {
640 while req_ch.recv().is_ok() {
641 let _ = stats_waker.write(1);
643 }
644 });
645
646 while !shutdown.load(Ordering::Relaxed) && !uei_exited!(&self.skel, uei) {
647 let mut events = [EpollEvent::empty(); 1];
648 let timeout = EpollTimeout::try_from(self.monitor_interval).with_context(|| {
649 format!(
650 "monitor_interval {:?} exceeds maximum epoll timeout",
651 self.monitor_interval,
652 )
653 })?;
654
655 match self.epoll.wait(&mut events, timeout) {
656 Ok(n) => {
657 for event in &events[..n] {
658 match event.data() {
659 INOTIFY_TOKEN => {
660 self.process_cell_events()
662 .context("processing cell manager events")?;
663 }
664 STATS_TOKEN => {
665 let _ = self.stats_waker.read();
667 res_ch
668 .send(self.get_metrics())
669 .context("sending metrics response")?;
670 }
671 _ => {}
672 }
673 }
674 }
675 Err(nix::errno::Errno::EINTR) => continue,
676 Err(e) => return Err(e.into()),
677 }
678
679 self.refresh_bpf_cells()
681 .context("refreshing BPF cell state")?;
682 self.check_cpuset_changes()
683 .context("checking cpuset changes")?;
684 self.maybe_refresh_configured_cells()
685 .context("refreshing configured cells")?;
686 self.collect_metrics().context("collecting metrics")?;
687
688 if self.enable_rebalancing {
689 self.maybe_rebalance().context("running rebalance check")?;
690 }
691 }
692
693 drop(struct_ops);
694 drop(self.stats_server.take());
696 let _ = stats_bridge.join();
697 info!("Unregister {SCHEDULER_NAME} scheduler");
698 uei_report!(&self.skel, uei)
699 }
700
701 fn cell_manager(&self) -> Result<&CellManager> {
703 match &self.cell_source {
704 CellSource::Parent(cell_manager) => Ok(cell_manager),
705 CellSource::Configured(_) => {
706 bail!("BUG: cell manager requested in --cell-config mode")
707 }
708 }
709 }
710
711 fn cell_manager_mut(&mut self) -> Result<&mut CellManager> {
712 match &mut self.cell_source {
713 CellSource::Parent(cell_manager) => Ok(cell_manager),
714 CellSource::Configured(_) => {
715 bail!("BUG: cell manager requested in --cell-config mode")
716 }
717 }
718 }
719
720 fn configured_cells(&self) -> Result<&ConfiguredCells> {
722 match &self.cell_source {
723 CellSource::Parent(_) => {
724 bail!("BUG: configured cells requested in --cell-parent-cgroup mode")
725 }
726 CellSource::Configured(configured_cells) => Ok(configured_cells),
727 }
728 }
729
730 fn configured_cells_mut(&mut self) -> Result<&mut ConfiguredCells> {
731 match &mut self.cell_source {
732 CellSource::Parent(_) => {
733 bail!("BUG: configured cells requested in --cell-parent-cgroup mode")
734 }
735 CellSource::Configured(configured_cells) => Ok(configured_cells),
736 }
737 }
738
739 fn apply_initial_cells(&mut self) -> Result<()> {
741 match self.cell_source {
742 CellSource::Parent(_) => self.apply_initial_parent_cells(),
743 CellSource::Configured(_) => self.apply_initial_configured_cells(),
744 }
745 }
746
747 fn apply_initial_parent_cells(&mut self) -> Result<()> {
748 let cpu_assignments = self
749 .compute_and_apply_cell_config(&[])
750 .context("computing initial cell configuration")?;
751
752 info!(
753 "Applied initial cell configuration: {}",
754 self.cell_manager()?.format_cell_config(&cpu_assignments)
755 );
756
757 Ok(())
758 }
759
760 fn apply_initial_configured_cells(&mut self) -> Result<()> {
761 let cpu_assignments = self
762 .compute_and_apply_configured_cell_config()
763 .context("computing initial configured cell configuration")?;
764 self.last_config_refresh = Instant::now();
765
766 info!(
767 "Applied initial configured cell configuration: {}",
768 self.configured_cells()?
769 .format_cell_config(&cpu_assignments)
770 );
771
772 Ok(())
773 }
774
775 fn process_cell_events(&mut self) -> Result<()> {
777 let (num_new, num_destroyed, new_cell_ids, destroyed_cell_ids) = {
778 let (new_cells, destroyed_cells) = self
779 .cell_manager_mut()?
780 .process_events()
781 .context("processing inotify events")?;
782
783 if new_cells.is_empty() && destroyed_cells.is_empty() {
784 return Ok(());
785 }
786
787 let new_ids: Vec<u32> = new_cells.iter().map(|(_, cell_id)| *cell_id).collect();
788 (
789 new_cells.len(),
790 destroyed_cells.len(),
791 new_ids,
792 destroyed_cells,
793 )
794 };
795
796 for &cell_id in &destroyed_cell_ids {
799 self.smoothed_util[cell_id as usize] = 0.0;
800 self.smoothed_subcell_util[cell_id as usize] = [0.0; MAX_SUBCELLS_PER_CELL];
801 self.smoothed_subcell_load[cell_id as usize] = [0.0; MAX_SUBCELLS_PER_CELL];
802 self.smoothed_subcell_demand[cell_id as usize] = [0.0; MAX_SUBCELLS_PER_CELL];
803 }
804
805 let cpu_assignments = self
806 .compute_and_apply_cell_config(&new_cell_ids)
807 .context("recomputing cell configuration for new cgroups")?;
808
809 info!(
810 "Cell config updated ({} new, {} destroyed): {}",
811 num_new,
812 num_destroyed,
813 self.cell_manager()?.format_cell_config(&cpu_assignments)
814 );
815
816 Ok(())
817 }
818
819 fn compute_and_apply_cell_config(
828 &mut self,
829 new_cell_ids: &[u32],
830 ) -> Result<Vec<CpuAssignment>> {
831 let (cell_assignments, cpu_assignments) = {
832 let active_cell_ids: Vec<u32> = self
833 .cell_manager()?
834 .get_cell_assignments()
835 .iter()
836 .map(|(_, cell_id)| *cell_id)
837 .collect();
838 let all_cell_ids: Vec<u32> = std::iter::once(0)
840 .chain(active_cell_ids.iter().copied())
841 .collect();
842
843 let cpu_assignments = if self.enable_rebalancing {
844 let new_set: HashSet<u32> = new_cell_ids.iter().copied().collect();
846 let existing_utils: Vec<f64> = all_cell_ids
847 .iter()
848 .filter(|id| !new_set.contains(id))
849 .map(|&id| self.smoothed_util[id as usize])
850 .collect();
851
852 let has_data = existing_utils.iter().any(|&u| u > 0.0);
853
854 if has_data {
855 let avg_util: f64 =
857 existing_utils.iter().sum::<f64>() / existing_utils.len().max(1) as f64;
858 for &id in new_cell_ids {
859 self.smoothed_util[id as usize] = avg_util;
860 info!(
861 "Seeded new cell {} smoothed_util to average {:.1}%",
862 id, avg_util
863 );
864 }
865
866 let cell_demands: HashMap<u32, f64> = all_cell_ids
868 .iter()
869 .map(|&id| (id, self.smoothed_util[id as usize]))
870 .collect();
871
872 self.cell_manager()?
873 .compute_demand_cpu_assignments(&cell_demands, self.enable_borrowing)
874 .context("computing demand-weighted CPU assignments")?
875 } else {
876 self.cell_manager()?
878 .compute_cpu_assignments(self.enable_borrowing)
879 .context("computing equal-weight CPU assignments (no utilization data)")?
880 }
881 } else {
882 self.cell_manager()?
883 .compute_cpu_assignments(self.enable_borrowing)
884 .context("computing equal-weight CPU assignments (rebalancing disabled)")?
885 };
886
887 (self.cell_manager()?.get_cell_assignments(), cpu_assignments)
888 };
889
890 let subcell_assignments = self.compute_subcell_assignments(&cpu_assignments)?;
891
892 self.apply_cell_config(&cell_assignments, &cpu_assignments, &subcell_assignments)
893 .context("applying cell configuration to BPF")?;
894
895 Ok(cpu_assignments)
896 }
897
898 fn compute_and_apply_configured_cell_config(&mut self) -> Result<Vec<CpuAssignment>> {
900 let cell_demands = if self.enable_rebalancing {
901 Some(self.active_cell_demands())
902 } else {
903 None
904 };
905 let cell0_min_cpus = self.cell0_min_cpus;
906
907 let (resolution, all_cpus) = {
908 let configured_cells = self.configured_cells_mut()?;
909 let resolution = configured_cells
910 .resolve(cell_demands.as_ref(), cell0_min_cpus)
911 .context("resolving configured cells")?;
912 (resolution, configured_cells.all_cpus().clone())
913 };
914
915 for configured_cell in &resolution.cells {
916 let cell = self
917 .cells
918 .entry(configured_cell.id)
919 .or_insert_with(Cell::new);
920 let mut existing: HashMap<u32, Subcell> = std::mem::take(&mut cell.subcells)
924 .into_iter()
925 .map(|subcell| (subcell.id, subcell))
926 .collect();
927 cell.subcells = configured_cell
928 .subcells
929 .iter()
930 .map(|subcell| {
931 let (primary, borrowable) = existing
932 .remove(&subcell.id)
933 .map(|old| (old.primary, old.borrowable))
934 .unwrap_or((Cpumask::new(), None));
935 Subcell {
936 id: subcell.id,
937 name: subcell.name.clone(),
938 primary,
939 borrowable,
940 matches: subcell.matches.clone(),
941 }
942 })
943 .collect();
944 }
945
946 let (cpu_assignments, displaced_claim) =
947 CellManager::compute_configured_cell_cpu_assignments(
948 &all_cpus,
949 &resolution.cell_recipients,
950 self.enable_borrowing,
951 &self.cpu_to_llc,
952 )
953 .context("computing configured cell CPU assignments")?;
954 self.configured_enforced_holdout |= displaced_claim;
955 let subcell_assignments = self.compute_subcell_assignments(&cpu_assignments)?;
956
957 let cell_assignments = resolution.cell_assignments;
958
959 self.apply_cell_config(&cell_assignments, &cpu_assignments, &subcell_assignments)
960 .context("applying configured cell configuration to BPF")?;
961
962 Ok(cpu_assignments)
963 }
964
965 fn active_cell_demands(&self) -> HashMap<u32, f64> {
966 self.cells
967 .keys()
968 .map(|&id| (id, self.smoothed_util[id as usize]))
969 .collect()
970 }
971
972 fn maybe_rebalance(&mut self) -> Result<()> {
974 if self.last_rebalance.elapsed() < self.rebalance_cooldown {
976 return Ok(());
977 }
978
979 let active_cells: Vec<u32> = self.cells.keys().copied().collect();
981 if active_cells.len() < 2 {
982 return Ok(());
983 }
984
985 let mut min_util = f64::MAX;
986 let mut max_util = f64::MIN;
987 for &cell_id in &active_cells {
988 let util = self.smoothed_util[cell_id as usize];
989 if util < min_util {
990 min_util = util;
991 }
992 if util > max_util {
993 max_util = util;
994 }
995 }
996
997 let spread = max_util - min_util;
998 if spread < self.rebalance_threshold {
999 return Ok(());
1000 }
1001
1002 match self.cell_source {
1003 CellSource::Parent(_) => self.rebalance_parent_cells(spread, &active_cells),
1004 CellSource::Configured(_) => self.rebalance_configured_cells(spread),
1005 }
1006 }
1007
1008 fn rebalance_parent_cells(&mut self, spread: f64, active_cells: &[u32]) -> Result<()> {
1010 let cell_demands: HashMap<u32, f64> = active_cells
1012 .iter()
1013 .map(|&cell_id| (cell_id, self.smoothed_util[cell_id as usize]))
1014 .collect();
1015
1016 let (cell_assignments, cpu_assignments) = {
1018 let cpu_assignments = self
1019 .cell_manager()?
1020 .compute_demand_cpu_assignments(&cell_demands, self.enable_borrowing)
1021 .context("computing demand-weighted CPU assignments for rebalance")?;
1022
1023 let changed = cpu_assignments.iter().any(|a| {
1024 self.cells
1025 .get(&a.id)
1026 .is_none_or(|cell| cell.cpus != a.primary)
1027 });
1028
1029 if !changed {
1033 return Ok(());
1034 }
1035
1036 (self.cell_manager()?.get_cell_assignments(), cpu_assignments)
1037 };
1038
1039 let subcell_assignments = self.compute_subcell_assignments(&cpu_assignments)?;
1040
1041 self.apply_cell_config(&cell_assignments, &cpu_assignments, &subcell_assignments)
1042 .context("applying rebalanced cell configuration to BPF")?;
1043
1044 self.last_rebalance = Instant::now();
1045 self.rebalance_count += 1;
1046 self.metrics.rebalance_count = self.rebalance_count;
1047
1048 info!(
1049 "Rebalanced CPUs (spread={:.1}%, count={}): {}",
1050 spread,
1051 self.rebalance_count,
1052 self.cell_manager()?.format_cell_config(&cpu_assignments)
1053 );
1054
1055 Ok(())
1056 }
1057
1058 fn rebalance_configured_cells(&mut self, spread: f64) -> Result<()> {
1060 let cpu_assignments = self
1061 .compute_and_apply_configured_cell_config()
1062 .context("recomputing configured cell configuration for rebalance")?;
1063 self.last_rebalance = Instant::now();
1064 self.last_config_refresh = Instant::now();
1065 self.rebalance_count += 1;
1066 self.metrics.rebalance_count = self.rebalance_count;
1067
1068 info!(
1069 "Rebalanced configured cells (spread={:.1}%, count={}): {}",
1070 spread,
1071 self.rebalance_count,
1072 self.configured_cells()?
1073 .format_cell_config(&cpu_assignments)
1074 );
1075
1076 Ok(())
1077 }
1078
1079 fn compute_subcell_assignments(
1080 &self,
1081 cell_cpu_assignments: &[CpuAssignment],
1082 ) -> Result<Vec<Vec<CpuAssignment>>> {
1083 cell_cpu_assignments
1084 .iter()
1085 .map(|cell_assignment| {
1086 let recipients: Vec<CpuRecipient> = self
1087 .cells
1088 .get(&cell_assignment.id)
1089 .map(|cell| {
1090 let use_demand = self.enable_rebalancing
1091 && (cell_assignment.id as usize) < MAX_CELLS
1092 && cell.subcells.iter().any(|subcell| {
1093 (subcell.id as usize) < MAX_SUBCELLS_PER_CELL
1094 && self.smoothed_subcell_demand[cell_assignment.id as usize]
1095 [subcell.id as usize]
1096 > 0.0
1097 });
1098 cell.subcells
1099 .iter()
1100 .map(|subcell| {
1101 let weight = if use_demand
1102 && (subcell.id as usize) < MAX_SUBCELLS_PER_CELL
1103 {
1104 self.smoothed_subcell_demand[cell_assignment.id as usize]
1105 [subcell.id as usize]
1106 } else {
1107 1.0
1108 };
1109 CpuRecipient::unpinned(subcell.id, weight, &cell_assignment.primary)
1110 })
1111 .collect()
1112 })
1113 .unwrap_or_else(|| {
1114 vec![CpuRecipient::unpinned(0, 1.0, &cell_assignment.primary)]
1115 });
1116
1117 CellManager::compute_subcell_cpu_assignments(
1118 &cell_assignment.primary,
1119 &recipients,
1120 SHARE_SIBLING_SUBCELL_CPUS,
1121 &self.cpu_to_llc,
1122 )
1123 })
1124 .collect()
1125 }
1126
1127 fn refresh_bpf_subcells(&mut self, active_cells: &HashSet<u32>) -> Result<()> {
1128 for cell_id in active_cells {
1129 let bpf_cell = read_bpf_cell(&self.skel, *cell_id)?;
1130 let existing_matches: HashMap<u32, Vec<Vec<SubcellMatch>>> = self
1131 .cells
1132 .get(cell_id)
1133 .map(|cell| {
1134 cell.subcells
1135 .iter()
1136 .map(|subcell| (subcell.id, subcell.matches.clone()))
1137 .collect()
1138 })
1139 .unwrap_or_default();
1140 let existing_names: HashMap<u32, String> = self
1141 .cells
1142 .get(cell_id)
1143 .map(|cell| {
1144 cell.subcells
1145 .iter()
1146 .map(|subcell| (subcell.id, subcell.name.clone()))
1147 .collect()
1148 })
1149 .unwrap_or_default();
1150 let subcells: Vec<Subcell> = bpf_cell
1151 .subcells
1152 .iter()
1153 .filter(|subcell| subcell.in_use != 0)
1154 .map(|subcell| {
1155 Ok(Subcell {
1156 id: subcell.id,
1157 name: existing_names
1158 .get(&subcell.id)
1159 .cloned()
1160 .unwrap_or_else(|| default_subcell_name(subcell.id)),
1161 primary: read_cpumask_from_bytes(&subcell.primary.mask)?,
1162 borrowable: Some(read_cpumask_from_bytes(&subcell.borrowable.mask)?),
1163 matches: existing_matches
1164 .get(&subcell.id)
1165 .cloned()
1166 .unwrap_or_default(),
1167 })
1168 })
1169 .collect::<Result<_>>()?;
1170
1171 if subcells.is_empty() {
1172 bail!("Cell {} has no active subcells in BPF state", cell_id);
1173 }
1174
1175 let cell = self.cells.get_mut(cell_id).ok_or_else(|| {
1176 anyhow::anyhow!("Cell {} missing during subcell refresh", cell_id)
1177 })?;
1178 cell.subcells = subcells;
1179 }
1180
1181 Ok(())
1182 }
1183
1184 fn apply_cell_config(
1189 &mut self,
1190 cell_assignments: &[(u64, u32)],
1191 cpu_assignments: &[CpuAssignment],
1192 subcell_assignments: &[Vec<CpuAssignment>],
1193 ) -> Result<()> {
1194 let subcell_matches: HashMap<(u32, u32), Vec<Vec<SubcellMatch>>> = self
1195 .cells
1196 .iter()
1197 .flat_map(|(&cell_id, cell)| {
1198 cell.subcells
1199 .iter()
1200 .map(move |subcell| ((cell_id, subcell.id), subcell.matches.clone()))
1201 })
1202 .collect();
1203
1204 let bss_data = self
1205 .skel
1206 .maps
1207 .bss_data
1208 .as_mut()
1209 .expect("bss_data must be available after scheduler load");
1210
1211 let config = &mut bss_data.cell_config;
1212
1213 unsafe {
1221 std::ptr::write_bytes(
1222 config as *mut _ as *mut u8,
1223 0,
1224 std::mem::size_of_val(config),
1225 );
1226 }
1227
1228 if subcell_assignments.len() != cpu_assignments.len() {
1229 bail!(
1230 "Mismatched subcell assignments: {} cell assignments vs {} subcell groups",
1231 cpu_assignments.len(),
1232 subcell_assignments.len()
1233 );
1234 }
1235
1236 if cell_assignments.len() > bpf_intf::consts_MAX_CELLS as usize {
1237 bail!(
1238 "Too many cell assignments: {} > MAX_CELLS ({})",
1239 cell_assignments.len(),
1240 bpf_intf::consts_MAX_CELLS
1241 );
1242 }
1243 config.num_cell_assignments = cell_assignments.len() as u32;
1244
1245 for cell_id in 0..MAX_CELLS {
1246 for subcell_id in 0..MAX_SUBCELLS_PER_CELL {
1247 config.subcells[cell_id][subcell_id].id = subcell_id as u32;
1248 }
1249 }
1250
1251 for (i, (cgid, cell_id)) in cell_assignments.iter().enumerate() {
1252 config.assignments[i].cgid = *cgid;
1253 config.assignments[i].cell_id = *cell_id;
1254 }
1255
1256 let mut max_cell_id: u32 = 0;
1258 for (a, cell_subcells) in cpu_assignments.iter().zip(subcell_assignments.iter()) {
1259 validate_subcell_assignments(a, cell_subcells, SHARE_SIBLING_SUBCELL_CPUS)?;
1260
1261 if a.id >= bpf_intf::consts_MAX_CELLS {
1262 bail!(
1263 "Cell ID {} exceeds MAX_CELLS ({})",
1264 a.id,
1265 bpf_intf::consts_MAX_CELLS
1266 );
1267 }
1268 max_cell_id = max_cell_id.max(a.id + 1);
1269
1270 write_cpumask_to_config(&a.primary, &mut config.cpumasks[a.id as usize].mask);
1271
1272 if let Some(ref borrowable) = a.borrowable {
1273 write_cpumask_to_config(
1274 borrowable,
1275 &mut config.borrowable_cpumasks[a.id as usize].mask,
1276 );
1277 }
1278
1279 for subcell in cell_subcells {
1280 let slot = subcell.id as usize;
1281 config.subcells[a.id as usize][slot].id = subcell.id;
1282 config.subcells[a.id as usize][slot].in_use = 1;
1283 let matches = subcell_matches
1284 .get(&(a.id, subcell.id))
1285 .map_or(&[][..], |matches| matches.as_slice());
1286 write_subcell_matches(matches, &mut config.subcells[a.id as usize][slot])
1287 .with_context(|| {
1288 format!(
1289 "writing subcell matches for cell {} subcell {}",
1290 a.id, subcell.id
1291 )
1292 })?;
1293 write_cpumask_to_config(
1294 &subcell.primary,
1295 &mut config.subcells[a.id as usize][slot].primary.mask,
1296 );
1297 if let Some(ref borrowable) = subcell.borrowable {
1298 write_cpumask_to_config(
1299 borrowable,
1300 &mut config.subcells[a.id as usize][slot].borrowable.mask,
1301 );
1302 }
1303 }
1304 }
1305 config.num_cells = max_cell_id;
1306
1307 let prog = &mut self.skel.progs.apply_cell_config;
1309 let out = prog
1310 .test_run(ProgramInput::default())
1311 .context("Failed to run apply_cell_config BPF program")?;
1312 if out.return_value != 0 {
1313 bail!(
1314 "apply_cell_config BPF program returned error {} (num_assignments={}, num_cells={})",
1315 out.return_value as i32,
1316 cell_assignments.len(),
1317 cpu_assignments.len()
1318 );
1319 }
1320
1321 Ok(())
1322 }
1323
1324 fn get_metrics(&self) -> Metrics {
1325 self.metrics.clone()
1326 }
1327
1328 fn calculate_distribution_stats(
1329 &self,
1330 queue_counts: &[u64; QUEUE_STATS_IDX.len()],
1331 global_queue_decisions: u64,
1332 scope_queue_decisions: u64,
1333 scope_affn_viols: u64,
1334 scope_steals: u64,
1335 scope_pin_skips: u64,
1336 ) -> Result<DistributionStats> {
1337 let share_of_global =
1340 100.0 * (scope_queue_decisions as f64) / (global_queue_decisions as f64);
1341
1342 let queue_pct = if scope_queue_decisions == 0 {
1344 debug!("No queue decisions in scope, zeroing out queue distribution");
1345 [0.0; QUEUE_STATS_IDX.len()]
1346 } else {
1347 core::array::from_fn(|i| {
1348 100.0 * (queue_counts[i] as f64) / (scope_queue_decisions as f64)
1349 })
1350 };
1351
1352 let affinity_violations_percent = if scope_queue_decisions == 0 {
1354 debug!("No queue decisions in scope, zeroing out affinity violations");
1355 0.0
1356 } else {
1357 100.0 * (scope_affn_viols as f64) / (scope_queue_decisions as f64)
1358 };
1359
1360 let steal_pct = if scope_queue_decisions == 0 {
1361 0.0
1362 } else {
1363 100.0 * (scope_steals as f64) / (scope_queue_decisions as f64)
1364 };
1365
1366 let pin_skip_pct = if scope_queue_decisions == 0 {
1367 0.0
1368 } else {
1369 100.0 * (scope_pin_skips as f64) / (scope_queue_decisions as f64)
1370 };
1371
1372 const EXPECTED_QUEUES: usize = 4;
1373 if queue_pct.len() != EXPECTED_QUEUES {
1374 bail!(
1375 "Expected {} queues, got {}",
1376 EXPECTED_QUEUES,
1377 queue_pct.len()
1378 );
1379 }
1380
1381 Ok(DistributionStats {
1382 total_decisions: scope_queue_decisions,
1383 share_of_decisions_pct: share_of_global,
1384 local_q_pct: queue_pct[0],
1385 cpu_q_pct: queue_pct[1],
1386 cell_q_pct: queue_pct[2],
1387 borrowed_pct: queue_pct[3],
1388 affn_viol_pct: affinity_violations_percent,
1389 steal_pct,
1390 pin_skip_pct,
1391 global_queue_decisions,
1392 })
1393 }
1394
1395 fn update_and_log_global_queue_stats(
1397 &mut self,
1398 global_queue_decisions: u64,
1399 cell_stats_delta: &[[u64; NR_CSTATS]; MAX_CELLS],
1400 ) -> Result<()> {
1401 let mut queue_counts = [0; QUEUE_STATS_IDX.len()];
1403 for cells in 0..MAX_CELLS {
1404 for (i, stat) in QUEUE_STATS_IDX.iter().enumerate() {
1405 queue_counts[i] += cell_stats_delta[cells][*stat as usize];
1406 }
1407 }
1408
1409 let prefix = "Total Decisions:";
1410
1411 let scope_affn_viols: u64 = cell_stats_delta
1413 .iter()
1414 .map(|&cell| cell[bpf_intf::cell_stat_idx_CSTAT_AFFN_VIOL as usize])
1415 .sum::<u64>();
1416
1417 let scope_steals: u64 = cell_stats_delta
1419 .iter()
1420 .map(|&cell| cell[bpf_intf::cell_stat_idx_CSTAT_STEAL as usize])
1421 .sum::<u64>();
1422
1423 let scope_pin_skips: u64 = cell_stats_delta
1425 .iter()
1426 .map(|&cell| cell[bpf_intf::cell_stat_idx_CSTAT_PIN_SKIP as usize])
1427 .sum::<u64>();
1428
1429 let stats = self
1431 .calculate_distribution_stats(
1432 &queue_counts,
1433 global_queue_decisions,
1434 global_queue_decisions,
1435 scope_affn_viols,
1436 scope_steals,
1437 scope_pin_skips,
1438 )
1439 .context("calculating global queue distribution stats")?;
1440
1441 self.metrics.update(&stats);
1442
1443 let sum = |idx: usize| -> u64 { cell_stats_delta.iter().map(|c| c[idx]).sum() };
1445 self.metrics.drain_cnt = sum(bpf_intf::cell_stat_idx_CSTAT_DRAIN_CNT as usize);
1446 self.metrics.drain_affn_cnt = sum(bpf_intf::cell_stat_idx_CSTAT_DRAIN_AFFN_CNT as usize);
1447 self.metrics.slice_shrink_max =
1448 sum(bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_MAX as usize);
1449 self.metrics.slice_shrink_proportional =
1450 sum(bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_PROPORTIONAL as usize);
1451 self.metrics.slice_shrink_min =
1452 sum(bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_MIN as usize);
1453 self.metrics.slice_shrink = self.metrics.slice_shrink_max
1454 + self.metrics.slice_shrink_proportional
1455 + self.metrics.slice_shrink_min;
1456
1457 trace!("{} {}", prefix, stats);
1458
1459 Ok(())
1460 }
1461
1462 fn update_and_log_cell_queue_stats(
1464 &mut self,
1465 global_queue_decisions: u64,
1466 cell_stats_delta: &[[u64; NR_CSTATS]; MAX_CELLS],
1467 ) -> Result<()> {
1468 for cell in 0..MAX_CELLS {
1469 let cell_queue_decisions = QUEUE_STATS_IDX
1470 .iter()
1471 .map(|&stat| cell_stats_delta[cell][stat as usize])
1472 .sum::<u64>();
1473
1474 if cell_queue_decisions == 0 {
1476 continue;
1477 }
1478
1479 let mut queue_counts = [0; QUEUE_STATS_IDX.len()];
1480 for (i, &stat) in QUEUE_STATS_IDX.iter().enumerate() {
1481 queue_counts[i] = cell_stats_delta[cell][stat as usize];
1482 }
1483
1484 const MIN_CELL_WIDTH: usize = 2;
1485 let cell_width: usize = max(MIN_CELL_WIDTH, (MAX_CELLS as f64).log10().ceil() as usize);
1486
1487 let prefix = format!(" Cell {:width$}:", cell, width = cell_width);
1488
1489 let scope_affn_viols: u64 =
1491 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_AFFN_VIOL as usize];
1492
1493 let scope_steals: u64 =
1495 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_STEAL as usize];
1496
1497 let scope_pin_skips: u64 =
1499 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_PIN_SKIP as usize];
1500
1501 let stats = self
1502 .calculate_distribution_stats(
1503 &queue_counts,
1504 global_queue_decisions,
1505 cell_queue_decisions,
1506 scope_affn_viols,
1507 scope_steals,
1508 scope_pin_skips,
1509 )
1510 .with_context(|| {
1511 format!("calculating queue distribution stats for cell {}", cell)
1512 })?;
1513
1514 let cell_metrics = self.metrics.cells.entry(cell as u32).or_default();
1515 cell_metrics.update(&stats);
1516
1517 cell_metrics.drain_cnt =
1519 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_DRAIN_CNT as usize];
1520 cell_metrics.drain_affn_cnt =
1521 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_DRAIN_AFFN_CNT as usize];
1522 cell_metrics.slice_shrink_max =
1523 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_MAX as usize];
1524 cell_metrics.slice_shrink_proportional = cell_stats_delta[cell]
1525 [bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_PROPORTIONAL as usize];
1526 cell_metrics.slice_shrink_min =
1527 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_MIN as usize];
1528 cell_metrics.slice_shrink = cell_metrics.slice_shrink_max
1529 + cell_metrics.slice_shrink_proportional
1530 + cell_metrics.slice_shrink_min;
1531
1532 trace!("{} {}", prefix, stats);
1533 }
1534 Ok(())
1535 }
1536
1537 fn update_drain_metrics(&mut self, cell_stats_delta: &[[u64; NR_CSTATS]; MAX_CELLS]) {
1538 let mut total = 0;
1539 let mut affn_total = 0;
1540
1541 for cell in 0..MAX_CELLS {
1542 let drain_cnt =
1543 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_DRAIN_CNT as usize];
1544 let drain_affn_cnt =
1545 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_DRAIN_AFFN_CNT as usize];
1546 total += drain_cnt;
1547 affn_total += drain_affn_cnt;
1548
1549 if let Some(cell_metrics) = self.metrics.cells.get_mut(&(cell as u32)) {
1550 cell_metrics.drain_cnt = drain_cnt;
1551 cell_metrics.drain_affn_cnt = drain_affn_cnt;
1552 } else if drain_cnt > 0 || drain_affn_cnt > 0 {
1553 let cell_metrics = self.metrics.cells.entry(cell as u32).or_default();
1554 cell_metrics.drain_cnt = drain_cnt;
1555 cell_metrics.drain_affn_cnt = drain_affn_cnt;
1556 }
1557 }
1558
1559 self.metrics.drain_cnt = total;
1560 self.metrics.drain_affn_cnt = affn_total;
1561 }
1562
1563 fn log_all_queue_stats(
1564 &mut self,
1565 cell_stats_delta: &[[u64; NR_CSTATS]; MAX_CELLS],
1566 ) -> Result<()> {
1567 let global_queue_decisions: u64 = cell_stats_delta
1569 .iter()
1570 .flat_map(|cell| QUEUE_STATS_IDX.iter().map(|&idx| cell[idx as usize]))
1571 .sum();
1572
1573 self.update_drain_metrics(cell_stats_delta);
1574
1575 if global_queue_decisions == 0 {
1576 if self.metrics.drain_cnt == 0 {
1577 warn!("No queueing decisions made globally");
1578 }
1579 return Ok(());
1580 }
1581
1582 self.update_and_log_global_queue_stats(global_queue_decisions, cell_stats_delta)
1583 .context("updating global queue stats")?;
1584
1585 self.update_and_log_cell_queue_stats(global_queue_decisions, cell_stats_delta)
1586 .context("updating per-cell queue stats")?;
1587
1588 Ok(())
1589 }
1590
1591 fn calculate_cell_stat_delta(
1592 &mut self,
1593 cpu_ctxs: &[bpf_intf::cpu_ctx],
1594 ) -> Result<[[u64; NR_CSTATS]; MAX_CELLS]> {
1595 let mut cell_stats_delta = [[0_u64; NR_CSTATS]; MAX_CELLS];
1596
1597 for cell in 0..MAX_CELLS {
1600 for stat in 0..NR_CSTATS {
1601 let mut cur_cell_stat = 0;
1602
1603 for cpu_ctx in cpu_ctxs.iter() {
1605 cur_cell_stat += cpu_ctx.cstats[cell][stat];
1606 }
1607
1608 cell_stats_delta[cell][stat] = cur_cell_stat - self.prev_cell_stats[cell][stat];
1610 self.prev_cell_stats[cell][stat] = cur_cell_stat;
1611 }
1612 }
1613 Ok(cell_stats_delta)
1614 }
1615
1616 fn cgroup_path_for_cell(&self, cell_id: u32) -> String {
1618 match &self.cell_source {
1619 CellSource::Parent(cell_manager) => cell_manager.cgroup_path_for_cell(cell_id),
1620 CellSource::Configured(configured_cells) => {
1621 configured_cells.cgroup_path_for_cell(cell_id)
1622 }
1623 }
1624 }
1625
1626 fn collect_metrics(&mut self) -> Result<()> {
1628 let sampled_at = Instant::now();
1630 let cpu_ctxs = read_cpu_ctxs(&self.skel).context("reading per-CPU contexts for metrics")?;
1631
1632 let cell_stats_delta = self
1633 .calculate_cell_stat_delta(&cpu_ctxs)
1634 .context("calculating cell stat deltas")?;
1635
1636 self.log_all_queue_stats(&cell_stats_delta)
1637 .context("logging queue stats")?;
1638
1639 let enforced_holdout = match &self.cell_source {
1642 CellSource::Parent(cell_manager) => cell_manager.enforced_holdout(),
1643 CellSource::Configured(_) => self.configured_enforced_holdout,
1644 };
1645 self.metrics.enforced_holdout = enforced_holdout as u64;
1646
1647 self.collect_demand_metrics(&cpu_ctxs, sampled_at)
1648 .context("collecting demand metrics")?;
1649 self.collect_subcell_demand_metrics(&cpu_ctxs)
1650 .context("collecting subcell demand metrics")?;
1651
1652 for (cell_id, cell) in &self.cells {
1653 trace!("CELL[{}]: {}", cell_id, cell.cpus);
1654 }
1655
1656 for (cell_id, cell) in self.cells.iter() {
1657 let cgroup_path = self.cgroup_path_for_cell(*cell_id);
1658 self.metrics
1660 .cells
1661 .entry(*cell_id)
1662 .and_modify(|cell_metrics| {
1663 cell_metrics.num_cpus = cell.cpus.weight() as u32;
1664 cell_metrics.cgroup_path = cgroup_path;
1665 for subcell in &cell.subcells {
1666 cell_metrics
1667 .subcells
1668 .entry(subcell.id)
1669 .or_default()
1670 .update_name_and_cpus(&subcell.name, subcell.primary.weight() as u32);
1671 }
1672 });
1673 }
1674 self.metrics.num_cells = self.cells.len() as u32;
1675
1676 Ok(())
1677 }
1678
1679 fn collect_demand_metrics(
1681 &mut self,
1682 cpu_ctxs: &[bpf_intf::cpu_ctx],
1683 sampled_at: Instant,
1684 ) -> Result<()> {
1685 let half_interval = self.monitor_interval / 2;
1690 let interval_ns = match self.demand_sampled_at {
1691 Some(prev) if sampled_at.duration_since(prev) < half_interval => return Ok(()),
1692 Some(prev) => Some(sampled_at.duration_since(prev).as_nanos() as u64),
1693 None => None,
1694 };
1695 self.demand_sampled_at = Some(sampled_at);
1696
1697 let mut total_running_ns = [0u64; MAX_CELLS];
1702 let mut on_own_ns = [0u64; MAX_CELLS];
1703 let mut lent_ns = [0u64; MAX_CELLS];
1704
1705 for cpu_ctx in cpu_ctxs.iter() {
1706 let owner = cpu_ctx.cell as usize;
1707 for cell in 0..MAX_CELLS {
1708 let ns = cpu_ctx.running_ns[cell];
1709 total_running_ns[cell] += ns;
1710 if owner == cell {
1711 on_own_ns[cell] += ns;
1712 }
1713 }
1714 if owner >= MAX_CELLS {
1715 bail!(
1716 "CPU has invalid cell assignment {} (MAX_CELLS={})",
1717 owner,
1718 MAX_CELLS
1719 );
1720 }
1721 let total_on_cpu: u64 = cpu_ctx.running_ns.iter().sum();
1723 let owner_on_cpu = cpu_ctx.running_ns[owner];
1724 lent_ns[owner] += total_on_cpu.saturating_sub(owner_on_cpu);
1725 }
1726
1727 let Some(interval_ns) = interval_ns else {
1729 self.prev_cell_running_ns = total_running_ns;
1730 self.prev_cell_own_ns = on_own_ns;
1731 self.prev_cell_lent_ns = lent_ns;
1732 return Ok(());
1733 };
1734
1735 let mut global_running_delta = 0u64;
1736 let mut global_borrowed_delta = 0u64;
1737 let mut global_lent_delta = 0u64;
1738 let mut global_capacity = 0u64;
1739
1740 for cell in 0..MAX_CELLS {
1741 let delta_running =
1742 total_running_ns[cell].saturating_sub(self.prev_cell_running_ns[cell]);
1743 let delta_on_own = on_own_ns[cell].saturating_sub(self.prev_cell_own_ns[cell]);
1744 let delta_lent = lent_ns[cell].saturating_sub(self.prev_cell_lent_ns[cell]);
1745
1746 self.prev_cell_running_ns[cell] = total_running_ns[cell];
1747 self.prev_cell_own_ns[cell] = on_own_ns[cell];
1748 self.prev_cell_lent_ns[cell] = lent_ns[cell];
1749
1750 let delta_borrowed = delta_running.saturating_sub(delta_on_own);
1752
1753 let Some(cell_info) = self.cells.get(&(cell as u32)) else {
1756 continue;
1757 };
1758
1759 let nr_cpus = cell_info.cpus.weight() as u64;
1760 if nr_cpus == 0 {
1761 continue;
1764 }
1765
1766 let capacity = nr_cpus * interval_ns;
1768 let util_pct = 100.0 * (delta_running as f64) / (capacity as f64);
1770 let demand_borrow_pct = if delta_running > 0 {
1772 100.0 * (delta_borrowed as f64) / (delta_running as f64)
1773 } else {
1774 0.0
1775 };
1776 let lent_pct = 100.0 * (delta_lent as f64) / (capacity as f64);
1778
1779 if self.enable_rebalancing {
1781 self.smoothed_util[cell] = self.demand_smoothing * util_pct
1782 + (1.0 - self.demand_smoothing) * self.smoothed_util[cell];
1783 }
1784
1785 self.metrics
1786 .cells
1787 .entry(cell as u32)
1788 .or_default()
1789 .update_demand(util_pct, demand_borrow_pct, lent_pct);
1790
1791 if self.enable_rebalancing {
1793 self.metrics
1794 .cells
1795 .entry(cell as u32)
1796 .or_default()
1797 .smoothed_util_pct = self.smoothed_util[cell];
1798 }
1799
1800 global_running_delta = global_running_delta.saturating_add(delta_running);
1801 global_borrowed_delta = global_borrowed_delta.saturating_add(delta_borrowed);
1802 global_lent_delta = global_lent_delta.saturating_add(delta_lent);
1803 global_capacity = global_capacity.saturating_add(capacity);
1804 }
1805
1806 let global_util_pct = if global_capacity > 0 {
1807 100.0 * (global_running_delta as f64) / (global_capacity as f64)
1808 } else {
1809 0.0
1810 };
1811 let global_borrow_pct = if global_running_delta > 0 {
1812 100.0 * (global_borrowed_delta as f64) / (global_running_delta as f64)
1813 } else {
1814 0.0
1815 };
1816 let global_lent_pct = if global_capacity > 0 {
1817 100.0 * (global_lent_delta as f64) / (global_capacity as f64)
1818 } else {
1819 0.0
1820 };
1821
1822 self.metrics
1823 .update_demand(global_util_pct, global_borrow_pct, global_lent_pct);
1824
1825 Ok(())
1826 }
1827
1828 fn collect_subcell_demand_metrics(&mut self, cpu_ctxs: &[bpf_intf::cpu_ctx]) -> Result<()> {
1830 let mut total_running_ns = [[0u64; MAX_SUBCELLS_PER_CELL]; MAX_CELLS];
1831 let mut on_own_ns = [[0u64; MAX_SUBCELLS_PER_CELL]; MAX_CELLS];
1832 let mut lent_ns = [[0u64; MAX_SUBCELLS_PER_CELL]; MAX_CELLS];
1833 let mut queued_ns = [[0u64; MAX_SUBCELLS_PER_CELL]; MAX_CELLS];
1834 let mut demand_sum = [[0u64; MAX_SUBCELLS_PER_CELL]; MAX_CELLS];
1835 let active_subcells: HashMap<(usize, usize), (usize, String)> = self
1836 .cells
1837 .iter()
1838 .flat_map(|(&cell_id, cell)| {
1839 cell.subcells.iter().map(move |subcell| {
1840 (
1841 (cell_id as usize, subcell.id as usize),
1842 (subcell.primary.weight(), subcell.name.clone()),
1843 )
1844 })
1845 })
1846 .collect();
1847 let subcell_accounts = read_subcell_accounts(&self.skel, active_subcells.keys().copied())
1848 .context("reading subcell accounts")?;
1849
1850 for (cpu, cpu_ctx) in cpu_ctxs.iter().enumerate() {
1851 let owner_cell = cpu_ctx.cell as usize;
1852 let owner_subcell = cpu_ctx.subcell as usize;
1853 if owner_cell >= MAX_CELLS || owner_subcell >= MAX_SUBCELLS_PER_CELL {
1854 bail!(
1855 "CPU has invalid subcell assignment {}:{} (max {}:{})",
1856 owner_cell,
1857 owner_subcell,
1858 MAX_CELLS,
1859 MAX_SUBCELLS_PER_CELL
1860 );
1861 }
1862
1863 let mut total_on_cpu = 0u64;
1864 for (&(cell, subcell), per_cpu_accounts) in &subcell_accounts {
1865 let ns = per_cpu_accounts[cpu].running_ns;
1866 total_running_ns[cell][subcell] =
1867 total_running_ns[cell][subcell].saturating_add(ns);
1868 queued_ns[cell][subcell] =
1869 queued_ns[cell][subcell].saturating_add(per_cpu_accounts[cpu].queued_ns);
1870 demand_sum[cell][subcell] =
1871 demand_sum[cell][subcell].wrapping_add(per_cpu_accounts[cpu].demand_sum);
1872 total_on_cpu = total_on_cpu.saturating_add(ns);
1873 if owner_cell == cell && owner_subcell == subcell {
1874 on_own_ns[cell][subcell] = on_own_ns[cell][subcell].saturating_add(ns);
1875 }
1876 }
1877
1878 let owner_on_cpu = subcell_accounts
1879 .get(&(owner_cell, owner_subcell))
1880 .map_or(0, |per_cpu_accounts| per_cpu_accounts[cpu].running_ns);
1881 lent_ns[owner_cell][owner_subcell] = lent_ns[owner_cell][owner_subcell]
1882 .saturating_add(total_on_cpu.saturating_sub(owner_on_cpu));
1883 }
1884
1885 let interval_ns = self.monitor_interval.as_nanos() as u64;
1886
1887 for cell in 0..MAX_CELLS {
1888 for subcell in 0..MAX_SUBCELLS_PER_CELL {
1889 let delta_running = total_running_ns[cell][subcell]
1890 .saturating_sub(self.prev_subcell_running_ns[cell][subcell]);
1891 let delta_on_own = on_own_ns[cell][subcell]
1892 .saturating_sub(self.prev_subcell_own_ns[cell][subcell]);
1893 let delta_lent =
1894 lent_ns[cell][subcell].saturating_sub(self.prev_subcell_lent_ns[cell][subcell]);
1895 let delta_queued = queued_ns[cell][subcell]
1896 .saturating_sub(self.prev_subcell_queued_ns[cell][subcell]);
1897 let delta_demand = demand_sum[cell][subcell]
1899 .wrapping_sub(self.prev_subcell_demand_sum[cell][subcell]);
1900
1901 self.prev_subcell_running_ns[cell][subcell] = total_running_ns[cell][subcell];
1902 self.prev_subcell_own_ns[cell][subcell] = on_own_ns[cell][subcell];
1903 self.prev_subcell_lent_ns[cell][subcell] = lent_ns[cell][subcell];
1904 self.prev_subcell_queued_ns[cell][subcell] = queued_ns[cell][subcell];
1905 self.prev_subcell_demand_sum[cell][subcell] = demand_sum[cell][subcell];
1906
1907 if delta_running == 0 && delta_lent == 0 && delta_queued == 0 && delta_demand == 0 {
1908 continue;
1909 }
1910
1911 let Some((nr_cpus, name)) = active_subcells.get(&(cell, subcell)) else {
1912 continue;
1913 };
1914 if *nr_cpus == 0 {
1915 continue;
1918 }
1919
1920 let capacity = (*nr_cpus as u64) * interval_ns;
1921 let delta_borrowed = delta_running.saturating_sub(delta_on_own);
1922 let util_pct = 100.0 * (delta_running as f64) / (capacity as f64);
1923 let demand_borrow_pct = if delta_running > 0 {
1924 100.0 * (delta_borrowed as f64) / (delta_running as f64)
1925 } else {
1926 0.0
1927 };
1928 let lent_pct = 100.0 * (delta_lent as f64) / (capacity as f64);
1929 let load = delta_running.saturating_add(delta_queued) as f64 / interval_ns as f64;
1933 let demand = delta_demand as f64 / (DEMAND_ONE * interval_ns as f64);
1935
1936 if self.enable_rebalancing {
1937 self.smoothed_subcell_util[cell][subcell] = self.demand_smoothing * util_pct
1938 + (1.0 - self.demand_smoothing) * self.smoothed_subcell_util[cell][subcell];
1939 self.smoothed_subcell_load[cell][subcell] = self.demand_smoothing * load
1940 + (1.0 - self.demand_smoothing) * self.smoothed_subcell_load[cell][subcell];
1941 self.smoothed_subcell_demand[cell][subcell] = self.demand_smoothing * demand
1942 + (1.0 - self.demand_smoothing)
1943 * self.smoothed_subcell_demand[cell][subcell];
1944 }
1945
1946 let subcell_metrics = self
1947 .metrics
1948 .cells
1949 .entry(cell as u32)
1950 .or_default()
1951 .subcells
1952 .entry(subcell as u32)
1953 .or_default();
1954 subcell_metrics.update_demand(
1955 util_pct,
1956 demand_borrow_pct,
1957 lent_pct,
1958 delta_running,
1959 delta_borrowed,
1960 delta_lent,
1961 delta_queued,
1962 load,
1963 demand,
1964 );
1965 subcell_metrics.update_name_and_cpus(name, *nr_cpus as u32);
1966 if self.enable_rebalancing {
1967 subcell_metrics.smoothed_util_pct = self.smoothed_subcell_util[cell][subcell];
1968 subcell_metrics.smoothed_load = self.smoothed_subcell_load[cell][subcell];
1969 subcell_metrics.smoothed_demand = self.smoothed_subcell_demand[cell][subcell];
1970 }
1971 }
1972 }
1973
1974 Ok(())
1975 }
1976
1977 fn update_applied_cpuset_seq(&mut self) {
1979 unsafe {
1980 let ptr = &mut self
1981 .skel
1982 .maps
1983 .bss_data
1984 .as_mut()
1985 .expect("BUG: bss_data missing after scheduler load")
1986 .applied_cpuset_seq as *mut u32;
1987 std::ptr::write_volatile(ptr, self.last_cpuset_seq);
1988 }
1989 }
1990
1991 fn check_cpuset_changes(&mut self) -> Result<()> {
1993 let current_seq = unsafe {
1994 let ptr = &self
1995 .skel
1996 .maps
1997 .bss_data
1998 .as_ref()
1999 .expect("BUG: bss_data missing after scheduler load")
2000 .cpuset_seq as *const u32;
2001 (ptr as *const AtomicU32)
2002 .as_ref()
2003 .expect("BUG: cpuset_seq pointer cast yielded null")
2004 .load(Ordering::Acquire)
2005 };
2006
2007 if current_seq == self.last_cpuset_seq {
2008 return Ok(());
2009 }
2010 self.last_cpuset_seq = current_seq;
2011
2012 match self.cell_source {
2013 CellSource::Parent(_) => self.recompute_parent_cells_after_cpuset_change(),
2014 CellSource::Configured(_) => self.recompute_configured_cells_after_cpuset_change(),
2015 }
2016 }
2017
2018 fn recompute_parent_cells_after_cpuset_change(&mut self) -> Result<()> {
2019 if !self
2020 .cell_manager_mut()?
2021 .refresh_cpusets()
2022 .context("refreshing cell cpusets")?
2023 {
2024 self.update_applied_cpuset_seq();
2026 return Ok(());
2027 }
2028
2029 let cpu_assignments = self
2030 .compute_and_apply_cell_config(&[])
2031 .context("recomputing cell configuration after cpuset change")?;
2032 self.update_applied_cpuset_seq();
2033 info!(
2034 "Cpuset change detected, recomputed config: {}",
2035 self.cell_manager()?.format_cell_config(&cpu_assignments)
2036 );
2037 Ok(())
2038 }
2039
2040 fn recompute_configured_cells_after_cpuset_change(&mut self) -> Result<()> {
2041 let cpu_assignments = self
2042 .compute_and_apply_configured_cell_config()
2043 .context("recomputing configured cell configuration after cpuset change")?;
2044 self.last_config_refresh = Instant::now();
2045 self.update_applied_cpuset_seq();
2046 info!(
2047 "Cpuset change detected, recomputed configured cells: {}",
2048 self.configured_cells()?
2049 .format_cell_config(&cpu_assignments)
2050 );
2051 Ok(())
2052 }
2053
2054 fn maybe_refresh_configured_cells(&mut self) -> Result<()> {
2056 if !matches!(self.cell_source, CellSource::Configured(_))
2057 || self.last_config_refresh.elapsed() < self.reconfiguration_interval
2058 {
2059 return Ok(());
2060 }
2061
2062 let cpu_assignments = self
2063 .compute_and_apply_configured_cell_config()
2064 .context("recomputing configured cells")?;
2065 self.last_config_refresh = Instant::now();
2066
2067 info!(
2068 "Refreshed configured cells: {}",
2069 self.configured_cells()?
2070 .format_cell_config(&cpu_assignments)
2071 );
2072 Ok(())
2073 }
2074
2075 fn refresh_bpf_cells(&mut self) -> Result<()> {
2076 let applied_configuration = unsafe {
2077 let ptr = &self
2078 .skel
2079 .maps
2080 .bss_data
2081 .as_ref()
2082 .expect("BUG: bss_data missing after scheduler load")
2083 .applied_configuration_seq as *const u32;
2084 (ptr as *const std::sync::atomic::AtomicU32)
2085 .as_ref()
2086 .expect("BUG: applied_configuration_seq pointer cast yielded null")
2087 .load(std::sync::atomic::Ordering::Acquire)
2088 };
2089 if self
2090 .last_configuration_seq
2091 .is_some_and(|seq| applied_configuration == seq)
2092 {
2093 return Ok(());
2094 }
2095 let mut cell_to_cpus: HashMap<u32, Cpumask> = HashMap::new();
2097 let cpu_ctxs =
2098 read_cpu_ctxs(&self.skel).context("reading per-CPU contexts for BPF cell refresh")?;
2099 for (i, cpu_ctx) in cpu_ctxs.iter().enumerate() {
2100 cell_to_cpus
2101 .entry(cpu_ctx.cell)
2102 .or_insert_with(Cpumask::new)
2103 .set_cpu(i)
2104 .expect("set cpu in existing mask");
2105 }
2106
2107 let cells_with_cpus: HashSet<u32> = cell_to_cpus.keys().copied().collect();
2118 let mut active_cells = cells_with_cpus.clone();
2119 active_cells.insert(0);
2120
2121 for cell_idx in &active_cells {
2122 let cpus = cell_to_cpus
2123 .get(cell_idx)
2124 .cloned()
2125 .unwrap_or_else(Cpumask::new);
2126 self.cells.entry(*cell_idx).or_insert_with(Cell::new).cpus = cpus;
2127 self.metrics.cells.insert(*cell_idx, CellMetrics::default());
2128 }
2129
2130 self.cells.retain(|&k, _| active_cells.contains(&k));
2132 self.metrics.cells.retain(|&k, _| active_cells.contains(&k));
2133
2134 self.refresh_bpf_subcells(&active_cells)?;
2135
2136 self.last_configuration_seq = Some(applied_configuration);
2137
2138 Ok(())
2139 }
2140}
2141
2142fn validate_subcell_assignments(
2143 cell: &CpuAssignment,
2144 subcells: &[CpuAssignment],
2145 share_sibling_cpus: bool,
2146) -> Result<()> {
2147 if subcells.is_empty() {
2148 bail!("Cell {} has no subcell assignments", cell.id);
2149 }
2150 if subcells.len() > MAX_SUBCELLS_PER_CELL {
2151 bail!(
2152 "Cell {} has too many subcells: {} > MAX_SUBCELLS_PER_CELL ({})",
2153 cell.id,
2154 subcells.len(),
2155 MAX_SUBCELLS_PER_CELL
2156 );
2157 }
2158
2159 let mut seen = [false; MAX_SUBCELLS_PER_CELL];
2160 let mut primary_union = Cpumask::new();
2161
2162 for subcell in subcells {
2163 if subcell.id >= bpf_intf::consts_MAX_SUBCELLS_PER_CELL {
2164 bail!(
2165 "Subcell ID {} for cell {} exceeds MAX_SUBCELLS_PER_CELL ({})",
2166 subcell.id,
2167 cell.id,
2168 bpf_intf::consts_MAX_SUBCELLS_PER_CELL
2169 );
2170 }
2171
2172 let slot = subcell.id as usize;
2173 if seen[slot] {
2174 bail!("Duplicate subcell ID {} for cell {}", subcell.id, cell.id);
2175 }
2176 seen[slot] = true;
2177
2178 if subcell.primary.weight() == 0 {
2179 bail!(
2180 "Subcell {} in cell {} has no primary CPUs",
2181 subcell.id,
2182 cell.id
2183 );
2184 }
2185 if subcell.primary.and(&cell.primary.not()).weight() != 0 {
2186 bail!(
2187 "Subcell {} primary CPUs are outside cell {}",
2188 subcell.id,
2189 cell.id
2190 );
2191 }
2192 if primary_union.and(&subcell.primary).weight() != 0 {
2193 bail!(
2194 "Subcell {} overlaps another subcell in cell {}",
2195 subcell.id,
2196 cell.id
2197 );
2198 }
2199
2200 if share_sibling_cpus {
2201 let expected = cell.primary.and(&subcell.primary.not());
2202 let borrowable = subcell.borrowable.as_ref().ok_or_else(|| {
2203 anyhow::anyhow!(
2204 "Subcell {} in cell {} is missing its borrowable mask",
2205 subcell.id,
2206 cell.id
2207 )
2208 })?;
2209 if borrowable != &expected {
2210 bail!(
2211 "Subcell {} in cell {} has an invalid borrowable mask",
2212 subcell.id,
2213 cell.id
2214 );
2215 }
2216 } else if subcell.borrowable.is_some() {
2217 bail!(
2218 "Subcell {} in cell {} has borrowing disabled but a borrowable mask",
2219 subcell.id,
2220 cell.id
2221 );
2222 }
2223
2224 primary_union = primary_union.or(&subcell.primary);
2225 }
2226
2227 if primary_union != cell.primary {
2228 bail!("Subcell primary CPUs do not cover cell {}", cell.id);
2229 }
2230
2231 Ok(())
2232}
2233
2234fn write_cpumask_to_config(cpumask: &Cpumask, dest: &mut [u8]) {
2235 let raw_slice = cpumask.as_raw_slice();
2236 for (word_idx, word) in raw_slice.iter().enumerate() {
2237 let byte_start = word_idx * 8;
2238 let bytes = word.to_le_bytes();
2239 for (j, byte) in bytes.iter().enumerate() {
2240 let idx = byte_start + j;
2241 if idx < dest.len() {
2242 dest[idx] = *byte;
2243 }
2244 }
2245 }
2246}
2247
2248fn write_subcell_matches(
2249 matches: &[Vec<SubcellMatch>],
2250 dest: &mut types::subcell_config,
2251) -> Result<()> {
2252 if matches.len() > bpf_intf::consts_MAX_SUBCELL_MATCH_ORS as usize {
2253 bail!(
2254 "Too many subcell match OR groups: {} > {}",
2255 matches.len(),
2256 bpf_intf::consts_MAX_SUBCELL_MATCH_ORS
2257 );
2258 }
2259
2260 dest.nr_match_ors = matches.len() as u32;
2261 for (or_idx, ands) in matches.iter().enumerate() {
2262 if ands.len() > bpf_intf::consts_MAX_SUBCELL_MATCH_ANDS as usize {
2263 bail!(
2264 "Too many subcell match AND terms: {} > {}",
2265 ands.len(),
2266 bpf_intf::consts_MAX_SUBCELL_MATCH_ANDS
2267 );
2268 }
2269
2270 dest.matches[or_idx].nr_matches = ands.len() as u32;
2271 for (and_idx, subcell_match) in ands.iter().enumerate() {
2272 match subcell_match {
2273 SubcellMatch::CommPrefix(prefix) => {
2274 dest.matches[or_idx].matches[and_idx].kind =
2275 bpf_intf::subcell_match_kind_SUBCELL_MATCH_COMM_PREFIX;
2276 copy_cstr(&mut dest.matches[or_idx].matches[and_idx].value, prefix)?;
2277 }
2278 }
2279 }
2280 }
2281
2282 Ok(())
2283}
2284
2285fn copy_cstr(dest: &mut [i8], src: &str) -> Result<()> {
2286 if dest.is_empty() {
2287 return Ok(());
2288 }
2289 if src.as_bytes().contains(&0) {
2290 bail!("subcell match string contains NUL byte");
2291 }
2292
2293 for value in dest.iter_mut() {
2294 *value = 0;
2295 }
2296
2297 let len = src.len().min(dest.len() - 1);
2298 for (dst, src) in dest.iter_mut().zip(src.as_bytes().iter()).take(len) {
2299 *dst = *src as i8;
2300 }
2301
2302 Ok(())
2303}
2304
2305fn default_subcell_name(id: u32) -> String {
2306 if id == 0 {
2307 "rest".to_string()
2308 } else {
2309 format!("subcell{}", id)
2310 }
2311}
2312
2313fn read_cpumask_from_bytes(src: &[u8]) -> Result<Cpumask> {
2314 let mut cpumask = Cpumask::new();
2315
2316 for (byte_idx, byte) in src.iter().copied().enumerate() {
2317 let mut bits = byte;
2318 while bits != 0 {
2319 let bit = bits.trailing_zeros() as usize;
2320 bits &= !(1u8 << bit);
2321 cpumask.set_cpu((byte_idx * 8) + bit)?;
2322 }
2323 }
2324
2325 Ok(cpumask)
2326}
2327
2328fn read_bpf_cell(skel: &BpfSkel, cell_id: u32) -> Result<bpf_intf::cell> {
2329 let raw = skel
2330 .maps
2331 .cells
2332 .lookup(&cell_id.to_ne_bytes(), libbpf_rs::MapFlags::ANY)
2333 .with_context(|| format!("Failed to lookup cell {}", cell_id))?
2334 .ok_or_else(|| anyhow::anyhow!("Cell {} missing from BPF map", cell_id))?;
2335
2336 if raw.len() != std::mem::size_of::<bpf_intf::cell>() {
2337 bail!(
2338 "Unexpected cell {} value size: {} != {}",
2339 cell_id,
2340 raw.len(),
2341 std::mem::size_of::<bpf_intf::cell>()
2342 );
2343 }
2344
2345 Ok(unsafe { std::ptr::read_unaligned(raw.as_ptr() as *const bpf_intf::cell) })
2346}
2347
2348fn read_cpu_ctxs(skel: &BpfSkel) -> Result<Vec<bpf_intf::cpu_ctx>> {
2349 let mut cpu_ctxs = vec![];
2350 let cpu_ctxs_vec = skel
2351 .maps
2352 .cpu_ctxs
2353 .lookup_percpu(&0u32.to_ne_bytes(), libbpf_rs::MapFlags::ANY)
2354 .context("Failed to lookup cpu_ctx")?
2355 .expect("BUG: cpu_ctxs lookup_percpu returned None for key 0");
2356 if cpu_ctxs_vec.len() < *NR_CPUS_POSSIBLE {
2357 bail!(
2358 "Percpu map returned {} entries but expected {}",
2359 cpu_ctxs_vec.len(),
2360 *NR_CPUS_POSSIBLE
2361 );
2362 }
2363 for cpu in 0..*NR_CPUS_POSSIBLE {
2364 cpu_ctxs.push(*unsafe {
2365 &*(cpu_ctxs_vec[cpu].as_slice().as_ptr() as *const bpf_intf::cpu_ctx)
2366 });
2367 }
2368 Ok(cpu_ctxs)
2369}
2370
2371fn read_subcell_accounts<I>(
2373 skel: &BpfSkel,
2374 active_subcells: I,
2375) -> Result<HashMap<(usize, usize), Vec<bpf_intf::subcell_account>>>
2376where
2377 I: IntoIterator<Item = (usize, usize)>,
2378{
2379 let mut accounts_by_subcell = HashMap::new();
2380
2381 for (cell, subcell) in active_subcells {
2382 if cell >= MAX_CELLS || subcell >= MAX_SUBCELLS_PER_CELL {
2383 bail!(
2384 "Invalid subcell account key {}:{} (max {}:{})",
2385 cell,
2386 subcell,
2387 MAX_CELLS,
2388 MAX_SUBCELLS_PER_CELL
2389 );
2390 }
2391
2392 let packed = ((cell * MAX_SUBCELLS_PER_CELL) + subcell) as u32;
2393 let per_cpu_values = skel
2394 .maps
2395 .subcell_accounts
2396 .lookup_percpu(&packed.to_ne_bytes(), libbpf_rs::MapFlags::ANY)
2397 .with_context(|| format!("Failed to lookup subcell_accounts key {}", packed))?
2398 .with_context(|| format!("subcell_accounts key {} is missing", packed))?;
2399
2400 if per_cpu_values.len() < *NR_CPUS_POSSIBLE {
2401 bail!(
2402 "subcell_accounts returned {} entries but expected {}",
2403 per_cpu_values.len(),
2404 *NR_CPUS_POSSIBLE
2405 );
2406 }
2407
2408 let mut values = Vec::with_capacity(*NR_CPUS_POSSIBLE);
2409 for cpu in 0..*NR_CPUS_POSSIBLE {
2410 let value = per_cpu_values[cpu].as_slice();
2411 if value.len() < std::mem::size_of::<bpf_intf::subcell_account>() {
2412 bail!(
2413 "subcell_accounts key {} cpu {} value is too small: {}",
2414 packed,
2415 cpu,
2416 value.len()
2417 );
2418 }
2419 values.push(unsafe {
2420 std::ptr::read_unaligned(value.as_ptr() as *const bpf_intf::subcell_account)
2421 });
2422 }
2423
2424 accounts_by_subcell.insert((cell, subcell), values);
2425 }
2426
2427 Ok(accounts_by_subcell)
2428}
2429
2430fn run(opts: Opts) -> Result<()> {
2431 if opts.version {
2432 println!(
2433 "scx_mitosis {}",
2434 build_id::full_version(env!("CARGO_PKG_VERSION"))
2435 );
2436 return Ok(());
2437 }
2438
2439 let env_filter = EnvFilter::try_from_default_env()
2440 .or_else(|_| match EnvFilter::try_new(&opts.log_level) {
2441 Ok(filter) => Ok(filter),
2442 Err(e) => {
2443 eprintln!(
2444 "invalid log envvar: {}, using info, err is: {}",
2445 opts.log_level, e
2446 );
2447 EnvFilter::try_new("info")
2448 }
2449 })
2450 .unwrap_or_else(|_| EnvFilter::new("info"));
2451
2452 match tracing_subscriber::fmt()
2453 .with_env_filter(env_filter)
2454 .with_target(true)
2455 .with_thread_ids(true)
2456 .with_file(true)
2457 .with_line_number(true)
2458 .try_init()
2459 {
2460 Ok(()) => {}
2461 Err(e) => eprintln!("failed to init logger: {}", e),
2462 }
2463
2464 if opts.verbose > 0 {
2465 warn!("Setting verbose via -v is deprecated and will be an error in future releases.");
2466 }
2467
2468 debug!("opts={:?}", &opts);
2469
2470 if let Some(run_id) = opts.run_id {
2471 info!("scx_mitosis run_id: {}", run_id);
2472 }
2473
2474 let shutdown = Arc::new(AtomicBool::new(false));
2475 let shutdown_clone = shutdown.clone();
2476 ctrlc::set_handler(move || {
2477 shutdown_clone.store(true, Ordering::Relaxed);
2478 })
2479 .context("Error setting Ctrl-C handler")?;
2480
2481 if let Some(intv) = opts.monitor.or(opts.monitor_sync) {
2482 let shutdown_clone = shutdown.clone();
2483 let jh = std::thread::spawn(move || {
2484 match stats::monitor(Duration::from_secs_f64(intv), shutdown_clone) {
2485 Ok(_) => {
2486 debug!("stats monitor thread finished successfully")
2487 }
2488 Err(error_object) => {
2489 warn!(
2490 "stats monitor thread finished because of an error {}",
2491 error_object
2492 )
2493 }
2494 }
2495 });
2496 if opts.monitor.is_some() {
2497 let _ = jh.join();
2498 return Ok(());
2499 }
2500 }
2501
2502 let mut open_object = MaybeUninit::uninit();
2503 loop {
2504 let mut sched =
2505 Scheduler::init(&opts, &mut open_object).context("initializing scheduler")?;
2506 if !sched
2507 .run(shutdown.clone())
2508 .context("running scheduler main loop")?
2509 .should_restart()
2510 {
2511 break;
2512 }
2513 }
2514
2515 Ok(())
2516}
2517
2518fn main() -> Result<()> {
2519 let parsed = undefok_flags::parse_args::<Opts>()?;
2520 for undefok in &parsed.ignored_undefok_flags {
2522 eprintln!("warning: ignoring undefok flag --{}", undefok.long);
2523 }
2524 run(parsed.opts)
2525}
2526
2527#[cfg(test)]
2528mod tests {
2529 use super::{CpuAssignment, Cpumask, Opts, validate_subcell_assignments};
2530 use clap::Parser;
2531
2532 fn cpumask(cpus: &[usize]) -> Cpumask {
2533 let mut mask = Cpumask::new();
2534 for &cpu in cpus {
2535 mask.set_cpu(cpu).unwrap();
2536 }
2537 mask
2538 }
2539
2540 fn assignment(id: u32, primary: &[usize], borrowable: Option<&[usize]>) -> CpuAssignment {
2541 CpuAssignment {
2542 id,
2543 primary: cpumask(primary),
2544 borrowable: borrowable.map(cpumask),
2545 }
2546 }
2547
2548 #[test]
2549 fn requires_cell_parent_cgroup_for_scheduler_mode() {
2550 assert!(Opts::try_parse_from(["scx_mitosis"]).is_err());
2551 }
2552
2553 #[test]
2554 fn allows_cell_config_for_scheduler_mode() {
2555 assert!(Opts::try_parse_from(["scx_mitosis", "--cell-config", "/tmp/cells.json"]).is_ok());
2556 }
2557
2558 #[test]
2559 fn rejects_both_cell_modes() {
2560 assert!(
2561 Opts::try_parse_from([
2562 "scx_mitosis",
2563 "--cell-parent-cgroup",
2564 "/workloads",
2565 "--cell-config",
2566 "/tmp/cells.json"
2567 ])
2568 .is_err()
2569 );
2570 }
2571
2572 #[test]
2573 fn allows_monitor_without_cell_parent_cgroup() {
2574 assert!(Opts::try_parse_from(["scx_mitosis", "--monitor", "1"]).is_ok());
2575 }
2576
2577 #[test]
2578 fn allows_version_without_cell_parent_cgroup() {
2579 assert!(Opts::try_parse_from(["scx_mitosis", "--version"]).is_ok());
2580 }
2581
2582 #[test]
2583 fn accepts_complete_unpinned_subcell_partition() {
2584 let cell = assignment(3, &[0, 1, 2, 3], None);
2585 let subcells = [
2586 assignment(0, &[0, 1], Some(&[2, 3])),
2587 assignment(1, &[2, 3], Some(&[0, 1])),
2588 ];
2589
2590 validate_subcell_assignments(&cell, &subcells, true).unwrap();
2591 }
2592
2593 #[test]
2594 fn rejects_overlapping_subcell_primaries() {
2595 let cell = assignment(3, &[0, 1, 2, 3], None);
2596 let subcells = [
2597 assignment(0, &[0, 1, 2], None),
2598 assignment(1, &[2, 3], None),
2599 ];
2600
2601 assert!(validate_subcell_assignments(&cell, &subcells, false).is_err());
2602 }
2603
2604 #[test]
2605 fn rejects_incomplete_subcell_partition() {
2606 let cell = assignment(3, &[0, 1, 2, 3], None);
2607 let subcells = [assignment(0, &[0, 1, 2], None)];
2608
2609 assert!(validate_subcell_assignments(&cell, &subcells, false).is_err());
2610 }
2611}