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