1#[allow(clippy::unwrap_used)]
7mod bpf_skel;
8pub use bpf_skel::*;
9pub mod bpf_intf;
10mod cell_manager;
11mod mitosis_topology_utils;
12mod stats;
13
14use cell_manager::{CellManager, CpuAssignment};
15
16use std::cmp::max;
17use std::collections::{HashMap, HashSet};
18use std::fmt;
19use std::fmt::Display;
20use std::mem::MaybeUninit;
21use std::os::fd::AsFd;
22use std::sync::atomic::AtomicBool;
23use std::sync::atomic::AtomicU32;
24use std::sync::atomic::Ordering;
25use std::sync::Arc;
26use std::time::Duration;
27use std::time::Instant;
28
29use anyhow::anyhow;
30use anyhow::bail;
31use anyhow::Context;
32use anyhow::Result;
33use clap::Parser;
34use libbpf_rs::MapCore as _;
35use libbpf_rs::OpenObject;
36use libbpf_rs::ProgramInput;
37use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags, EpollTimeout};
38use nix::sys::eventfd::EventFd;
39use scx_stats::prelude::*;
40use scx_utils::build_id;
41use scx_utils::compat;
42use scx_utils::init_libbpf_logging;
43use scx_utils::libbpf_clap_opts::LibbpfOpts;
44use scx_utils::scx_enums;
45use scx_utils::scx_ops_attach;
46use scx_utils::scx_ops_load;
47use scx_utils::scx_ops_open;
48use scx_utils::uei_exited;
49use scx_utils::uei_report;
50use scx_utils::Cpumask;
51use scx_utils::Topology;
52use scx_utils::UserExitInfo;
53use scx_utils::NR_CPUS_POSSIBLE;
54use tracing::{debug, info, trace, warn};
55use tracing_subscriber::filter::EnvFilter;
56
57use stats::CellMetrics;
58use stats::Metrics;
59
60const SCHEDULER_NAME: &str = "scx_mitosis";
61const MAX_CELLS: usize = bpf_intf::consts_MAX_CELLS as usize;
62const NR_CSTATS: usize = bpf_intf::cell_stat_idx_NR_CSTATS as usize;
63const INOTIFY_TOKEN: u64 = 1;
65const STATS_TOKEN: u64 = 2;
67
68fn parse_ewma_factor(s: &str) -> Result<f64, String> {
69 let v: f64 = s.parse().map_err(|e| format!("{e}"))?;
70 if !(0.0..=1.0).contains(&v) {
71 return Err(format!("value {v} not in range 0.0..=1.0"));
72 }
73 Ok(v)
74}
75
76#[derive(Debug, Parser)]
84struct Opts {
85 #[clap(short = 'v', long, action = clap::ArgAction::Count)]
87 verbose: u8,
88
89 #[clap(long, default_value = "info")]
92 log_level: String,
93
94 #[clap(long, default_value = "0")]
96 exit_dump_len: u32,
97
98 #[clap(long, default_value = "1")]
100 monitor_interval_s: u64,
101
102 #[clap(long)]
105 monitor: Option<f64>,
106
107 #[clap(short = 'V', long, action = clap::ArgAction::SetTrue)]
109 version: bool,
110
111 #[clap(long)]
113 run_id: Option<u64>,
114
115 #[clap(long, action = clap::ArgAction::SetTrue)]
118 debug_events: bool,
119
120 #[clap(long, default_value = "true", action = clap::ArgAction::Set)]
124 exiting_task_workaround: bool,
125
126 #[clap(long, action = clap::ArgAction::SetTrue)]
129 cpu_controller_disabled: bool,
130
131 #[clap(long, action = clap::ArgAction::SetTrue)]
134 reject_multicpu_pinning: bool,
135
136 #[clap(long, action = clap::ArgAction::SetTrue)]
139 enable_llc_awareness: bool,
140
141 #[clap(long, action = clap::ArgAction::SetTrue)]
143 enable_work_stealing: bool,
144
145 #[clap(long, required_unless_present_any = ["monitor", "version"])]
149 cell_parent_cgroup: Option<String>,
150
151 #[clap(long)]
156 cell_exclude: Vec<String>,
157
158 #[clap(long, default_value_t = 0)]
165 cell0_min_cpus: usize,
166
167 #[clap(long, action = clap::ArgAction::SetTrue)]
170 enable_borrowing: bool,
171
172 #[clap(long, action = clap::ArgAction::SetTrue)]
174 use_lockless_peek: bool,
175
176 #[clap(long, action = clap::ArgAction::SetTrue)]
178 enable_rebalancing: bool,
179
180 #[clap(long, default_value = "20.0")]
182 rebalance_threshold: f64,
183
184 #[clap(long, default_value = "5")]
186 rebalance_cooldown_s: u64,
187
188 #[clap(long, default_value = "0.3", value_parser = parse_ewma_factor)]
190 demand_smoothing: f64,
191
192 #[clap(long, action = clap::ArgAction::SetTrue)]
196 dynamic_affinity_cpu_selection: bool,
197
198 #[clap(long, action = clap::ArgAction::SetTrue)]
201 enable_slice_shrinking: bool,
202
203 #[clap(long, default_value = "4000")]
206 slice_shrink_max_us: u64,
207
208 #[clap(long, default_value = "500")]
212 slice_shrink_min_us: u64,
213
214 #[clap(flatten, next_help_heading = "Libbpf Options")]
215 pub libbpf: LibbpfOpts,
216}
217
218const QUEUE_STATS_IDX: [bpf_intf::cell_stat_idx; 4] = [
223 bpf_intf::cell_stat_idx_CSTAT_LOCAL,
224 bpf_intf::cell_stat_idx_CSTAT_CPU_DSQ,
225 bpf_intf::cell_stat_idx_CSTAT_CELL_DSQ,
226 bpf_intf::cell_stat_idx_CSTAT_BORROWED,
227];
228
229#[derive(Debug)]
231struct Cell {
232 cpus: Cpumask,
233}
234
235struct Scheduler<'a> {
236 skel: BpfSkel<'a>,
237 monitor_interval: Duration,
238 cells: HashMap<u32, Cell>,
239 prev_cell_stats: [[u64; NR_CSTATS]; MAX_CELLS],
242 prev_cell_running_ns: [u64; MAX_CELLS],
244 prev_cell_own_ns: [u64; MAX_CELLS],
245 prev_cell_lent_ns: [u64; MAX_CELLS],
246 metrics: Metrics,
247 stats_server: Option<StatsServer<(), Metrics>>,
248 last_configuration_seq: Option<u32>,
249 last_cpuset_seq: u32,
251 cell_manager: CellManager,
253 enable_borrowing: bool,
255 enable_rebalancing: bool,
257 rebalance_threshold: f64,
259 rebalance_cooldown: Duration,
261 demand_smoothing: f64,
263 smoothed_util: [f64; MAX_CELLS],
265 last_rebalance: Instant,
267 rebalance_count: u64,
269 epoll: Epoll,
271 stats_waker: EventFd,
273}
274
275struct DistributionStats {
276 total_decisions: u64,
277 share_of_decisions_pct: f64,
278 local_q_pct: f64,
279 cpu_q_pct: f64,
280 cell_q_pct: f64,
281 borrowed_pct: f64,
282 affn_viol_pct: f64,
283 steal_pct: f64,
284 pin_skip_pct: f64,
285
286 global_queue_decisions: u64,
288}
289
290impl Display for DistributionStats {
291 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292 const MIN_DECISIONS_WIDTH: usize = 5;
296 let descisions_width = if self.global_queue_decisions > 0 {
297 max(
298 MIN_DECISIONS_WIDTH,
299 (self.global_queue_decisions as f64).log10().ceil() as usize,
300 )
301 } else {
302 MIN_DECISIONS_WIDTH
303 };
304 write!(
305 f,
306 "{: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}%",
307 self.total_decisions,
308 self.share_of_decisions_pct,
309 self.local_q_pct,
310 self.cpu_q_pct,
311 self.cell_q_pct,
312 self.borrowed_pct,
313 self.affn_viol_pct,
314 self.steal_pct,
315 self.pin_skip_pct,
316 width = descisions_width,
317 )
318 }
319}
320
321impl<'a> Scheduler<'a> {
322 fn managed_cell_parent<'b>(opts: &'b Opts) -> Result<&'b str> {
323 opts.cell_parent_cgroup
324 .as_deref()
325 .ok_or_else(|| anyhow!("--cell-parent-cgroup is required to run the scheduler"))
326 }
327
328 fn validate_args(_opts: &Opts) -> Result<()> {
329 Ok(())
330 }
331
332 fn init(opts: &Opts, open_object: &'a mut MaybeUninit<OpenObject>) -> Result<Self> {
333 Self::validate_args(opts).context("validating scheduler options")?;
334
335 let topology = Topology::new().context("detecting system topology")?;
336
337 let nr_llc = topology.all_llcs.len().max(1);
338
339 let mut skel_builder = BpfSkelBuilder::default();
340 skel_builder
341 .obj_builder
342 .debug(opts.log_level.contains("trace"));
343 init_libbpf_logging(None);
344 info!(
345 "Running scx_mitosis (build ID: {})",
346 build_id::full_version(env!("CARGO_PKG_VERSION"))
347 );
348
349 let open_opts = opts.libbpf.clone().into_bpf_open_opts();
350 let mut skel = scx_ops_open!(skel_builder, open_object, mitosis, open_opts)
351 .context("opening BPF skeleton")?;
352
353 skel.struct_ops.mitosis_mut().exit_dump_len = opts.exit_dump_len;
354
355 let rodata = skel
356 .maps
357 .rodata_data
358 .as_mut()
359 .expect("BUG: rodata_data missing after skel open");
360
361 rodata.slice_ns = scx_enums.SCX_SLICE_DFL;
362 rodata.debug_events_enabled = opts.debug_events;
363 rodata.exiting_task_workaround_enabled = opts.exiting_task_workaround;
364 rodata.cpu_controller_disabled = opts.cpu_controller_disabled;
365 rodata.dynamic_affinity_cpu_selection = opts.dynamic_affinity_cpu_selection;
366
367 if opts.slice_shrink_min_us >= opts.slice_shrink_max_us {
369 bail!(
370 "--slice-shrink-min-us ({}) must be less than --slice-shrink-max-us ({})",
371 opts.slice_shrink_min_us,
372 opts.slice_shrink_max_us
373 );
374 }
375 rodata.enable_slice_shrinking = opts.enable_slice_shrinking;
376 rodata.slice_shrink_max_ns = opts.slice_shrink_max_us * 1_000;
377 rodata.slice_shrink_multiplier = 2;
379 rodata.slice_shrink_min_ns = opts.slice_shrink_min_us * 1_000;
380
381 rodata.nr_possible_cpus = *NR_CPUS_POSSIBLE as u32;
382 for cpu in topology.all_cpus.keys() {
383 rodata.all_cpus[cpu / 8] |= 1 << (cpu % 8);
384 }
385
386 rodata.reject_multicpu_pinning = opts.reject_multicpu_pinning;
387
388 rodata.nr_llc = nr_llc as u32;
390 rodata.enable_llc_awareness = opts.enable_llc_awareness;
391
392 rodata.enable_borrowing = opts.enable_borrowing;
393 rodata.use_lockless_peek = opts.use_lockless_peek;
394
395 match *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP {
396 0 => info!("Kernel does not support queued wakeup optimization."),
397 v => skel.struct_ops.mitosis_mut().flags |= v,
398 }
399
400 mitosis_topology_utils::populate_topology_maps(
402 &mut skel,
403 mitosis_topology_utils::MapKind::CpuToLLC,
404 None,
405 )
406 .context("populating CPU-to-LLC topology map")?;
407 mitosis_topology_utils::populate_topology_maps(
408 &mut skel,
409 mitosis_topology_utils::MapKind::LLCToCpus,
410 None,
411 )
412 .context("populating LLC-to-CPUs topology map")?;
413
414 let skel = scx_ops_load!(skel, mitosis, uei).context("loading BPF skeleton")?;
415
416 let stats_server = StatsServer::new(stats::server_data())
417 .launch()
418 .context("launching stats server")?;
419
420 let parent_cgroup = Self::managed_cell_parent(opts)?;
421 let exclude: HashSet<String> = opts.cell_exclude.iter().cloned().collect();
422 let cpu_to_llc: HashMap<usize, usize> = topology
423 .all_cpus
424 .iter()
425 .map(|(&cpu, c)| (cpu, c.llc_id))
426 .collect();
427 let cell_manager = CellManager::new(
428 parent_cgroup,
429 MAX_CELLS as u32,
430 topology.span.clone(),
431 exclude,
432 opts.cell0_min_cpus,
433 cpu_to_llc,
434 )
435 .with_context(|| format!("initializing cell manager for cgroup {}", parent_cgroup))?;
436
437 let epoll = Epoll::new(EpollCreateFlags::empty()).context("creating epoll instance")?;
439
440 let stats_waker = EventFd::from_value_and_flags(
442 0,
443 nix::sys::eventfd::EfdFlags::EFD_NONBLOCK | nix::sys::eventfd::EfdFlags::EFD_SEMAPHORE,
444 )
445 .context("creating stats-waker eventfd")?;
446
447 epoll
449 .add(
450 &stats_waker,
451 EpollEvent::new(EpollFlags::EPOLLIN, STATS_TOKEN),
452 )
453 .context("registering stats-waker with epoll")?;
454
455 epoll
456 .add(
457 &cell_manager,
458 EpollEvent::new(EpollFlags::EPOLLIN, INOTIFY_TOKEN),
459 )
460 .context("registering cell manager inotify with epoll")?;
461
462 Ok(Self {
463 skel,
464 monitor_interval: Duration::from_secs(opts.monitor_interval_s),
465 cells: HashMap::new(),
466 prev_cell_stats: [[0; NR_CSTATS]; MAX_CELLS],
467 prev_cell_running_ns: [0; MAX_CELLS],
468 prev_cell_own_ns: [0; MAX_CELLS],
469 prev_cell_lent_ns: [0; MAX_CELLS],
470 metrics: Metrics::default(),
471 stats_server: Some(stats_server),
472 last_configuration_seq: None,
473 last_cpuset_seq: 0,
474 cell_manager,
475 enable_borrowing: opts.enable_borrowing,
476 enable_rebalancing: opts.enable_rebalancing,
477 rebalance_threshold: opts.rebalance_threshold,
478 rebalance_cooldown: Duration::from_secs(opts.rebalance_cooldown_s),
479 demand_smoothing: opts.demand_smoothing,
480 smoothed_util: [0.0; MAX_CELLS],
481 last_rebalance: Instant::now(),
482 rebalance_count: 0,
483 epoll,
484 stats_waker,
485 })
486 }
487
488 fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
489 let struct_ops = scx_ops_attach!(self.skel, mitosis).context("attaching BPF scheduler")?;
490
491 info!("Mitosis Scheduler Attached. Run `scx_mitosis --monitor` for metrics.");
492
493 self.apply_initial_cells()
495 .context("applying initial cell configuration")?;
496
497 let (res_ch, req_ch) = self
498 .stats_server
499 .as_ref()
500 .expect("BUG: stats_server missing after init")
501 .channels();
502
503 let stats_waker_fd = self
507 .stats_waker
508 .as_fd()
509 .try_clone_to_owned()
510 .context("cloning stats-waker fd for bridge thread")?;
511 let stats_waker = unsafe { EventFd::from_owned_fd(stats_waker_fd) };
512 let stats_bridge = std::thread::spawn(move || {
513 while req_ch.recv().is_ok() {
514 let _ = stats_waker.write(1);
516 }
517 });
518
519 while !shutdown.load(Ordering::Relaxed) && !uei_exited!(&self.skel, uei) {
520 let mut events = [EpollEvent::empty(); 1];
521 let timeout = EpollTimeout::try_from(self.monitor_interval).with_context(|| {
522 format!(
523 "monitor_interval {:?} exceeds maximum epoll timeout",
524 self.monitor_interval,
525 )
526 })?;
527
528 match self.epoll.wait(&mut events, timeout) {
529 Ok(n) => {
530 for event in &events[..n] {
531 match event.data() {
532 INOTIFY_TOKEN => {
533 self.process_cell_events()
535 .context("processing cell manager events")?;
536 }
537 STATS_TOKEN => {
538 let _ = self.stats_waker.read();
540 res_ch
541 .send(self.get_metrics())
542 .context("sending metrics response")?;
543 }
544 _ => {}
545 }
546 }
547 }
548 Err(nix::errno::Errno::EINTR) => continue,
549 Err(e) => return Err(e.into()),
550 }
551
552 self.refresh_bpf_cells()
554 .context("refreshing BPF cell state")?;
555 self.check_cpuset_changes()
556 .context("checking cpuset changes")?;
557 self.collect_metrics().context("collecting metrics")?;
558
559 if self.enable_rebalancing {
560 self.maybe_rebalance().context("running rebalance check")?;
561 }
562 }
563
564 drop(struct_ops);
565 drop(self.stats_server.take());
567 let _ = stats_bridge.join();
568 info!("Unregister {SCHEDULER_NAME} scheduler");
569 uei_report!(&self.skel, uei)
570 }
571
572 fn apply_initial_cells(&mut self) -> Result<()> {
574 let cpu_assignments = self
575 .compute_and_apply_cell_config(&[])
576 .context("computing initial cell configuration")?;
577
578 info!(
579 "Applied initial cell configuration: {}",
580 self.cell_manager.format_cell_config(&cpu_assignments)
581 );
582
583 Ok(())
584 }
585
586 fn process_cell_events(&mut self) -> Result<()> {
588 let (num_new, num_destroyed, new_cell_ids, destroyed_cell_ids) = {
589 let (new_cells, destroyed_cells) = self
590 .cell_manager
591 .process_events()
592 .context("processing inotify events")?;
593
594 if new_cells.is_empty() && destroyed_cells.is_empty() {
595 return Ok(());
596 }
597
598 let new_ids: Vec<u32> = new_cells.iter().map(|(_, cell_id)| *cell_id).collect();
599 (
600 new_cells.len(),
601 destroyed_cells.len(),
602 new_ids,
603 destroyed_cells,
604 )
605 };
606
607 for &cell_id in &destroyed_cell_ids {
610 self.smoothed_util[cell_id as usize] = 0.0;
611 }
612
613 let cpu_assignments = self
614 .compute_and_apply_cell_config(&new_cell_ids)
615 .context("recomputing cell configuration for new cgroups")?;
616
617 info!(
618 "Cell config updated ({} new, {} destroyed): {}",
619 num_new,
620 num_destroyed,
621 self.cell_manager.format_cell_config(&cpu_assignments)
622 );
623
624 Ok(())
625 }
626
627 fn compute_and_apply_cell_config(
636 &mut self,
637 new_cell_ids: &[u32],
638 ) -> Result<Vec<CpuAssignment>> {
639 let (cell_assignments, cpu_assignments) = {
640 let active_cell_ids: Vec<u32> = self
641 .cell_manager
642 .get_cell_assignments()
643 .iter()
644 .map(|(_, cell_id)| *cell_id)
645 .collect();
646 let all_cell_ids: Vec<u32> = std::iter::once(0)
648 .chain(active_cell_ids.iter().copied())
649 .collect();
650
651 let cpu_assignments = if self.enable_rebalancing {
652 let new_set: HashSet<u32> = new_cell_ids.iter().copied().collect();
654 let existing_utils: Vec<f64> = all_cell_ids
655 .iter()
656 .filter(|id| !new_set.contains(id))
657 .map(|&id| self.smoothed_util[id as usize])
658 .collect();
659
660 let has_data = existing_utils.iter().any(|&u| u > 0.0);
661
662 if has_data {
663 let avg_util: f64 =
665 existing_utils.iter().sum::<f64>() / existing_utils.len().max(1) as f64;
666 for &id in new_cell_ids {
667 self.smoothed_util[id as usize] = avg_util;
668 info!(
669 "Seeded new cell {} smoothed_util to average {:.1}%",
670 id, avg_util
671 );
672 }
673
674 let cell_demands: HashMap<u32, f64> = all_cell_ids
676 .iter()
677 .map(|&id| (id, self.smoothed_util[id as usize]))
678 .collect();
679
680 self.cell_manager
681 .compute_demand_cpu_assignments(&cell_demands, self.enable_borrowing)
682 .context("computing demand-weighted CPU assignments")?
683 } else {
684 self.cell_manager
686 .compute_cpu_assignments(self.enable_borrowing)
687 .context("computing equal-weight CPU assignments (no utilization data)")?
688 }
689 } else {
690 self.cell_manager
691 .compute_cpu_assignments(self.enable_borrowing)
692 .context("computing equal-weight CPU assignments (rebalancing disabled)")?
693 };
694
695 (self.cell_manager.get_cell_assignments(), cpu_assignments)
696 };
697
698 self.apply_cell_config(&cell_assignments, &cpu_assignments)
699 .context("applying cell configuration to BPF")?;
700
701 Ok(cpu_assignments)
702 }
703
704 fn maybe_rebalance(&mut self) -> Result<()> {
706 if self.last_rebalance.elapsed() < self.rebalance_cooldown {
708 return Ok(());
709 }
710
711 let active_cells: Vec<u32> = self.cells.keys().copied().collect();
713 if active_cells.len() < 2 {
714 return Ok(());
715 }
716
717 let mut min_util = f64::MAX;
718 let mut max_util = f64::MIN;
719 for &cell_id in &active_cells {
720 let util = self.smoothed_util[cell_id as usize];
721 if util < min_util {
722 min_util = util;
723 }
724 if util > max_util {
725 max_util = util;
726 }
727 }
728
729 let spread = max_util - min_util;
730 if spread < self.rebalance_threshold {
731 return Ok(());
732 }
733
734 let cell_demands: HashMap<u32, f64> = active_cells
736 .iter()
737 .map(|&cell_id| (cell_id, self.smoothed_util[cell_id as usize]))
738 .collect();
739
740 let (cell_assignments, cpu_assignments) = {
742 let cpu_assignments = self
743 .cell_manager
744 .compute_demand_cpu_assignments(&cell_demands, self.enable_borrowing)
745 .context("computing demand-weighted CPU assignments for rebalance")?;
746
747 let changed = cpu_assignments.iter().any(|a| {
748 self.cells
749 .get(&a.cell_id)
750 .map_or(true, |cell| cell.cpus != a.primary)
751 });
752
753 if !changed {
754 return Ok(());
755 }
756
757 (self.cell_manager.get_cell_assignments(), cpu_assignments)
758 };
759
760 self.apply_cell_config(&cell_assignments, &cpu_assignments)
761 .context("applying rebalanced cell configuration to BPF")?;
762
763 self.last_rebalance = Instant::now();
764 self.rebalance_count += 1;
765 self.metrics.rebalance_count = self.rebalance_count;
766
767 info!(
768 "Rebalanced CPUs (spread={:.1}%, count={}): {}",
769 spread,
770 self.rebalance_count,
771 self.cell_manager.format_cell_config(&cpu_assignments)
772 );
773
774 Ok(())
775 }
776
777 fn apply_cell_config(
782 &mut self,
783 cell_assignments: &[(u64, u32)],
784 cpu_assignments: &[CpuAssignment],
785 ) -> Result<()> {
786 let bss_data = self
787 .skel
788 .maps
789 .bss_data
790 .as_mut()
791 .expect("bss_data must be available after scheduler load");
792
793 let config = &mut bss_data.cell_config;
794
795 unsafe {
803 std::ptr::write_bytes(
804 config as *mut _ as *mut u8,
805 0,
806 std::mem::size_of_val(config),
807 );
808 }
809
810 if cell_assignments.len() > bpf_intf::consts_MAX_CELLS as usize {
811 bail!(
812 "Too many cell assignments: {} > MAX_CELLS ({})",
813 cell_assignments.len(),
814 bpf_intf::consts_MAX_CELLS
815 );
816 }
817 config.num_cell_assignments = cell_assignments.len() as u32;
818
819 for (i, (cgid, cell_id)) in cell_assignments.iter().enumerate() {
820 config.assignments[i].cgid = *cgid;
821 config.assignments[i].cell_id = *cell_id;
822 }
823
824 let mut max_cell_id: u32 = 0;
826 for a in cpu_assignments {
827 if a.cell_id >= bpf_intf::consts_MAX_CELLS {
828 bail!(
829 "Cell ID {} exceeds MAX_CELLS ({})",
830 a.cell_id,
831 bpf_intf::consts_MAX_CELLS
832 );
833 }
834 max_cell_id = max_cell_id.max(a.cell_id + 1);
835
836 write_cpumask_to_config(&a.primary, &mut config.cpumasks[a.cell_id as usize].mask);
837
838 if let Some(ref borrowable) = a.borrowable {
839 write_cpumask_to_config(
840 borrowable,
841 &mut config.borrowable_cpumasks[a.cell_id as usize].mask,
842 );
843 }
844 }
845 config.num_cells = max_cell_id;
846
847 let prog = &mut self.skel.progs.apply_cell_config;
849 let out = prog
850 .test_run(ProgramInput::default())
851 .context("Failed to run apply_cell_config BPF program")?;
852 if out.return_value != 0 {
853 bail!(
854 "apply_cell_config BPF program returned error {} (num_assignments={}, num_cells={})",
855 out.return_value as i32,
856 cell_assignments.len(),
857 cpu_assignments.len()
858 );
859 }
860
861 Ok(())
862 }
863
864 fn get_metrics(&self) -> Metrics {
865 self.metrics.clone()
866 }
867
868 fn calculate_distribution_stats(
869 &self,
870 queue_counts: &[u64; QUEUE_STATS_IDX.len()],
871 global_queue_decisions: u64,
872 scope_queue_decisions: u64,
873 scope_affn_viols: u64,
874 scope_steals: u64,
875 scope_pin_skips: u64,
876 ) -> Result<DistributionStats> {
877 let share_of_global =
880 100.0 * (scope_queue_decisions as f64) / (global_queue_decisions as f64);
881
882 let queue_pct = if scope_queue_decisions == 0 {
884 debug!("No queue decisions in scope, zeroing out queue distribution");
885 [0.0; QUEUE_STATS_IDX.len()]
886 } else {
887 core::array::from_fn(|i| {
888 100.0 * (queue_counts[i] as f64) / (scope_queue_decisions as f64)
889 })
890 };
891
892 let affinity_violations_percent = if scope_queue_decisions == 0 {
894 debug!("No queue decisions in scope, zeroing out affinity violations");
895 0.0
896 } else {
897 100.0 * (scope_affn_viols as f64) / (scope_queue_decisions as f64)
898 };
899
900 let steal_pct = if scope_queue_decisions == 0 {
901 0.0
902 } else {
903 100.0 * (scope_steals as f64) / (scope_queue_decisions as f64)
904 };
905
906 let pin_skip_pct = if scope_queue_decisions == 0 {
907 0.0
908 } else {
909 100.0 * (scope_pin_skips as f64) / (scope_queue_decisions as f64)
910 };
911
912 const EXPECTED_QUEUES: usize = 4;
913 if queue_pct.len() != EXPECTED_QUEUES {
914 bail!(
915 "Expected {} queues, got {}",
916 EXPECTED_QUEUES,
917 queue_pct.len()
918 );
919 }
920
921 return Ok(DistributionStats {
922 total_decisions: scope_queue_decisions,
923 share_of_decisions_pct: share_of_global,
924 local_q_pct: queue_pct[0],
925 cpu_q_pct: queue_pct[1],
926 cell_q_pct: queue_pct[2],
927 borrowed_pct: queue_pct[3],
928 affn_viol_pct: affinity_violations_percent,
929 steal_pct,
930 pin_skip_pct,
931 global_queue_decisions,
932 });
933 }
934
935 fn update_and_log_global_queue_stats(
937 &mut self,
938 global_queue_decisions: u64,
939 cell_stats_delta: &[[u64; NR_CSTATS]; MAX_CELLS],
940 ) -> Result<()> {
941 let mut queue_counts = [0; QUEUE_STATS_IDX.len()];
943 for cells in 0..MAX_CELLS {
944 for (i, stat) in QUEUE_STATS_IDX.iter().enumerate() {
945 queue_counts[i] += cell_stats_delta[cells][*stat as usize];
946 }
947 }
948
949 let prefix = "Total Decisions:";
950
951 let scope_affn_viols: u64 = cell_stats_delta
953 .iter()
954 .map(|&cell| cell[bpf_intf::cell_stat_idx_CSTAT_AFFN_VIOL as usize])
955 .sum::<u64>();
956
957 let scope_steals: u64 = cell_stats_delta
959 .iter()
960 .map(|&cell| cell[bpf_intf::cell_stat_idx_CSTAT_STEAL as usize])
961 .sum::<u64>();
962
963 let scope_pin_skips: u64 = cell_stats_delta
965 .iter()
966 .map(|&cell| cell[bpf_intf::cell_stat_idx_CSTAT_PIN_SKIP as usize])
967 .sum::<u64>();
968
969 let stats = self
971 .calculate_distribution_stats(
972 &queue_counts,
973 global_queue_decisions,
974 global_queue_decisions,
975 scope_affn_viols,
976 scope_steals,
977 scope_pin_skips,
978 )
979 .context("calculating global queue distribution stats")?;
980
981 self.metrics.update(&stats);
982
983 let sum = |idx: usize| -> u64 { cell_stats_delta.iter().map(|c| c[idx]).sum() };
985 self.metrics.drain_cnt = sum(bpf_intf::cell_stat_idx_CSTAT_DRAIN_CNT as usize);
986 self.metrics.slice_shrink_max =
987 sum(bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_MAX as usize);
988 self.metrics.slice_shrink_proportional =
989 sum(bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_PROPORTIONAL as usize);
990 self.metrics.slice_shrink_min =
991 sum(bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_MIN as usize);
992 self.metrics.slice_shrink = self.metrics.slice_shrink_max
993 + self.metrics.slice_shrink_proportional
994 + self.metrics.slice_shrink_min;
995
996 trace!("{} {}", prefix, stats);
997
998 Ok(())
999 }
1000
1001 fn update_and_log_cell_queue_stats(
1003 &mut self,
1004 global_queue_decisions: u64,
1005 cell_stats_delta: &[[u64; NR_CSTATS]; MAX_CELLS],
1006 ) -> Result<()> {
1007 for cell in 0..MAX_CELLS {
1008 let cell_queue_decisions = QUEUE_STATS_IDX
1009 .iter()
1010 .map(|&stat| cell_stats_delta[cell][stat as usize])
1011 .sum::<u64>();
1012
1013 if cell_queue_decisions == 0 {
1015 continue;
1016 }
1017
1018 let mut queue_counts = [0; QUEUE_STATS_IDX.len()];
1019 for (i, &stat) in QUEUE_STATS_IDX.iter().enumerate() {
1020 queue_counts[i] = cell_stats_delta[cell][stat as usize];
1021 }
1022
1023 const MIN_CELL_WIDTH: usize = 2;
1024 let cell_width: usize = max(MIN_CELL_WIDTH, (MAX_CELLS as f64).log10().ceil() as usize);
1025
1026 let prefix = format!(" Cell {:width$}:", cell, width = cell_width);
1027
1028 let scope_affn_viols: u64 =
1030 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_AFFN_VIOL as usize];
1031
1032 let scope_steals: u64 =
1034 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_STEAL as usize];
1035
1036 let scope_pin_skips: u64 =
1038 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_PIN_SKIP as usize];
1039
1040 let stats = self
1041 .calculate_distribution_stats(
1042 &queue_counts,
1043 global_queue_decisions,
1044 cell_queue_decisions,
1045 scope_affn_viols,
1046 scope_steals,
1047 scope_pin_skips,
1048 )
1049 .with_context(|| {
1050 format!("calculating queue distribution stats for cell {}", cell)
1051 })?;
1052
1053 let cell_metrics = self.metrics.cells.entry(cell as u32).or_default();
1054 cell_metrics.update(&stats);
1055
1056 cell_metrics.drain_cnt =
1058 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_DRAIN_CNT as usize];
1059 cell_metrics.slice_shrink_max =
1060 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_MAX as usize];
1061 cell_metrics.slice_shrink_proportional = cell_stats_delta[cell]
1062 [bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_PROPORTIONAL as usize];
1063 cell_metrics.slice_shrink_min =
1064 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_MIN as usize];
1065 cell_metrics.slice_shrink = cell_metrics.slice_shrink_max
1066 + cell_metrics.slice_shrink_proportional
1067 + cell_metrics.slice_shrink_min;
1068
1069 trace!("{} {}", prefix, stats);
1070 }
1071 Ok(())
1072 }
1073
1074 fn update_drain_metrics(&mut self, cell_stats_delta: &[[u64; NR_CSTATS]; MAX_CELLS]) {
1075 let mut total = 0;
1076
1077 for cell in 0..MAX_CELLS {
1078 let drain_cnt =
1079 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_DRAIN_CNT as usize];
1080 total += drain_cnt;
1081
1082 if let Some(cell_metrics) = self.metrics.cells.get_mut(&(cell as u32)) {
1083 cell_metrics.drain_cnt = drain_cnt;
1084 } else if drain_cnt > 0 {
1085 self.metrics.cells.entry(cell as u32).or_default().drain_cnt = drain_cnt;
1086 }
1087 }
1088
1089 self.metrics.drain_cnt = total;
1090 }
1091
1092 fn log_all_queue_stats(
1093 &mut self,
1094 cell_stats_delta: &[[u64; NR_CSTATS]; MAX_CELLS],
1095 ) -> Result<()> {
1096 let global_queue_decisions: u64 = cell_stats_delta
1098 .iter()
1099 .flat_map(|cell| QUEUE_STATS_IDX.iter().map(|&idx| cell[idx as usize]))
1100 .sum();
1101
1102 self.update_drain_metrics(cell_stats_delta);
1103
1104 if global_queue_decisions == 0 {
1105 if self.metrics.drain_cnt == 0 {
1106 warn!("No queueing decisions made globally");
1107 }
1108 return Ok(());
1109 }
1110
1111 self.update_and_log_global_queue_stats(global_queue_decisions, &cell_stats_delta)
1112 .context("updating global queue stats")?;
1113
1114 self.update_and_log_cell_queue_stats(global_queue_decisions, &cell_stats_delta)
1115 .context("updating per-cell queue stats")?;
1116
1117 Ok(())
1118 }
1119
1120 fn calculate_cell_stat_delta(
1121 &mut self,
1122 cpu_ctxs: &[bpf_intf::cpu_ctx],
1123 ) -> Result<[[u64; NR_CSTATS]; MAX_CELLS]> {
1124 let mut cell_stats_delta = [[0 as u64; NR_CSTATS]; MAX_CELLS];
1125
1126 for cell in 0..MAX_CELLS {
1129 for stat in 0..NR_CSTATS {
1130 let mut cur_cell_stat = 0;
1131
1132 for cpu_ctx in cpu_ctxs.iter() {
1134 cur_cell_stat += cpu_ctx.cstats[cell][stat];
1135 }
1136
1137 cell_stats_delta[cell][stat] = cur_cell_stat - self.prev_cell_stats[cell][stat];
1139 self.prev_cell_stats[cell][stat] = cur_cell_stat;
1140 }
1141 }
1142 Ok(cell_stats_delta)
1143 }
1144
1145 fn collect_metrics(&mut self) -> Result<()> {
1147 let cpu_ctxs = read_cpu_ctxs(&self.skel).context("reading per-CPU contexts for metrics")?;
1148
1149 let cell_stats_delta = self
1150 .calculate_cell_stat_delta(&cpu_ctxs)
1151 .context("calculating cell stat deltas")?;
1152
1153 self.log_all_queue_stats(&cell_stats_delta)
1154 .context("logging queue stats")?;
1155
1156 self.metrics.enforced_holdout = self.cell_manager.enforced_holdout() as u64;
1159
1160 self.collect_demand_metrics(&cpu_ctxs)
1161 .context("collecting demand metrics")?;
1162
1163 for (cell_id, cell) in &self.cells {
1164 trace!("CELL[{}]: {}", cell_id, cell.cpus);
1165 }
1166
1167 for (cell_id, cell) in self.cells.iter() {
1168 self.metrics
1170 .cells
1171 .entry(*cell_id)
1172 .and_modify(|cell_metrics| {
1173 cell_metrics.num_cpus = cell.cpus.weight() as u32;
1174 cell_metrics.cgroup_path = self.cell_manager.cgroup_path_for_cell(*cell_id);
1175 });
1176 }
1177 self.metrics.num_cells = self.cells.len() as u32;
1178
1179 Ok(())
1180 }
1181
1182 fn collect_demand_metrics(&mut self, cpu_ctxs: &[bpf_intf::cpu_ctx]) -> Result<()> {
1184 let mut total_running_ns = [0u64; MAX_CELLS];
1189 let mut on_own_ns = [0u64; MAX_CELLS];
1190 let mut lent_ns = [0u64; MAX_CELLS];
1191
1192 for cpu_ctx in cpu_ctxs.iter() {
1193 let owner = cpu_ctx.cell as usize;
1194 for cell in 0..MAX_CELLS {
1195 let ns = cpu_ctx.running_ns[cell];
1196 total_running_ns[cell] += ns;
1197 if owner == cell {
1198 on_own_ns[cell] += ns;
1199 }
1200 }
1201 if owner >= MAX_CELLS {
1202 bail!(
1203 "CPU has invalid cell assignment {} (MAX_CELLS={})",
1204 owner,
1205 MAX_CELLS
1206 );
1207 }
1208 let total_on_cpu: u64 = cpu_ctx.running_ns.iter().sum();
1210 let owner_on_cpu = cpu_ctx.running_ns[owner];
1211 lent_ns[owner] += total_on_cpu.saturating_sub(owner_on_cpu);
1212 }
1213
1214 let interval_ns = self.monitor_interval.as_nanos() as u64;
1216
1217 let mut global_running_delta = 0u64;
1218 let mut global_borrowed_delta = 0u64;
1219 let mut global_lent_delta = 0u64;
1220 let mut global_capacity = 0u64;
1221
1222 for cell in 0..MAX_CELLS {
1223 let delta_running =
1224 total_running_ns[cell].saturating_sub(self.prev_cell_running_ns[cell]);
1225 let delta_on_own = on_own_ns[cell].saturating_sub(self.prev_cell_own_ns[cell]);
1226 let delta_lent = lent_ns[cell].saturating_sub(self.prev_cell_lent_ns[cell]);
1227
1228 self.prev_cell_running_ns[cell] = total_running_ns[cell];
1229 self.prev_cell_own_ns[cell] = on_own_ns[cell];
1230 self.prev_cell_lent_ns[cell] = lent_ns[cell];
1231
1232 if delta_running == 0 && delta_lent == 0 {
1233 continue;
1234 }
1235
1236 let delta_borrowed = delta_running.saturating_sub(delta_on_own);
1238
1239 let Some(cell_info) = self.cells.get(&(cell as u32)) else {
1242 continue;
1243 };
1244
1245 let nr_cpus = cell_info.cpus.weight() as u64;
1246 if nr_cpus == 0 {
1247 bail!("Cell {} has 0 CPUs assigned", cell);
1248 }
1249
1250 let capacity = nr_cpus * interval_ns;
1252 let util_pct = 100.0 * (delta_running as f64) / (capacity as f64);
1254 let demand_borrow_pct = if delta_running > 0 {
1256 100.0 * (delta_borrowed as f64) / (delta_running as f64)
1257 } else {
1258 0.0
1259 };
1260 let lent_pct = 100.0 * (delta_lent as f64) / (capacity as f64);
1262
1263 if self.enable_rebalancing {
1265 self.smoothed_util[cell] = self.demand_smoothing * util_pct
1266 + (1.0 - self.demand_smoothing) * self.smoothed_util[cell];
1267 }
1268
1269 self.metrics
1270 .cells
1271 .entry(cell as u32)
1272 .or_default()
1273 .update_demand(util_pct, demand_borrow_pct, lent_pct);
1274
1275 if self.enable_rebalancing {
1277 self.metrics
1278 .cells
1279 .entry(cell as u32)
1280 .or_default()
1281 .smoothed_util_pct = self.smoothed_util[cell];
1282 }
1283
1284 global_running_delta = global_running_delta.saturating_add(delta_running);
1285 global_borrowed_delta = global_borrowed_delta.saturating_add(delta_borrowed);
1286 global_lent_delta = global_lent_delta.saturating_add(delta_lent);
1287 global_capacity = global_capacity.saturating_add(capacity);
1288 }
1289
1290 let global_util_pct = if global_capacity > 0 {
1291 100.0 * (global_running_delta as f64) / (global_capacity as f64)
1292 } else {
1293 0.0
1294 };
1295 let global_borrow_pct = if global_running_delta > 0 {
1296 100.0 * (global_borrowed_delta as f64) / (global_running_delta as f64)
1297 } else {
1298 0.0
1299 };
1300 let global_lent_pct = if global_capacity > 0 {
1301 100.0 * (global_lent_delta as f64) / (global_capacity as f64)
1302 } else {
1303 0.0
1304 };
1305
1306 self.metrics
1307 .update_demand(global_util_pct, global_borrow_pct, global_lent_pct);
1308
1309 Ok(())
1310 }
1311
1312 fn update_applied_cpuset_seq(&mut self) {
1314 unsafe {
1315 let ptr = &mut self
1316 .skel
1317 .maps
1318 .bss_data
1319 .as_mut()
1320 .expect("BUG: bss_data missing after scheduler load")
1321 .applied_cpuset_seq as *mut u32;
1322 std::ptr::write_volatile(ptr, self.last_cpuset_seq);
1323 }
1324 }
1325
1326 fn check_cpuset_changes(&mut self) -> Result<()> {
1328 let current_seq = unsafe {
1329 let ptr = &self
1330 .skel
1331 .maps
1332 .bss_data
1333 .as_ref()
1334 .expect("BUG: bss_data missing after scheduler load")
1335 .cpuset_seq as *const u32;
1336 (ptr as *const AtomicU32)
1337 .as_ref()
1338 .expect("BUG: cpuset_seq pointer cast yielded null")
1339 .load(Ordering::Acquire)
1340 };
1341
1342 if current_seq == self.last_cpuset_seq {
1343 return Ok(());
1344 }
1345 self.last_cpuset_seq = current_seq;
1346
1347 if !self
1348 .cell_manager
1349 .refresh_cpusets()
1350 .context("refreshing cell cpusets")?
1351 {
1352 self.update_applied_cpuset_seq();
1354 return Ok(());
1355 }
1356
1357 let cpu_assignments = self
1358 .compute_and_apply_cell_config(&[])
1359 .context("recomputing cell configuration after cpuset change")?;
1360 self.update_applied_cpuset_seq();
1361 info!(
1362 "Cpuset change detected, recomputed config: {}",
1363 self.cell_manager.format_cell_config(&cpu_assignments)
1364 );
1365 Ok(())
1366 }
1367
1368 fn refresh_bpf_cells(&mut self) -> Result<()> {
1369 let applied_configuration = unsafe {
1370 let ptr = &self
1371 .skel
1372 .maps
1373 .bss_data
1374 .as_ref()
1375 .expect("BUG: bss_data missing after scheduler load")
1376 .applied_configuration_seq as *const u32;
1377 (ptr as *const std::sync::atomic::AtomicU32)
1378 .as_ref()
1379 .expect("BUG: applied_configuration_seq pointer cast yielded null")
1380 .load(std::sync::atomic::Ordering::Acquire)
1381 };
1382 if self
1383 .last_configuration_seq
1384 .is_some_and(|seq| applied_configuration == seq)
1385 {
1386 return Ok(());
1387 }
1388 let mut cell_to_cpus: HashMap<u32, Cpumask> = HashMap::new();
1390 let cpu_ctxs =
1391 read_cpu_ctxs(&self.skel).context("reading per-CPU contexts for BPF cell refresh")?;
1392 for (i, cpu_ctx) in cpu_ctxs.iter().enumerate() {
1393 cell_to_cpus
1394 .entry(cpu_ctx.cell)
1395 .or_insert_with(|| Cpumask::new())
1396 .set_cpu(i)
1397 .expect("set cpu in existing mask");
1398 }
1399
1400 let cells_with_cpus: HashSet<u32> = cell_to_cpus.keys().copied().collect();
1411 let mut active_cells = cells_with_cpus.clone();
1412 active_cells.insert(0);
1413
1414 for cell_idx in &active_cells {
1415 let cpus = cell_to_cpus
1416 .get(cell_idx)
1417 .cloned()
1418 .unwrap_or_else(|| Cpumask::new());
1419 self.cells
1420 .entry(*cell_idx)
1421 .or_insert_with(|| Cell {
1422 cpus: Cpumask::new(),
1423 })
1424 .cpus = cpus;
1425 self.metrics.cells.insert(*cell_idx, CellMetrics::default());
1426 }
1427
1428 self.cells.retain(|&k, _| active_cells.contains(&k));
1430 self.metrics.cells.retain(|&k, _| active_cells.contains(&k));
1431
1432 self.last_configuration_seq = Some(applied_configuration);
1433
1434 Ok(())
1435 }
1436}
1437
1438fn write_cpumask_to_config(cpumask: &Cpumask, dest: &mut [u8]) {
1439 let raw_slice = cpumask.as_raw_slice();
1440 for (word_idx, word) in raw_slice.iter().enumerate() {
1441 let byte_start = word_idx * 8;
1442 let bytes = word.to_le_bytes();
1443 for (j, byte) in bytes.iter().enumerate() {
1444 let idx = byte_start + j;
1445 if idx < dest.len() {
1446 dest[idx] = *byte;
1447 }
1448 }
1449 }
1450}
1451
1452fn read_cpu_ctxs(skel: &BpfSkel) -> Result<Vec<bpf_intf::cpu_ctx>> {
1453 let mut cpu_ctxs = vec![];
1454 let cpu_ctxs_vec = skel
1455 .maps
1456 .cpu_ctxs
1457 .lookup_percpu(&0u32.to_ne_bytes(), libbpf_rs::MapFlags::ANY)
1458 .context("Failed to lookup cpu_ctx")?
1459 .expect("BUG: cpu_ctxs lookup_percpu returned None for key 0");
1460 if cpu_ctxs_vec.len() < *NR_CPUS_POSSIBLE {
1461 bail!(
1462 "Percpu map returned {} entries but expected {}",
1463 cpu_ctxs_vec.len(),
1464 *NR_CPUS_POSSIBLE
1465 );
1466 }
1467 for cpu in 0..*NR_CPUS_POSSIBLE {
1468 cpu_ctxs.push(*unsafe {
1469 &*(cpu_ctxs_vec[cpu].as_slice().as_ptr() as *const bpf_intf::cpu_ctx)
1470 });
1471 }
1472 Ok(cpu_ctxs)
1473}
1474
1475#[clap_main::clap_main]
1476fn main(opts: Opts) -> Result<()> {
1477 if opts.version {
1478 println!(
1479 "scx_mitosis {}",
1480 build_id::full_version(env!("CARGO_PKG_VERSION"))
1481 );
1482 return Ok(());
1483 }
1484
1485 let env_filter = EnvFilter::try_from_default_env()
1486 .or_else(|_| match EnvFilter::try_new(&opts.log_level) {
1487 Ok(filter) => Ok(filter),
1488 Err(e) => {
1489 eprintln!(
1490 "invalid log envvar: {}, using info, err is: {}",
1491 opts.log_level, e
1492 );
1493 EnvFilter::try_new("info")
1494 }
1495 })
1496 .unwrap_or_else(|_| EnvFilter::new("info"));
1497
1498 match tracing_subscriber::fmt()
1499 .with_env_filter(env_filter)
1500 .with_target(true)
1501 .with_thread_ids(true)
1502 .with_file(true)
1503 .with_line_number(true)
1504 .try_init()
1505 {
1506 Ok(()) => {}
1507 Err(e) => eprintln!("failed to init logger: {}", e),
1508 }
1509
1510 if opts.verbose > 0 {
1511 warn!("Setting verbose via -v is deprecated and will be an error in future releases.");
1512 }
1513
1514 debug!("opts={:?}", &opts);
1515
1516 if let Some(run_id) = opts.run_id {
1517 info!("scx_mitosis run_id: {}", run_id);
1518 }
1519
1520 let shutdown = Arc::new(AtomicBool::new(false));
1521 let shutdown_clone = shutdown.clone();
1522 ctrlc::set_handler(move || {
1523 shutdown_clone.store(true, Ordering::Relaxed);
1524 })
1525 .context("Error setting Ctrl-C handler")?;
1526
1527 if let Some(intv) = opts.monitor {
1528 let shutdown_clone = shutdown.clone();
1529 let jh = std::thread::spawn(move || {
1530 match stats::monitor(Duration::from_secs_f64(intv), shutdown_clone) {
1531 Ok(_) => {
1532 debug!("stats monitor thread finished successfully")
1533 }
1534 Err(error_object) => {
1535 warn!(
1536 "stats monitor thread finished because of an error {}",
1537 error_object
1538 )
1539 }
1540 }
1541 });
1542 if opts.monitor.is_some() {
1543 let _ = jh.join();
1544 return Ok(());
1545 }
1546 }
1547
1548 let mut open_object = MaybeUninit::uninit();
1549 loop {
1550 let mut sched =
1551 Scheduler::init(&opts, &mut open_object).context("initializing scheduler")?;
1552 if !sched
1553 .run(shutdown.clone())
1554 .context("running scheduler main loop")?
1555 .should_restart()
1556 {
1557 break;
1558 }
1559 }
1560
1561 Ok(())
1562}
1563
1564#[cfg(test)]
1565mod tests {
1566 use super::Opts;
1567 use clap::Parser;
1568
1569 #[test]
1570 fn requires_cell_parent_cgroup_for_scheduler_mode() {
1571 assert!(Opts::try_parse_from(["scx_mitosis"]).is_err());
1572 }
1573
1574 #[test]
1575 fn allows_monitor_without_cell_parent_cgroup() {
1576 assert!(Opts::try_parse_from(["scx_mitosis", "--monitor", "1"]).is_ok());
1577 }
1578
1579 #[test]
1580 fn allows_version_without_cell_parent_cgroup() {
1581 assert!(Opts::try_parse_from(["scx_mitosis", "--version"]).is_ok());
1582 }
1583}