Skip to main content

scx_mitosis/
main.rs

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