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::Arc;
24use std::sync::atomic::AtomicBool;
25use std::sync::atomic::AtomicU32;
26use std::sync::atomic::Ordering;
27use std::time::Duration;
28use std::time::Instant;
29
30use anyhow::Context;
31use anyhow::Result;
32use anyhow::anyhow;
33use anyhow::bail;
34use clap::Parser;
35use libbpf_rs::OpenObject;
36use libbpf_rs::ProgramInput;
37use libbpf_rs::skel::Skel as _;
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::Cpumask;
43use scx_utils::NR_CPU_IDS;
44use scx_utils::NR_CPUS_POSSIBLE;
45use scx_utils::Topology;
46use scx_utils::UserExitInfo;
47use scx_utils::build_id;
48use scx_utils::compat;
49use scx_utils::init_libbpf_logging;
50use scx_utils::libbpf_clap_opts::LibbpfOpts;
51use scx_utils::scx_enums;
52use scx_utils::scx_ops_attach;
53use scx_utils::scx_ops_cid_load;
54use scx_utils::scx_ops_cid_open;
55use scx_utils::uei_exited;
56use scx_utils::uei_report;
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 #[allow(dead_code)]
133 #[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; 5] = [
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 bpf_intf::cell_stat_idx_CSTAT_SUB_DISPATCH,
233];
234
235#[derive(Debug)]
237struct Cell {
238 cpus: Cpumask,
239}
240
241struct Scheduler<'a> {
242 _arenalib: ArenaLib,
243 skel: BpfSkel<'a>,
244 monitor_interval: Duration,
245 cells: HashMap<u32, Cell>,
246 prev_cell_stats: [[u64; NR_CSTATS]; MAX_CELLS],
249 prev_cell_running_ns: [u64; MAX_CELLS],
251 prev_cell_own_ns: [u64; MAX_CELLS],
252 prev_cell_lent_ns: [u64; MAX_CELLS],
253 demand_sampled_at: Option<Instant>,
255 metrics: Metrics,
256 stats_server: Option<StatsServer<(), Metrics>>,
257 last_configuration_seq: Option<u32>,
258 last_cpuset_seq: u32,
260 cell_manager: CellManager,
262 enable_borrowing: bool,
264 enable_rebalancing: bool,
266 rebalance_threshold: f64,
268 rebalance_cooldown: Duration,
270 demand_smoothing: f64,
272 smoothed_util: [f64; MAX_CELLS],
274 last_rebalance: Instant,
276 rebalance_count: u64,
278 epoll: Epoll,
280 stats_waker: EventFd,
282}
283
284struct DistributionStats {
285 total_decisions: u64,
286 share_of_decisions_pct: f64,
287 local_q_pct: f64,
288 cpu_q_pct: f64,
289 cell_q_pct: f64,
290 borrowed_pct: f64,
291 sub_q_pct: f64,
292 affn_viol_pct: f64,
293 steal_pct: f64,
294 pin_skip_pct: f64,
295 borrow_bounce_pct: f64,
296
297 global_queue_decisions: u64,
299}
300
301impl Display for DistributionStats {
302 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303 const MIN_DECISIONS_WIDTH: usize = 5;
307 let descisions_width = if self.global_queue_decisions > 0 {
308 max(
309 MIN_DECISIONS_WIDTH,
310 (self.global_queue_decisions as f64).log10().ceil() as usize,
311 )
312 } else {
313 MIN_DECISIONS_WIDTH
314 };
315 write!(
316 f,
317 "{:width$} {:5.1}% | Local:{:4.1}% From: CPU:{:4.1}% Cell:{:4.1}% Borrow:{:4.1}% Sub:{:4.1}% | V:{:4.1}% S:{:4.1}% PS:{:4.1}% Bounce:{:4.1}%",
318 self.total_decisions,
319 self.share_of_decisions_pct,
320 self.local_q_pct,
321 self.cpu_q_pct,
322 self.cell_q_pct,
323 self.borrowed_pct,
324 self.sub_q_pct,
325 self.affn_viol_pct,
326 self.steal_pct,
327 self.pin_skip_pct,
328 self.borrow_bounce_pct,
329 width = descisions_width,
330 )
331 }
332}
333
334impl<'a> Scheduler<'a> {
335 fn managed_cell_parent(opts: &Opts) -> Result<&str> {
336 opts.cell_parent_cgroup
337 .as_deref()
338 .ok_or_else(|| anyhow!("--cell-parent-cgroup is required to run the scheduler"))
339 }
340
341 fn validate_args(_opts: &Opts) -> Result<()> {
342 Ok(())
343 }
344
345 fn init(opts: &Opts, open_object: &'a mut MaybeUninit<OpenObject>) -> Result<Self> {
346 Self::validate_args(opts).context("validating scheduler options")?;
347
348 let topology = Topology::new().context("detecting system topology")?;
349
350 let mut skel_builder = BpfSkelBuilder::default();
351 skel_builder
352 .obj_builder
353 .debug(opts.log_level.contains("trace"));
354 init_libbpf_logging(None);
355 info!(
356 "Running scx_nitosis (build ID: {})",
357 build_id::full_version(env!("CARGO_PKG_VERSION"))
358 );
359
360 let open_opts = opts.libbpf.clone().into_bpf_open_opts();
361 let mut skel = scx_ops_cid_open!(skel_builder, open_object, mitosis, open_opts)
362 .context("opening BPF skeleton")?;
363
364 skel.struct_ops.mitosis_mut().exit_dump_len = opts.exit_dump_len;
365
366 let rodata = skel
367 .maps
368 .rodata_data
369 .as_mut()
370 .expect("BUG: rodata_data missing after skel open");
371
372 rodata.slice_ns = scx_enums.SCX_SLICE_DFL;
373 rodata.exiting_task_workaround_enabled = opts.exiting_task_workaround;
374 rodata.dynamic_affinity_cpu_selection = opts.dynamic_affinity_cpu_selection;
375
376 if opts.slice_shrink_min_us >= opts.slice_shrink_max_us {
378 bail!(
379 "--slice-shrink-min-us ({}) must be less than --slice-shrink-max-us ({})",
380 opts.slice_shrink_min_us,
381 opts.slice_shrink_max_us
382 );
383 }
384 rodata.enable_slice_shrinking = opts.enable_slice_shrinking;
385 rodata.slice_shrink_max_ns = opts.slice_shrink_max_us * 1_000;
386 rodata.slice_shrink_multiplier = 2;
388 rodata.slice_shrink_min_ns = opts.slice_shrink_min_us * 1_000;
389
390 rodata.nr_possible_cpus = *NR_CPUS_POSSIBLE as u32;
391 rodata.nr_cpu_ids = *NR_CPU_IDS as u32;
392
393 rodata.reject_multicpu_pinning = opts.reject_multicpu_pinning;
394
395 rodata.enable_llc_awareness = opts.enable_llc_awareness;
396
397 rodata.enable_borrowing = opts.enable_borrowing;
398 rodata.use_lockless_peek = opts.use_lockless_peek;
399
400 match *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP {
401 0 => info!("Kernel does not support queued wakeup optimization."),
402 v => skel.struct_ops.mitosis_mut().flags |= v,
403 }
404
405 let mitosis_topology = MitosisTopology::new(&topology);
406
407 let mut skel = scx_ops_cid_load!(skel, mitosis, uei).context("loading BPF skeleton")?;
408
409 let task_size = std::mem::size_of::<types::task_ctx>();
411 let arenalib = ArenaLib::setup(
412 skel.object_mut(),
413 task_size,
414 scx_arena::CACHELINE_SIZE,
415 *NR_CPU_IDS,
416 )
417 .context("setting up arenas")?;
418
419 let stats_server = StatsServer::new(stats::server_data())
420 .launch()
421 .context("launching stats server")?;
422
423 let parent_cgroup = Self::managed_cell_parent(opts)?;
424 let exclude: HashSet<String> = opts.cell_exclude.iter().cloned().collect();
425 let cell_manager = CellManager::new(
426 parent_cgroup,
427 MAX_CELLS as u32,
428 topology.span.clone(),
429 exclude,
430 opts.cell0_min_cpus,
431 mitosis_topology.cpu_to_llc.into_iter().collect(),
432 )
433 .with_context(|| format!("initializing cell manager for cgroup {}", parent_cgroup))?;
434
435 let epoll = Epoll::new(EpollCreateFlags::empty()).context("creating epoll instance")?;
437
438 let stats_waker = EventFd::from_value_and_flags(
440 0,
441 nix::sys::eventfd::EfdFlags::EFD_NONBLOCK | nix::sys::eventfd::EfdFlags::EFD_SEMAPHORE,
442 )
443 .context("creating stats-waker eventfd")?;
444
445 epoll
447 .add(
448 &stats_waker,
449 EpollEvent::new(EpollFlags::EPOLLIN, STATS_TOKEN),
450 )
451 .context("registering stats-waker with epoll")?;
452
453 epoll
454 .add(
455 &cell_manager,
456 EpollEvent::new(EpollFlags::EPOLLIN, INOTIFY_TOKEN),
457 )
458 .context("registering cell manager inotify with epoll")?;
459
460 Ok(Self {
461 _arenalib: arenalib,
462 skel,
463 monitor_interval: Duration::from_secs(opts.monitor_interval_s),
464 cells: HashMap::new(),
465 prev_cell_stats: [[0; NR_CSTATS]; MAX_CELLS],
466 prev_cell_running_ns: [0; MAX_CELLS],
467 prev_cell_own_ns: [0; MAX_CELLS],
468 prev_cell_lent_ns: [0; MAX_CELLS],
469 demand_sampled_at: None,
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_nitosis --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.release_detached_cells()
554 .context("releasing detached cells")?;
555 self.refresh_bpf_cells()
556 .context("refreshing BPF cell state")?;
557 self.check_cpuset_changes()
558 .context("checking cpuset changes")?;
559 self.collect_metrics().context("collecting metrics")?;
560
561 if self.enable_rebalancing {
562 self.maybe_rebalance().context("running rebalance check")?;
563 }
564 }
565
566 drop(struct_ops);
567 drop(self.stats_server.take());
569 let _ = stats_bridge.join();
570 info!("Unregister {SCHEDULER_NAME} scheduler");
571 uei_report!(&self.skel, uei)
572 }
573
574 fn apply_initial_cells(&mut self) -> Result<()> {
576 let cpu_assignments = self
577 .compute_and_apply_cell_config(&[])
578 .context("computing initial cell configuration")?;
579
580 info!(
581 "Applied initial cell configuration: {}",
582 self.cell_manager.format_cell_config(&cpu_assignments)
583 );
584
585 Ok(())
586 }
587
588 fn process_cell_events(&mut self) -> Result<()> {
590 let (num_new, num_destroyed, new_cell_ids, destroyed_cell_ids) = {
591 let (new_cells, destroyed_cells) = self
594 .cell_manager
595 .process_events(|| read_cell_sub_cgids(&self.skel).context("reading cell records"))
596 .context("processing inotify events")?;
597
598 if new_cells.is_empty() && destroyed_cells.is_empty() {
599 return Ok(());
600 }
601
602 let new_ids: Vec<u32> = new_cells.iter().map(|(_, cell_id)| *cell_id).collect();
603 (
604 new_cells.len(),
605 destroyed_cells.len(),
606 new_ids,
607 destroyed_cells,
608 )
609 };
610
611 for &cell_id in &destroyed_cell_ids {
614 self.smoothed_util[cell_id as usize] = 0.0;
615 }
616
617 let cpu_assignments = self
618 .compute_and_apply_cell_config(&new_cell_ids)
619 .context("recomputing cell configuration for new cgroups")?;
620
621 info!(
622 "Cell config updated ({} new, {} destroyed): {}",
623 num_new,
624 num_destroyed,
625 self.cell_manager.format_cell_config(&cpu_assignments)
626 );
627
628 Ok(())
629 }
630
631 fn release_detached_cells(&mut self) -> Result<()> {
635 let sub_cgids = read_cell_sub_cgids(&self.skel).context("reading cell records")?;
636 let released = self.cell_manager.release_detached_cells(&sub_cgids);
637 if released == 0 {
638 return Ok(());
639 }
640
641 let cpu_assignments = self
642 .compute_and_apply_cell_config(&[])
643 .context("recomputing cell configuration after a detach")?;
644
645 info!(
646 "Released {} detached cell(s): {}",
647 released,
648 self.cell_manager.format_cell_config(&cpu_assignments)
649 );
650
651 Ok(())
652 }
653
654 fn compute_and_apply_cell_config(
663 &mut self,
664 new_cell_ids: &[u32],
665 ) -> Result<Vec<CpuAssignment>> {
666 let (cell_assignments, cpu_assignments) = {
667 let active_cell_ids: Vec<u32> = self
668 .cell_manager
669 .get_cell_assignments()
670 .iter()
671 .map(|(_, cell_id)| *cell_id)
672 .collect();
673 let all_cell_ids: Vec<u32> = std::iter::once(0)
675 .chain(active_cell_ids.iter().copied())
676 .collect();
677
678 let cpu_assignments = if self.enable_rebalancing {
679 let new_set: HashSet<u32> = new_cell_ids.iter().copied().collect();
681 let existing_utils: Vec<f64> = all_cell_ids
682 .iter()
683 .filter(|id| !new_set.contains(id))
684 .map(|&id| self.smoothed_util[id as usize])
685 .collect();
686
687 let has_data = existing_utils.iter().any(|&u| u > 0.0);
688
689 if has_data {
690 let avg_util: f64 =
692 existing_utils.iter().sum::<f64>() / existing_utils.len().max(1) as f64;
693 for &id in new_cell_ids {
694 self.smoothed_util[id as usize] = avg_util;
695 info!(
696 "Seeded new cell {} smoothed_util to average {:.1}%",
697 id, avg_util
698 );
699 }
700
701 let cell_demands: HashMap<u32, f64> = all_cell_ids
703 .iter()
704 .map(|&id| (id, self.smoothed_util[id as usize]))
705 .collect();
706
707 self.cell_manager
708 .compute_demand_cpu_assignments(&cell_demands, self.enable_borrowing)
709 .context("computing demand-weighted CPU assignments")?
710 } else {
711 self.cell_manager
713 .compute_cpu_assignments(self.enable_borrowing)
714 .context("computing equal-weight CPU assignments (no utilization data)")?
715 }
716 } else {
717 self.cell_manager
718 .compute_cpu_assignments(self.enable_borrowing)
719 .context("computing equal-weight CPU assignments (rebalancing disabled)")?
720 };
721
722 (self.cell_manager.get_cell_assignments(), cpu_assignments)
723 };
724
725 self.apply_cell_config(&cell_assignments, &cpu_assignments)
726 .context("applying cell configuration to BPF")?;
727
728 Ok(cpu_assignments)
729 }
730
731 fn maybe_rebalance(&mut self) -> Result<()> {
733 if self.last_rebalance.elapsed() < self.rebalance_cooldown {
735 return Ok(());
736 }
737
738 let active_cells: Vec<u32> = self.cells.keys().copied().collect();
740 if active_cells.len() < 2 {
741 return Ok(());
742 }
743
744 let mut min_util = f64::MAX;
745 let mut max_util = f64::MIN;
746 for &cell_id in &active_cells {
747 let util = self.smoothed_util[cell_id as usize];
748 if util < min_util {
749 min_util = util;
750 }
751 if util > max_util {
752 max_util = util;
753 }
754 }
755
756 let spread = max_util - min_util;
757 if spread < self.rebalance_threshold {
758 return Ok(());
759 }
760
761 let cell_demands: HashMap<u32, f64> = active_cells
763 .iter()
764 .map(|&cell_id| (cell_id, self.smoothed_util[cell_id as usize]))
765 .collect();
766
767 let (cell_assignments, cpu_assignments) = {
769 let cpu_assignments = self
770 .cell_manager
771 .compute_demand_cpu_assignments(&cell_demands, self.enable_borrowing)
772 .context("computing demand-weighted CPU assignments for rebalance")?;
773
774 let changed = cpu_assignments.iter().any(|a| {
775 self.cells
776 .get(&a.id)
777 .is_none_or(|cell| cell.cpus != a.primary)
778 });
779
780 if !changed {
781 return Ok(());
782 }
783
784 (self.cell_manager.get_cell_assignments(), cpu_assignments)
785 };
786
787 self.apply_cell_config(&cell_assignments, &cpu_assignments)
788 .context("applying rebalanced cell configuration to BPF")?;
789
790 self.last_rebalance = Instant::now();
791 self.rebalance_count += 1;
792 self.metrics.rebalance_count = self.rebalance_count;
793
794 info!(
795 "Rebalanced CPUs (spread={:.1}%, count={}): {}",
796 spread,
797 self.rebalance_count,
798 self.cell_manager.format_cell_config(&cpu_assignments)
799 );
800
801 Ok(())
802 }
803
804 fn apply_cell_config(
809 &mut self,
810 cell_assignments: &[(u64, u32)],
811 cpu_assignments: &[CpuAssignment],
812 ) -> Result<()> {
813 let bss_data = self
814 .skel
815 .maps
816 .bss_data
817 .as_mut()
818 .expect("bss_data must be available after scheduler load");
819
820 let config = &mut bss_data.cell_config;
821
822 unsafe {
830 std::ptr::write_bytes(
831 config as *mut _ as *mut u8,
832 0,
833 std::mem::size_of_val(config),
834 );
835 }
836
837 if cell_assignments.len() > bpf_intf::consts_MAX_CELLS as usize {
838 bail!(
839 "Too many cell assignments: {} > MAX_CELLS ({})",
840 cell_assignments.len(),
841 bpf_intf::consts_MAX_CELLS
842 );
843 }
844 config.num_cell_assignments = cell_assignments.len() as u32;
845
846 for (i, (cgid, cell_id)) in cell_assignments.iter().enumerate() {
847 config.assignments[i].cgid = *cgid;
848 config.assignments[i].cell_id = *cell_id;
849 }
850
851 let mut max_cell_id: u32 = 0;
853 for a in cpu_assignments {
854 if a.id >= bpf_intf::consts_MAX_CELLS {
855 bail!(
856 "Cell ID {} exceeds MAX_CELLS ({})",
857 a.id,
858 bpf_intf::consts_MAX_CELLS
859 );
860 }
861 max_cell_id = max_cell_id.max(a.id + 1);
862
863 write_cpumask_to_config(&a.primary, &mut config.cpumasks[a.id as usize].mask);
864
865 if let Some(ref borrowable) = a.borrowable {
866 write_cpumask_to_config(
867 borrowable,
868 &mut config.borrowable_cpumasks[a.id as usize].mask,
869 );
870 }
871 }
872 config.num_cells = max_cell_id;
873
874 let prog = &mut self.skel.progs.apply_cell_config;
876 let out = prog
877 .test_run(ProgramInput::default())
878 .context("Failed to run apply_cell_config BPF program")?;
879 if out.return_value != 0 {
880 bail!(
881 "apply_cell_config BPF program returned error {} (num_assignments={}, num_cells={})",
882 out.return_value as i32,
883 cell_assignments.len(),
884 cpu_assignments.len()
885 );
886 }
887
888 Ok(())
889 }
890
891 fn get_metrics(&self) -> Metrics {
892 self.metrics.clone()
893 }
894
895 fn calculate_distribution_stats(
896 &self,
897 queue_counts: &[u64; QUEUE_STATS_IDX.len()],
898 global_queue_decisions: u64,
899 scope_queue_decisions: u64,
900 scope_affn_viols: u64,
901 scope_steals: u64,
902 scope_pin_skips: u64,
903 scope_borrow_bounces: u64,
904 ) -> Result<DistributionStats> {
905 let share_of_global =
908 100.0 * (scope_queue_decisions as f64) / (global_queue_decisions as f64);
909
910 let queue_pct = if scope_queue_decisions == 0 {
912 debug!("No queue decisions in scope, zeroing out queue distribution");
913 [0.0; QUEUE_STATS_IDX.len()]
914 } else {
915 core::array::from_fn(|i| {
916 100.0 * (queue_counts[i] as f64) / (scope_queue_decisions as f64)
917 })
918 };
919
920 let affinity_violations_percent = if scope_queue_decisions == 0 {
922 debug!("No queue decisions in scope, zeroing out affinity violations");
923 0.0
924 } else {
925 100.0 * (scope_affn_viols as f64) / (scope_queue_decisions as f64)
926 };
927
928 let steal_pct = if scope_queue_decisions == 0 {
929 0.0
930 } else {
931 100.0 * (scope_steals as f64) / (scope_queue_decisions as f64)
932 };
933
934 let pin_skip_pct = if scope_queue_decisions == 0 {
935 0.0
936 } else {
937 100.0 * (scope_pin_skips as f64) / (scope_queue_decisions as f64)
938 };
939
940 let borrow_bounce_pct = if scope_queue_decisions == 0 {
941 0.0
942 } else {
943 100.0 * (scope_borrow_bounces as f64) / (scope_queue_decisions as f64)
944 };
945
946 const EXPECTED_QUEUES: usize = 5;
947 if queue_pct.len() != EXPECTED_QUEUES {
948 bail!(
949 "Expected {} queues, got {}",
950 EXPECTED_QUEUES,
951 queue_pct.len()
952 );
953 }
954
955 Ok(DistributionStats {
956 total_decisions: scope_queue_decisions,
957 share_of_decisions_pct: share_of_global,
958 local_q_pct: queue_pct[0],
959 cpu_q_pct: queue_pct[1],
960 cell_q_pct: queue_pct[2],
961 borrowed_pct: queue_pct[3],
962 sub_q_pct: queue_pct[4],
963 affn_viol_pct: affinity_violations_percent,
964 steal_pct,
965 pin_skip_pct,
966 borrow_bounce_pct,
967 global_queue_decisions,
968 })
969 }
970
971 fn update_and_log_global_queue_stats(
973 &mut self,
974 global_queue_decisions: u64,
975 cell_stats_delta: &[[u64; NR_CSTATS]; MAX_CELLS],
976 ) -> Result<()> {
977 let mut queue_counts = [0; QUEUE_STATS_IDX.len()];
979 for cells in 0..MAX_CELLS {
980 for (i, stat) in QUEUE_STATS_IDX.iter().enumerate() {
981 queue_counts[i] += cell_stats_delta[cells][*stat as usize];
982 }
983 }
984
985 let prefix = "Total Decisions:";
986
987 let scope_affn_viols: u64 = cell_stats_delta
989 .iter()
990 .map(|&cell| cell[bpf_intf::cell_stat_idx_CSTAT_AFFN_VIOL as usize])
991 .sum::<u64>();
992
993 let scope_steals: u64 = cell_stats_delta
995 .iter()
996 .map(|&cell| cell[bpf_intf::cell_stat_idx_CSTAT_STEAL as usize])
997 .sum::<u64>();
998
999 let scope_pin_skips: u64 = cell_stats_delta
1001 .iter()
1002 .map(|&cell| cell[bpf_intf::cell_stat_idx_CSTAT_PIN_SKIP as usize])
1003 .sum::<u64>();
1004
1005 let scope_borrow_bounces: u64 = cell_stats_delta
1007 .iter()
1008 .map(|&cell| cell[bpf_intf::cell_stat_idx_CSTAT_BORROW_BOUNCE as usize])
1009 .sum::<u64>();
1010
1011 let stats = self
1013 .calculate_distribution_stats(
1014 &queue_counts,
1015 global_queue_decisions,
1016 global_queue_decisions,
1017 scope_affn_viols,
1018 scope_steals,
1019 scope_pin_skips,
1020 scope_borrow_bounces,
1021 )
1022 .context("calculating global queue distribution stats")?;
1023
1024 self.metrics.update(&stats);
1025
1026 let sum = |idx: usize| -> u64 { cell_stats_delta.iter().map(|c| c[idx]).sum() };
1028 self.metrics.drain_cnt = sum(bpf_intf::cell_stat_idx_CSTAT_DRAIN_CNT as usize);
1029 self.metrics.drain_affn_cnt = sum(bpf_intf::cell_stat_idx_CSTAT_DRAIN_AFFN_CNT as usize);
1030 self.metrics.slice_shrink_max =
1031 sum(bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_MAX as usize);
1032 self.metrics.slice_shrink_proportional =
1033 sum(bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_PROPORTIONAL as usize);
1034 self.metrics.slice_shrink_min =
1035 sum(bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_MIN as usize);
1036 self.metrics.slice_shrink = self.metrics.slice_shrink_max
1037 + self.metrics.slice_shrink_proportional
1038 + self.metrics.slice_shrink_min;
1039
1040 trace!("{} {}", prefix, stats);
1041
1042 Ok(())
1043 }
1044
1045 fn update_and_log_cell_queue_stats(
1047 &mut self,
1048 global_queue_decisions: u64,
1049 cell_stats_delta: &[[u64; NR_CSTATS]; MAX_CELLS],
1050 ) -> Result<()> {
1051 for cell in 0..MAX_CELLS {
1052 let cell_queue_decisions = QUEUE_STATS_IDX
1053 .iter()
1054 .map(|&stat| cell_stats_delta[cell][stat as usize])
1055 .sum::<u64>();
1056
1057 if cell_queue_decisions == 0 {
1059 continue;
1060 }
1061
1062 let mut queue_counts = [0; QUEUE_STATS_IDX.len()];
1063 for (i, &stat) in QUEUE_STATS_IDX.iter().enumerate() {
1064 queue_counts[i] = cell_stats_delta[cell][stat as usize];
1065 }
1066
1067 const MIN_CELL_WIDTH: usize = 2;
1068 let cell_width: usize = max(MIN_CELL_WIDTH, (MAX_CELLS as f64).log10().ceil() as usize);
1069
1070 let prefix = format!(" Cell {:width$}:", cell, width = cell_width);
1071
1072 let scope_affn_viols: u64 =
1074 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_AFFN_VIOL as usize];
1075
1076 let scope_steals: u64 =
1078 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_STEAL as usize];
1079
1080 let scope_pin_skips: u64 =
1082 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_PIN_SKIP as usize];
1083
1084 let scope_borrow_bounces: u64 =
1086 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_BORROW_BOUNCE as usize];
1087
1088 let stats = self
1089 .calculate_distribution_stats(
1090 &queue_counts,
1091 global_queue_decisions,
1092 cell_queue_decisions,
1093 scope_affn_viols,
1094 scope_steals,
1095 scope_pin_skips,
1096 scope_borrow_bounces,
1097 )
1098 .with_context(|| {
1099 format!("calculating queue distribution stats for cell {}", cell)
1100 })?;
1101
1102 let cell_metrics = self.metrics.cells.entry(cell as u32).or_default();
1103 cell_metrics.update(&stats);
1104
1105 cell_metrics.drain_cnt =
1107 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_DRAIN_CNT as usize];
1108 cell_metrics.drain_affn_cnt =
1109 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_DRAIN_AFFN_CNT as usize];
1110 cell_metrics.slice_shrink_max =
1111 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_MAX as usize];
1112 cell_metrics.slice_shrink_proportional = cell_stats_delta[cell]
1113 [bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_PROPORTIONAL as usize];
1114 cell_metrics.slice_shrink_min =
1115 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_SLICE_SHRINK_MIN as usize];
1116 cell_metrics.slice_shrink = cell_metrics.slice_shrink_max
1117 + cell_metrics.slice_shrink_proportional
1118 + cell_metrics.slice_shrink_min;
1119
1120 trace!("{} {}", prefix, stats);
1121 }
1122 Ok(())
1123 }
1124
1125 fn update_drain_metrics(&mut self, cell_stats_delta: &[[u64; NR_CSTATS]; MAX_CELLS]) {
1126 let mut total = 0;
1127 let mut affn_total = 0;
1128
1129 for cell in 0..MAX_CELLS {
1130 let drain_cnt =
1131 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_DRAIN_CNT as usize];
1132 let drain_affn_cnt =
1133 cell_stats_delta[cell][bpf_intf::cell_stat_idx_CSTAT_DRAIN_AFFN_CNT as usize];
1134 total += drain_cnt;
1135 affn_total += drain_affn_cnt;
1136
1137 if let Some(cell_metrics) = self.metrics.cells.get_mut(&(cell as u32)) {
1138 cell_metrics.drain_cnt = drain_cnt;
1139 cell_metrics.drain_affn_cnt = drain_affn_cnt;
1140 } else if drain_cnt > 0 || drain_affn_cnt > 0 {
1141 let cell_metrics = self.metrics.cells.entry(cell as u32).or_default();
1142 cell_metrics.drain_cnt = drain_cnt;
1143 cell_metrics.drain_affn_cnt = drain_affn_cnt;
1144 }
1145 }
1146
1147 self.metrics.drain_cnt = total;
1148 self.metrics.drain_affn_cnt = affn_total;
1149 }
1150
1151 fn log_all_queue_stats(
1152 &mut self,
1153 cell_stats_delta: &[[u64; NR_CSTATS]; MAX_CELLS],
1154 ) -> Result<()> {
1155 let global_queue_decisions: u64 = cell_stats_delta
1157 .iter()
1158 .flat_map(|cell| QUEUE_STATS_IDX.iter().map(|&idx| cell[idx as usize]))
1159 .sum();
1160
1161 self.update_drain_metrics(cell_stats_delta);
1162
1163 let sub_cgids = read_cell_sub_cgids(&self.skel).context("reading cell records")?;
1165 for (cell, cell_metrics) in self.metrics.cells.iter_mut() {
1166 cell_metrics.sub_cgid = sub_cgids[*cell as usize];
1167 }
1168
1169 if global_queue_decisions == 0 {
1170 if self.metrics.drain_cnt == 0 {
1171 warn!("No queueing decisions made globally");
1172 }
1173 return Ok(());
1174 }
1175
1176 self.update_and_log_global_queue_stats(global_queue_decisions, cell_stats_delta)
1177 .context("updating global queue stats")?;
1178
1179 self.update_and_log_cell_queue_stats(global_queue_decisions, cell_stats_delta)
1180 .context("updating per-cell queue stats")?;
1181
1182 Ok(())
1183 }
1184
1185 fn calculate_cell_stat_delta(
1186 &mut self,
1187 cpu_ctxs: &[bpf_intf::cpu_ctx],
1188 ) -> Result<[[u64; NR_CSTATS]; MAX_CELLS]> {
1189 let mut cell_stats_delta = [[0_u64; NR_CSTATS]; MAX_CELLS];
1190
1191 for cell in 0..MAX_CELLS {
1194 for stat in 0..NR_CSTATS {
1195 let mut cur_cell_stat = 0;
1196
1197 for cpu_ctx in cpu_ctxs.iter() {
1199 cur_cell_stat += cpu_ctx.cstats[cell][stat];
1200 }
1201
1202 cell_stats_delta[cell][stat] = cur_cell_stat - self.prev_cell_stats[cell][stat];
1204 self.prev_cell_stats[cell][stat] = cur_cell_stat;
1205 }
1206 }
1207 Ok(cell_stats_delta)
1208 }
1209
1210 fn collect_metrics(&mut self) -> Result<()> {
1212 self.skel
1215 .progs
1216 .sub_charge_cids
1217 .test_run(ProgramInput::default())
1218 .context("running sub_charge_cids")?;
1219 let sampled_at = Instant::now();
1221 let cpu_ctxs = read_cpu_ctxs(&self.skel).context("reading per-CPU contexts for metrics")?;
1222
1223 let cell_stats_delta = self
1224 .calculate_cell_stat_delta(&cpu_ctxs)
1225 .context("calculating cell stat deltas")?;
1226
1227 self.log_all_queue_stats(&cell_stats_delta)
1228 .context("logging queue stats")?;
1229
1230 self.metrics.enforced_holdout = self.cell_manager.enforced_holdout() as u64;
1233
1234 self.collect_demand_metrics(&cpu_ctxs, sampled_at)
1235 .context("collecting demand metrics")?;
1236
1237 for (cell_id, cell) in &self.cells {
1238 trace!("CELL[{}]: {}", cell_id, cell.cpus);
1239 }
1240
1241 for (cell_id, cell) in self.cells.iter() {
1242 self.metrics
1244 .cells
1245 .entry(*cell_id)
1246 .and_modify(|cell_metrics| {
1247 cell_metrics.num_cpus = cell.cpus.weight() as u32;
1248 cell_metrics.cgroup_path = self.cell_manager.cgroup_path_for_cell(*cell_id);
1249 });
1250 }
1251 self.metrics.num_cells = self.cells.len() as u32;
1252
1253 Ok(())
1254 }
1255
1256 fn collect_demand_metrics(
1258 &mut self,
1259 cpu_ctxs: &[bpf_intf::cpu_ctx],
1260 sampled_at: Instant,
1261 ) -> Result<()> {
1262 let half_interval = self.monitor_interval / 2;
1267 let interval_ns = match self.demand_sampled_at {
1268 Some(prev) if sampled_at.duration_since(prev) < half_interval => return Ok(()),
1269 Some(prev) => Some(sampled_at.duration_since(prev).as_nanos() as u64),
1270 None => None,
1271 };
1272 self.demand_sampled_at = Some(sampled_at);
1273
1274 let mut total_running_ns = [0u64; MAX_CELLS];
1279 let mut on_own_ns = [0u64; MAX_CELLS];
1280 let mut lent_ns = [0u64; MAX_CELLS];
1281
1282 for cpu_ctx in cpu_ctxs.iter() {
1285 let owner = cpu_ctx.cell as usize;
1286 let running_ns: [u64; MAX_CELLS] =
1287 std::array::from_fn(|cell| cpu_ctx.running_ns[cell] + cpu_ctx.sub_ns[cell]);
1288 for cell in 0..MAX_CELLS {
1289 let ns = running_ns[cell];
1290 total_running_ns[cell] += ns;
1291 if owner == cell {
1292 on_own_ns[cell] += ns;
1293 }
1294 }
1295 if owner >= MAX_CELLS {
1296 bail!(
1297 "CPU has invalid cell assignment {} (MAX_CELLS={})",
1298 owner,
1299 MAX_CELLS
1300 );
1301 }
1302 let total_on_cpu: u64 = running_ns.iter().sum();
1304 let owner_on_cpu = running_ns[owner];
1305 lent_ns[owner] += total_on_cpu.saturating_sub(owner_on_cpu);
1306 }
1307
1308 let Some(interval_ns) = interval_ns else {
1310 self.prev_cell_running_ns = total_running_ns;
1311 self.prev_cell_own_ns = on_own_ns;
1312 self.prev_cell_lent_ns = lent_ns;
1313 return Ok(());
1314 };
1315
1316 let mut global_running_delta = 0u64;
1317 let mut global_borrowed_delta = 0u64;
1318 let mut global_lent_delta = 0u64;
1319 let mut global_capacity = 0u64;
1320
1321 for cell in 0..MAX_CELLS {
1322 let delta_running =
1323 total_running_ns[cell].saturating_sub(self.prev_cell_running_ns[cell]);
1324 let delta_on_own = on_own_ns[cell].saturating_sub(self.prev_cell_own_ns[cell]);
1325 let delta_lent = lent_ns[cell].saturating_sub(self.prev_cell_lent_ns[cell]);
1326
1327 self.prev_cell_running_ns[cell] = total_running_ns[cell];
1328 self.prev_cell_own_ns[cell] = on_own_ns[cell];
1329 self.prev_cell_lent_ns[cell] = lent_ns[cell];
1330
1331 let delta_borrowed = delta_running.saturating_sub(delta_on_own);
1333
1334 let Some(cell_info) = self.cells.get(&(cell as u32)) else {
1337 continue;
1338 };
1339
1340 let nr_cpus = cell_info.cpus.weight() as u64;
1341 if nr_cpus == 0 {
1342 bail!("Cell {} has 0 CPUs assigned", cell);
1343 }
1344
1345 let capacity = nr_cpus * interval_ns;
1347 let util_pct = 100.0 * (delta_running as f64) / (capacity as f64);
1349 let demand_borrow_pct = if delta_running > 0 {
1351 100.0 * (delta_borrowed as f64) / (delta_running as f64)
1352 } else {
1353 0.0
1354 };
1355 let lent_pct = 100.0 * (delta_lent as f64) / (capacity as f64);
1357
1358 if self.enable_rebalancing {
1360 self.smoothed_util[cell] = self.demand_smoothing * util_pct
1361 + (1.0 - self.demand_smoothing) * self.smoothed_util[cell];
1362 }
1363
1364 self.metrics
1365 .cells
1366 .entry(cell as u32)
1367 .or_default()
1368 .update_demand(util_pct, demand_borrow_pct, lent_pct);
1369
1370 if self.enable_rebalancing {
1372 self.metrics
1373 .cells
1374 .entry(cell as u32)
1375 .or_default()
1376 .smoothed_util_pct = self.smoothed_util[cell];
1377 }
1378
1379 global_running_delta = global_running_delta.saturating_add(delta_running);
1380 global_borrowed_delta = global_borrowed_delta.saturating_add(delta_borrowed);
1381 global_lent_delta = global_lent_delta.saturating_add(delta_lent);
1382 global_capacity = global_capacity.saturating_add(capacity);
1383 }
1384
1385 let global_util_pct = if global_capacity > 0 {
1386 100.0 * (global_running_delta as f64) / (global_capacity as f64)
1387 } else {
1388 0.0
1389 };
1390 let global_borrow_pct = if global_running_delta > 0 {
1391 100.0 * (global_borrowed_delta as f64) / (global_running_delta as f64)
1392 } else {
1393 0.0
1394 };
1395 let global_lent_pct = if global_capacity > 0 {
1396 100.0 * (global_lent_delta as f64) / (global_capacity as f64)
1397 } else {
1398 0.0
1399 };
1400
1401 self.metrics
1402 .update_demand(global_util_pct, global_borrow_pct, global_lent_pct);
1403
1404 Ok(())
1405 }
1406
1407 fn update_applied_cpuset_seq(&mut self) {
1409 unsafe {
1410 let ptr = &mut self
1411 .skel
1412 .maps
1413 .bss_data
1414 .as_mut()
1415 .expect("BUG: bss_data missing after scheduler load")
1416 .applied_cpuset_seq as *mut u32;
1417 std::ptr::write_volatile(ptr, self.last_cpuset_seq);
1418 }
1419 }
1420
1421 fn check_cpuset_changes(&mut self) -> Result<()> {
1423 let current_seq = unsafe {
1424 let ptr = &self
1425 .skel
1426 .maps
1427 .bss_data
1428 .as_ref()
1429 .expect("BUG: bss_data missing after scheduler load")
1430 .cpuset_seq as *const u32;
1431 (ptr as *const AtomicU32)
1432 .as_ref()
1433 .expect("BUG: cpuset_seq pointer cast yielded null")
1434 .load(Ordering::Acquire)
1435 };
1436
1437 if current_seq == self.last_cpuset_seq {
1438 return Ok(());
1439 }
1440 self.last_cpuset_seq = current_seq;
1441
1442 if !self
1443 .cell_manager
1444 .refresh_cpusets()
1445 .context("refreshing cell cpusets")?
1446 {
1447 self.update_applied_cpuset_seq();
1449 return Ok(());
1450 }
1451
1452 let cpu_assignments = self
1453 .compute_and_apply_cell_config(&[])
1454 .context("recomputing cell configuration after cpuset change")?;
1455 self.update_applied_cpuset_seq();
1456 info!(
1457 "Cpuset change detected, recomputed config: {}",
1458 self.cell_manager.format_cell_config(&cpu_assignments)
1459 );
1460 Ok(())
1461 }
1462
1463 fn refresh_bpf_cells(&mut self) -> Result<()> {
1464 let applied_configuration = unsafe {
1465 let ptr = &self
1466 .skel
1467 .maps
1468 .bss_data
1469 .as_ref()
1470 .expect("BUG: bss_data missing after scheduler load")
1471 .applied_configuration_seq as *const u32;
1472 (ptr as *const std::sync::atomic::AtomicU32)
1473 .as_ref()
1474 .expect("BUG: applied_configuration_seq pointer cast yielded null")
1475 .load(std::sync::atomic::Ordering::Acquire)
1476 };
1477 if self
1478 .last_configuration_seq
1479 .is_some_and(|seq| applied_configuration == seq)
1480 {
1481 return Ok(());
1482 }
1483 let mut cell_to_cpus: HashMap<u32, Cpumask> = HashMap::new();
1485 let cpu_ctxs =
1486 read_cpu_ctxs(&self.skel).context("reading per-CPU contexts for BPF cell refresh")?;
1487 for cpu_ctx in cpu_ctxs.iter() {
1488 cell_to_cpus
1490 .entry(cpu_ctx.cell)
1491 .or_insert_with(Cpumask::new)
1492 .set_cpu(cpu_ctx.cpu as usize)
1493 .expect("set cpu in existing mask");
1494 }
1495
1496 let cells_with_cpus: HashSet<u32> = cell_to_cpus.keys().copied().collect();
1507 let mut active_cells = cells_with_cpus.clone();
1508 active_cells.insert(0);
1509
1510 for cell_idx in &active_cells {
1511 let cpus = cell_to_cpus
1512 .get(cell_idx)
1513 .cloned()
1514 .unwrap_or_else(Cpumask::new);
1515 self.cells
1516 .entry(*cell_idx)
1517 .or_insert_with(|| Cell {
1518 cpus: Cpumask::new(),
1519 })
1520 .cpus = cpus;
1521 self.metrics.cells.insert(*cell_idx, CellMetrics::default());
1522 }
1523
1524 self.cells.retain(|&k, _| active_cells.contains(&k));
1526 self.metrics.cells.retain(|&k, _| active_cells.contains(&k));
1527
1528 self.last_configuration_seq = Some(applied_configuration);
1529
1530 Ok(())
1531 }
1532}
1533
1534fn write_cpumask_to_config(cpumask: &Cpumask, dest: &mut [u8]) {
1535 let raw_slice = cpumask.as_raw_slice();
1536 for (word_idx, word) in raw_slice.iter().enumerate() {
1537 let byte_start = word_idx * 8;
1538 let bytes = word.to_le_bytes();
1539 for (j, byte) in bytes.iter().enumerate() {
1540 let idx = byte_start + j;
1541 if idx < dest.len() {
1542 dest[idx] = *byte;
1543 }
1544 }
1545 }
1546}
1547
1548fn read_cell_sub_cgids(skel: &BpfSkel) -> Result<Vec<u64>> {
1550 let bss = skel
1551 .maps
1552 .bss_data
1553 .as_ref()
1554 .context("bss_data not available")?;
1555 let ptr = bss.cells as *const bpf_intf::cell;
1556 if ptr.is_null() {
1557 bail!("cells arena array not initialized");
1558 }
1559 let mut cgids = Vec::with_capacity(MAX_CELLS);
1560 for cell in 0..MAX_CELLS {
1561 cgids.push(unsafe { std::ptr::addr_of!((*ptr.add(cell)).sub_cgid).read_volatile() });
1562 }
1563 Ok(cgids)
1564}
1565
1566fn read_cpu_ctxs(skel: &BpfSkel) -> Result<Vec<bpf_intf::cpu_ctx>> {
1567 let bss = skel
1568 .maps
1569 .bss_data
1570 .as_ref()
1571 .context("bss_data not available")?;
1572 let nr = bss.nr_cid_ctxs as usize;
1573 let ptr = bss.cpu_ctxs as *const bpf_intf::cpu_ctx;
1574 if ptr.is_null() || nr == 0 {
1575 bail!("cpu_ctxs arena array not initialized");
1576 }
1577 let mut cpu_ctxs = Vec::with_capacity(nr);
1583 for cid in 0..nr {
1584 cpu_ctxs.push(unsafe { std::ptr::read_volatile(ptr.add(cid)) });
1585 }
1586 Ok(cpu_ctxs)
1587}
1588
1589fn run(opts: Opts) -> Result<()> {
1590 if opts.version {
1591 println!(
1592 "scx_nitosis {}",
1593 build_id::full_version(env!("CARGO_PKG_VERSION"))
1594 );
1595 return Ok(());
1596 }
1597
1598 let env_filter = EnvFilter::try_from_default_env()
1599 .or_else(|_| match EnvFilter::try_new(&opts.log_level) {
1600 Ok(filter) => Ok(filter),
1601 Err(e) => {
1602 eprintln!(
1603 "invalid log envvar: {}, using info, err is: {}",
1604 opts.log_level, e
1605 );
1606 EnvFilter::try_new("info")
1607 }
1608 })
1609 .unwrap_or_else(|_| EnvFilter::new("info"));
1610
1611 match tracing_subscriber::fmt()
1612 .with_env_filter(env_filter)
1613 .with_target(true)
1614 .with_thread_ids(true)
1615 .with_file(true)
1616 .with_line_number(true)
1617 .try_init()
1618 {
1619 Ok(()) => {}
1620 Err(e) => eprintln!("failed to init logger: {}", e),
1621 }
1622
1623 if opts.verbose > 0 {
1624 warn!("Setting verbose via -v is deprecated and will be an error in future releases.");
1625 }
1626
1627 debug!("opts={:?}", &opts);
1628
1629 if let Some(run_id) = opts.run_id {
1630 info!("scx_nitosis run_id: {}", run_id);
1631 }
1632
1633 let shutdown = Arc::new(AtomicBool::new(false));
1634 let shutdown_clone = shutdown.clone();
1635 ctrlc::set_handler(move || {
1636 shutdown_clone.store(true, Ordering::Relaxed);
1637 })
1638 .context("Error setting Ctrl-C handler")?;
1639
1640 if let Some(intv) = opts.monitor {
1641 let shutdown_clone = shutdown.clone();
1642 let jh = std::thread::spawn(move || {
1643 match stats::monitor(Duration::from_secs_f64(intv), shutdown_clone) {
1644 Ok(_) => {
1645 debug!("stats monitor thread finished successfully")
1646 }
1647 Err(error_object) => {
1648 warn!(
1649 "stats monitor thread finished because of an error {}",
1650 error_object
1651 )
1652 }
1653 }
1654 });
1655 if opts.monitor.is_some() {
1656 let _ = jh.join();
1657 return Ok(());
1658 }
1659 }
1660
1661 let mut open_object = MaybeUninit::uninit();
1662 loop {
1663 let mut sched =
1664 Scheduler::init(&opts, &mut open_object).context("initializing scheduler")?;
1665 if !sched
1666 .run(shutdown.clone())
1667 .context("running scheduler main loop")?
1668 .should_restart()
1669 {
1670 break;
1671 }
1672 }
1673
1674 Ok(())
1675}
1676
1677fn main() -> Result<()> {
1678 let parsed = undefok_flags::parse_args::<Opts>()?;
1679 for undefok in &parsed.ignored_undefok_flags {
1681 eprintln!("warning: ignoring undefok flag --{}", undefok.long);
1682 }
1683 run(parsed.opts)
1684}
1685
1686#[cfg(test)]
1687mod tests {
1688 use super::Opts;
1689 use clap::Parser;
1690
1691 #[test]
1692 fn requires_cell_parent_cgroup_for_scheduler_mode() {
1693 assert!(Opts::try_parse_from(["scx_nitosis"]).is_err());
1694 }
1695
1696 #[test]
1697 fn allows_monitor_without_cell_parent_cgroup() {
1698 assert!(Opts::try_parse_from(["scx_nitosis", "--monitor", "1"]).is_ok());
1699 }
1700
1701 #[test]
1702 fn allows_version_without_cell_parent_cgroup() {
1703 assert!(Opts::try_parse_from(["scx_nitosis", "--version"]).is_ok());
1704 }
1705}