Skip to main content

scx_layered/
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.
5mod bpf_skel;
6mod stats;
7
8use std::collections::BTreeMap;
9use std::collections::BTreeSet;
10use std::collections::HashMap;
11use std::collections::HashSet;
12use std::ffi::CString;
13use std::fs;
14use std::io::Write;
15use std::mem::MaybeUninit;
16use std::ops::Sub;
17use std::path::Path;
18use std::path::PathBuf;
19use std::sync::Arc;
20use std::sync::atomic::AtomicBool;
21use std::sync::atomic::Ordering;
22use std::thread::ThreadId;
23use std::time::Duration;
24use std::time::Instant;
25
26use inotify::{Inotify, WatchMask};
27use std::os::unix::io::AsRawFd;
28
29use anyhow::Context;
30use anyhow::Result;
31use anyhow::anyhow;
32use anyhow::bail;
33pub use bpf_skel::*;
34use clap::Parser;
35use crossbeam::channel::Receiver;
36use crossbeam::select;
37use lazy_static::lazy_static;
38use libbpf_rs::AsRawLibbpf;
39use libbpf_rs::MapCore as _;
40use libbpf_rs::OpenObject;
41use libbpf_rs::ProgramInput;
42use libbpf_rs::libbpf_sys;
43use nix::sched::CpuSet;
44use nvml_wrapper::Nvml;
45use nvml_wrapper::error::NvmlError;
46use once_cell::sync::OnceCell;
47use regex::Regex;
48use scx_layered::alloc::{LayerAlloc, LayerDemand, unified_alloc};
49use scx_layered::*;
50use scx_raw_pmu::PMUManager;
51use scx_stats::prelude::*;
52use scx_utils::CoreType;
53use scx_utils::Cpumask;
54use scx_utils::Llc;
55use scx_utils::NR_CPU_IDS;
56use scx_utils::NR_CPUS_POSSIBLE;
57use scx_utils::NetDev;
58use scx_utils::Topology;
59use scx_utils::TopologyArgs;
60use scx_utils::UserExitInfo;
61use scx_utils::build_id;
62use scx_utils::compat;
63use scx_utils::init_libbpf_logging;
64use scx_utils::libbpf_clap_opts::LibbpfOpts;
65use scx_utils::perf;
66use scx_utils::pm::{cpu_idle_resume_latency_supported, update_cpu_idle_resume_latency};
67use scx_utils::read_netdevs;
68use scx_utils::scx_enums;
69use scx_utils::scx_ops_attach;
70use scx_utils::scx_ops_load;
71use scx_utils::scx_ops_open;
72use scx_utils::uei_exited;
73use scx_utils::uei_report;
74use stats::LayerStats;
75use stats::StatsReq;
76use stats::StatsRes;
77use stats::SysStats;
78use std::collections::VecDeque;
79use sysinfo::{Pid, ProcessRefreshKind, ProcessesToUpdate, System};
80use tracing::{debug, error, info, trace, warn};
81use tracing_subscriber::filter::EnvFilter;
82use walkdir::WalkDir;
83
84const SCHEDULER_NAME: &str = "scx_layered";
85const MAX_PATH: usize = bpf_intf::consts_MAX_PATH as usize;
86const MAX_COMM: usize = bpf_intf::consts_MAX_COMM as usize;
87const MAX_LAYER_WEIGHT: u32 = bpf_intf::consts_MAX_LAYER_WEIGHT;
88const MIN_LAYER_WEIGHT: u32 = bpf_intf::consts_MIN_LAYER_WEIGHT;
89const MAX_LAYER_MATCH_ORS: usize = bpf_intf::consts_MAX_LAYER_MATCH_ORS as usize;
90const MAX_LAYER_NAME: usize = bpf_intf::consts_MAX_LAYER_NAME as usize;
91const MAX_LAYERS: usize = bpf_intf::consts_MAX_LAYERS as usize;
92const DEFAULT_LAYER_WEIGHT: u32 = bpf_intf::consts_DEFAULT_LAYER_WEIGHT;
93const USAGE_HALF_LIFE: u32 = bpf_intf::consts_USAGE_HALF_LIFE;
94const USAGE_HALF_LIFE_F64: f64 = USAGE_HALF_LIFE as f64 / 1_000_000_000.0;
95
96const LAYER_USAGE_OWNED: usize = bpf_intf::layer_usage_LAYER_USAGE_OWNED as usize;
97const LAYER_USAGE_OPEN: usize = bpf_intf::layer_usage_LAYER_USAGE_OPEN as usize;
98const LAYER_USAGE_SUM_UPTO: usize = bpf_intf::layer_usage_LAYER_USAGE_SUM_UPTO as usize;
99const LAYER_USAGE_PROTECTED: usize = bpf_intf::layer_usage_LAYER_USAGE_PROTECTED as usize;
100const LAYER_USAGE_PROTECTED_PREEMPT: usize =
101    bpf_intf::layer_usage_LAYER_USAGE_PROTECTED_PREEMPT as usize;
102const NR_LAYER_USAGES: usize = bpf_intf::layer_usage_NR_LAYER_USAGES as usize;
103
104const NR_GSTATS: usize = bpf_intf::global_stat_id_NR_GSTATS as usize;
105const NR_LSTATS: usize = bpf_intf::layer_stat_id_NR_LSTATS as usize;
106const NR_LLC_LSTATS: usize = bpf_intf::llc_layer_stat_id_NR_LLC_LSTATS as usize;
107
108const NR_LAYER_MATCH_KINDS: usize = bpf_intf::layer_match_kind_NR_LAYER_MATCH_KINDS as usize;
109
110static NVML: OnceCell<Nvml> = OnceCell::new();
111
112fn nvml() -> Result<&'static Nvml, NvmlError> {
113    NVML.get_or_try_init(Nvml::init)
114}
115
116lazy_static! {
117    static ref USAGE_DECAY: f64 = 0.5f64.powf(1.0 / USAGE_HALF_LIFE_F64);
118    static ref DFL_DISALLOW_OPEN_AFTER_US: u64 = 2 * scx_enums.SCX_SLICE_DFL / 1000;
119    static ref DFL_DISALLOW_PREEMPT_AFTER_US: u64 = 4 * scx_enums.SCX_SLICE_DFL / 1000;
120    static ref EXAMPLE_CONFIG: LayerConfig = serde_json::from_str(
121        r#"[
122          {
123            "name": "batch",
124            "comment": "tasks under system.slice or tasks with nice value > 0",
125            "matches": [[{"CgroupPrefix": "system.slice/"}], [{"NiceAbove": 0}]],
126            "kind": {"Confined": {
127              "util_range": [0.8, 0.9], "cpus_range": [0, 16],
128              "min_exec_us": 1000, "slice_us": 20000, "weight": 100,
129              "xllc_mig_min_us": 1000.0, "perf": 1024
130            }}
131          },
132          {
133            "name": "immediate",
134            "comment": "tasks under workload.slice with nice value < 0",
135            "matches": [[{"CgroupPrefix": "workload.slice/"}, {"NiceBelow": 0}]],
136            "kind": {"Open": {
137              "min_exec_us": 100, "yield_ignore": 0.25, "slice_us": 20000,
138              "preempt": true, "exclusive": true,
139              "prev_over_idle_core": true,
140              "weight": 100, "perf": 1024
141            }}
142          },
143          {
144            "name": "stress-ng",
145            "comment": "stress-ng test layer",
146            "matches": [[{"CommPrefix": "stress-ng"}], [{"PcommPrefix": "stress-ng"}]],
147            "kind": {"Confined": {
148              "util_range": [0.2, 0.8],
149              "min_exec_us": 800, "preempt": true, "slice_us": 800,
150              "weight": 100, "growth_algo": "Topo", "perf": 1024
151            }}
152          },
153          {
154            "name": "normal",
155            "comment": "the rest",
156            "matches": [[]],
157            "kind": {"Grouped": {
158              "util_range": [0.5, 0.6], "util_includes_open_cputime": true,
159              "min_exec_us": 200, "slice_us": 20000, "weight": 100,
160              "xllc_mig_min_us": 100.0, "growth_algo": "Linear", "perf": 1024
161            }}
162          }
163        ]"#,
164    )
165    .unwrap();
166}
167
168/// scx_layered: A highly configurable multi-layer sched_ext scheduler
169///
170/// scx_layered allows classifying tasks into multiple layers and applying
171/// different scheduling policies to them. The configuration is specified in
172/// json and composed of two parts - matches and policies.
173///
174/// Matches
175/// =======
176///
177/// Whenever a task is forked or its attributes are changed, the task goes
178/// through a series of matches to determine the layer it belongs to. A
179/// match set is composed of OR groups of AND blocks. An example:
180///
181///   "matches": [
182///     [
183///       {
184///         "CgroupPrefix": "system.slice/"
185///       }
186///     ],
187///     [
188///       {
189///         "CommPrefix": "fbagent"
190///       },
191///       {
192///         "NiceAbove": 0
193///       }
194///     ]
195///   ],
196///
197/// The outer array contains the OR groups and the inner AND blocks, so the
198/// above matches:
199///
200/// - Tasks which are in the cgroup sub-hierarchy under "system.slice".
201///
202/// - Or tasks whose comm starts with "fbagent" and have a nice value > 0.
203///
204/// Currently, the following matches are supported:
205///
206/// - CgroupPrefix: Matches the prefix of the cgroup that the task belongs
207///   to. As this is a string match, whether the pattern has the trailing
208///   '/' makes a difference. For example, "TOP/CHILD/" only matches tasks
209///   which are under that particular cgroup while "TOP/CHILD" also matches
210///   tasks under "TOP/CHILD0/" or "TOP/CHILD1/".
211///
212/// - CommPrefix: Matches the task's comm prefix.
213///
214/// - PcommPrefix: Matches the task's thread group leader's comm prefix.
215///
216/// - NiceAbove: Matches if the task's nice value is greater than the
217///   pattern.
218///
219/// - NiceBelow: Matches if the task's nice value is smaller than the
220///   pattern.
221///
222/// - NiceEquals: Matches if the task's nice value is exactly equal to
223///   the pattern.
224///
225/// - UIDEquals: Matches if the task's effective user id matches the value
226///
227/// - GIDEquals: Matches if the task's effective group id matches the value.
228///
229/// - PIDEquals: Matches if the task's pid matches the value.
230///
231/// - PPIDEquals: Matches if the task's ppid matches the value.
232///
233/// - TGIDEquals: Matches if the task's tgid matches the value.
234///
235/// - NSPIDEquals: Matches if the task's namespace id and pid matches the values.
236///
237/// - NSEquals: Matches if the task's namespace id matches the values.
238///
239/// - IsGroupLeader: Bool. When true, matches if the task is group leader
240///   (i.e. PID == TGID), aka the thread from which other threads are made.
241///   When false, matches if the task is *not* the group leader (i.e. the rest).
242///
243/// - CmdJoin: Matches when the task uses pthread_setname_np to send a join/leave
244///   command to the scheduler. See examples/cmdjoin.c for more details.
245///
246/// - UsedGpuTid: Bool. When true, matches if the tasks which have used
247///   gpus by tid.
248///
249/// - UsedGpuPid: Bool. When true, matches if the tasks which have used gpu
250///   by tgid/pid.
251///
252/// - [EXPERIMENTAL] AvgRuntime: (u64, u64). Match tasks whose average runtime
253///   is within the provided values [min, max).
254///
255/// - HintEquals: u64. Match tasks whose hint value equals this value.
256///   The value must be in the range [0, 1024].
257///
258/// - SystemCpuUtilBelow: f64. Match when the system CPU utilization fraction
259///   is below the specified threshold (a value in the range [0.0, 1.0]). This
260///   option can only be used in conjunction with HintEquals.
261///
262/// - DsqInsertBelow: f64. Match when the layer DSQ insertion fraction is below
263///   the specified threshold (a value in the range [0.0, 1.0]). This option can
264///   only be used in conjunction with HintEquals.
265///
266/// While there are complexity limitations as the matches are performed in
267/// BPF, it is straightforward to add more types of matches.
268///
269/// Templates
270/// ---------
271///
272/// Templates let us create a variable number of layers dynamically at initialization
273/// time out of a cgroup name suffix/prefix. Sometimes we know there are multiple
274/// applications running on a machine, each with their own cgroup but do not know the
275/// exact names of the applications or cgroups, e.g., in cloud computing contexts where
276/// workloads are placed on machines dynamically and run under cgroups whose name is
277/// autogenerated. In that case, we cannot hardcode the cgroup match rules when writing
278/// the configuration. We thus cannot easily prevent tasks from different cgroups from
279/// falling into the same layer and affecting each other's performance.
280///
281///
282/// Templates offer a solution to this problem by generating one layer for each such cgroup,
283/// provided these cgroups share a suffix, and that the suffix is unique to them. Templates
284/// have a cgroup suffix rule that we use to find the relevant cgroups in the system. For each
285/// such cgroup, we copy the layer config and add a matching rule that matches just this cgroup.
286///
287///
288/// Policies
289/// ========
290///
291/// The following is an example policy configuration for a layer.
292///
293///   "kind": {
294///     "Confined": {
295///       "cpus_range": [1, 8],
296///       "util_range": [0.8, 0.9]
297///     }
298///   }
299///
300/// It's of "Confined" kind, which tries to concentrate the layer's tasks
301/// into a limited number of CPUs. In the above case, the number of CPUs
302/// assigned to the layer is scaled between 1 and 8 so that the per-cpu
303/// utilization is kept between 80% and 90%. If the CPUs are loaded higher
304/// than 90%, more CPUs are allocated to the layer. If the utilization drops
305/// below 80%, the layer loses CPUs.
306///
307/// Currently, the following policy kinds are supported:
308///
309/// - Confined: Tasks are restricted to the allocated CPUs. The number of
310///   CPUs allocated is modulated to keep the per-CPU utilization in
311///   "util_range". The range can optionally be restricted with the
312///   "cpus_range" property.
313///
314/// - Grouped: Similar to Confined but tasks may spill outside if there are
315///   idle CPUs outside the allocated ones. The range can optionally be
316///   restricted with the "cpus_range" property. The optional
317///   "idle_confined" flag restricts idle selection to
318///   the layer's CPUs until the layer becomes saturated or the system
319///   is fully allocated, providing Confined-style cache locality under
320///   normal load with Grouped-style overflow under pressure.
321///
322/// - Open: Prefer the CPUs which are not occupied by Confined or Grouped
323///   layers. Tasks in this group will spill into occupied CPUs if there are
324///   no unoccupied idle CPUs.
325///
326/// All layers take the following options:
327///
328/// - min_exec_us: Minimum execution time in microseconds. Whenever a task
329///   is scheduled in, this is the minimum CPU time that it's charged no
330///   matter how short the actual execution time may be.
331///
332/// - yield_ignore: Yield ignore ratio. If 0.0, yield(2) forfeits a whole
333///   execution slice. 0.25 yields three quarters of an execution slice and
334///   so on. If 1.0, yield is completely ignored.
335///
336/// - slice_us: Scheduling slice duration in microseconds.
337///
338/// - fifo: Use FIFO queues within the layer instead of the default vtime.
339///
340/// - preempt: If true, tasks in the layer will preempt tasks which belong
341///   to other non-preempting layers when no idle CPUs are available.
342///
343/// - preempt_first: If true, tasks in the layer will try to preempt tasks
344///   in their previous CPUs before trying to find idle CPUs.
345///
346/// - exclusive: If true, tasks in the layer will occupy the whole core. The
347///   other logical CPUs sharing the same core will be kept idle. This isn't
348///   a hard guarantee, so don't depend on it for security purposes.
349///
350/// - allow_node_aligned: DEPRECATED. Node-aligned tasks are now always
351///   dispatched on layer DSQs. This field is ignored if specified.
352///
353/// - prev_over_idle_core: On SMT enabled systems, prefer using the same CPU
354///   when picking a CPU for tasks on this layer, even if that CPUs SMT
355///   sibling is processing a task.
356///
357/// - weight: Weight of the layer, which is a range from 1 to 10000 with a
358///   default of 100. Layer weights are used during contention to prevent
359///   starvation across layers. Weights are used in combination with
360///   utilization to determine the infeasible adjusted weight with higher
361///   weights having a larger adjustment in adjusted utilization.
362///
363/// - disallow_open_after_us: Duration to wait after machine reaches saturation
364///   before confining tasks in Open layers.
365///
366/// - cpus_range_frac: Array of 2 floats between 0 and 1.0. Lower and upper
367///   bound fractions of all CPUs to give to a layer. Mutually exclusive
368///   with cpus_range.
369///
370/// - disallow_preempt_after_us: Duration to wait after machine reaches saturation
371///   before confining tasks to preempt.
372///
373/// - xllc_mig_min_us: Skip cross-LLC migrations if they are likely to run on
374///   their existing LLC sooner than this.
375///
376/// - idle_smt: *** DEPRECATED ****
377///
378/// - growth_algo: Determines the order in which CPUs are allocated to the
379///   layer as it grows. All algorithms are NUMA-aware and produce per-node
380///   core orderings. Three placement classes exist:
381///
382///   * Locality (most algorithms): prefer the layer's home NUMA node and
383///     spill to remote nodes only when local capacity is exhausted.
384///   * Balanced (RoundRobin): distribute proportionally across all NUMA
385///     nodes via cap-weighted water_fill. A congested node reduces its
386///     share but does not cap the total — freed capacity flows to less
387///     loaded nodes.
388///   * Strict spread (NodeSpread*): enforce equal CPU counts across all
389///     NUMA nodes, capped at the least available node. Use when the
390///     workload needs strictly equal per-node CPU counts (memory-
391///     bandwidth-bound or replicated work).
392///
393///   Default: Sticky.
394///
395/// - perf: CPU performance target. 0 means no configuration. A value
396///   between 1 and 1024 indicates the performance level CPUs running tasks
397///   in this layer are configured to using scx_bpf_cpuperf_set().
398///
399/// - idle_resume_us: Sets the idle resume QoS value. CPU idle time governors are expected to
400///   regard the minimum of the global (effective) CPU latency limit and the effective resume
401///   latency constraint for the given CPU as the upper limit for the exit latency of the idle
402///   states. See the latest kernel docs for more details:
403///   https://www.kernel.org/doc/html/latest/admin-guide/pm/cpuidle.html
404///
405/// - nodes: If set the layer will use the set of NUMA nodes for scheduling
406///   decisions. If unset then all available NUMA nodes will be used. If the
407///   llcs value is set the cpuset of NUMA nodes will be or'ed with the LLC
408///   config.
409///
410/// - llcs: If set the layer will use the set of LLCs (last level caches)
411///   for scheduling decisions. If unset then all LLCs will be used. If
412///   the nodes value is set the cpuset of LLCs will be or'ed with the nodes
413///   config.
414///
415///
416/// Similar to matches, adding new policies and extending existing ones
417/// should be relatively straightforward.
418///
419/// Configuration example and running scx_layered
420/// =============================================
421///
422/// An scx_layered config is composed of layer configs. A layer config is
423/// composed of a name, a set of matches, and a policy block. Running the
424/// following will write an example configuration into example.json.
425///
426///   $ scx_layered -e example.json
427///
428/// Note that the last layer in the configuration must have an empty match set
429/// as a catch-all for tasks which haven't been matched into previous layers.
430///
431/// The configuration can be specified in multiple json files and
432/// command line arguments, which are concatenated in the specified
433/// order. Each must contain valid layer configurations.
434///
435/// By default, an argument to scx_layered is interpreted as a JSON string. If
436/// the argument is a pointer to a JSON file, it should be prefixed with file:
437/// or f: as follows:
438///
439///   $ scx_layered file:example.json
440///   ...
441///   $ scx_layered f:example.json
442///
443/// Monitoring Statistics
444/// =====================
445///
446/// Run with `--stats INTERVAL` to enable stats monitoring. There is
447/// also an scx_stat server listening on /var/run/scx/root/stat that can
448/// be monitored by running `scx_layered --monitor INTERVAL` separately.
449///
450///   ```bash
451///   $ scx_layered --monitor 1
452///   tot= 117909 local=86.20 open_idle= 0.21 affn_viol= 1.37 proc=6ms
453///   busy= 34.2 util= 1733.6 load=  21744.1 fb_cpus=[n0:1]
454///     batch    : util/frac=   11.8/  0.7 load/frac=     29.7:  0.1 tasks=  2597
455///                tot=   3478 local=67.80 open_idle= 0.00 preempt= 0.00 affn_viol= 0.00
456///                cpus=  2 [  2,  2] 04000001 00000000
457///     immediate: util/frac= 1218.8/ 70.3 load/frac=  21399.9: 98.4 tasks=  1107
458///                tot=  68997 local=90.57 open_idle= 0.26 preempt= 9.36 affn_viol= 0.00
459///                cpus= 50 [ 50, 50] fbfffffe 000fffff
460///     normal   : util/frac=  502.9/ 29.0 load/frac=    314.5:  1.4 tasks=  3512
461///                tot=  45434 local=80.97 open_idle= 0.16 preempt= 0.00 affn_viol= 3.56
462///                cpus= 50 [ 50, 50] fbfffffe 000fffff
463///   ```
464///
465/// Global statistics: see [`SysStats`]
466///
467/// Per-layer statistics: see [`LayerStats`]
468///
469#[derive(Debug, Parser)]
470#[command(verbatim_doc_comment)]
471struct Opts {
472    /// Deprecated, noop, use RUST_LOG or --log-level instead.
473    #[clap(short = 'v', long, action = clap::ArgAction::Count)]
474    verbose: u8,
475
476    /// Scheduling slice duration in microseconds.
477    #[clap(short = 's', long, default_value = "20000")]
478    slice_us: u64,
479
480    /// Maximum consecutive execution time in microseconds. A task may be
481    /// allowed to keep executing on a CPU for this long. Note that this is
482    /// the upper limit and a task may have to moved off the CPU earlier. 0
483    /// indicates default - 20 * slice_us.
484    #[clap(short = 'M', long, default_value = "0")]
485    max_exec_us: u64,
486
487    /// Scheduling interval in seconds.
488    #[clap(short = 'i', long, default_value = "0.1")]
489    interval: f64,
490
491    /// ***DEPRECATED*** Disable load-fraction based max layer CPU limit.
492    /// recommended.
493    #[clap(short = 'n', long, default_value = "false")]
494    no_load_frac_limit: bool,
495
496    /// Exit debug dump buffer length. 0 indicates default.
497    #[clap(long, default_value = "0")]
498    exit_dump_len: u32,
499
500    /// Specify the logging level. Accepts rust's envfilter syntax for modular
501    /// logging: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax. Examples: ["info", "warn,tokio=info"]
502    #[clap(long, default_value = "info")]
503    log_level: String,
504
505    /// Disable topology awareness. When enabled, the "nodes" and "llcs" settings on
506    /// a layer are ignored. Defaults to false on topologies with multiple NUMA nodes
507    /// or LLCs, and true otherwise.
508    #[arg(short = 't', long, num_args = 0..=1, default_missing_value = "true", require_equals = true)]
509    disable_topology: Option<bool>,
510
511    /// Disable monitor
512    #[clap(long)]
513    monitor_disable: bool,
514
515    /// Write example layer specifications into the file and exit.
516    #[clap(short = 'e', long)]
517    example: Option<String>,
518
519    /// ***DEPRECATED*** Disables preemption if the weighted load fraction
520    /// of a layer (load_frac_adj) exceeds the threshold. The default is
521    /// disabled (0.0).
522    #[clap(long, default_value = "0.0")]
523    layer_preempt_weight_disable: f64,
524
525    /// ***DEPRECATED*** Disables layer growth if the weighted load fraction
526    /// of a layer (load_frac_adj) exceeds the threshold. The default is
527    /// disabled (0.0).
528    #[clap(long, default_value = "0.0")]
529    layer_growth_weight_disable: f64,
530
531    /// Enable stats monitoring with the specified interval.
532    #[clap(long)]
533    stats: Option<f64>,
534
535    /// Run in stats monitoring mode with the specified interval. Scheduler
536    /// is not launched.
537    #[clap(long)]
538    monitor: Option<f64>,
539
540    /// Column limit for stats monitor output.
541    #[clap(long, default_value = "95")]
542    stats_columns: usize,
543
544    /// Disable per-LLC stats in monitor output.
545    #[clap(long)]
546    stats_no_llc: bool,
547
548    /// Run with example layer specifications (useful for e.g. CI pipelines)
549    #[clap(long)]
550    run_example: bool,
551
552    /// Allocate CPUs at an SMT granularity (not core)
553    #[clap(long)]
554    allow_partial_core: bool,
555
556    /// ***DEPRECATED *** Enables iteration over local LLCs first for
557    /// dispatch.
558    #[clap(long, default_value = "false")]
559    local_llc_iteration: bool,
560
561    /// Low priority fallback DSQs are used to execute tasks with custom CPU
562    /// affinities. These DSQs are immediately executed iff a CPU is
563    /// otherwise idle. However, after the specified wait, they are
564    /// guaranteed upto --lo-fb-share fraction of each CPU.
565    #[clap(long, default_value = "10000")]
566    lo_fb_wait_us: u64,
567
568    /// The fraction of CPU time guaranteed to low priority fallback DSQs.
569    /// See --lo-fb-wait-us.
570    #[clap(long, default_value = ".05")]
571    lo_fb_share: f64,
572
573    /// Disable antistall
574    #[clap(long, default_value = "false")]
575    disable_antistall: bool,
576
577    /// Enable numa topology based gpu task affinitization.
578    #[clap(long, default_value = "false")]
579    enable_gpu_affinitize: bool,
580
581    /// Interval at which to reaffinitize gpu tasks to numa nodes.
582    /// Defaults to 900s
583    #[clap(long, default_value = "900")]
584    gpu_affinitize_secs: u64,
585
586    /// Enable match debug
587    /// This stores a mapping of task tid
588    /// to layer id such that bpftool map dump
589    /// can be used to debug layer matches.
590    #[clap(long, default_value = "false")]
591    enable_match_debug: bool,
592
593    /// Maximum task runnable_at delay (in seconds) before antistall turns on
594    #[clap(long, default_value = "3")]
595    antistall_sec: u64,
596
597    /// Enable gpu support
598    #[clap(long, default_value = "false")]
599    enable_gpu_support: bool,
600
601    /// Gpu Kprobe Level
602    /// The value set here determines how aggressive
603    /// the kprobes enabled on gpu driver functions are.
604    /// Higher values are more aggressive, incurring more system overhead
605    /// and more accurately identifying PIDs using GPUs in a more timely manner.
606    /// Lower values incur less system overhead, at the cost of less accurately
607    /// identifying GPU pids and taking longer to do so.
608    #[clap(long, default_value = "3")]
609    gpu_kprobe_level: u64,
610
611    /// Enable utilization compensation for unattributed CPU work (irq, softirq, stolen). When
612    /// enabled, each CPU's layer usage is scaled by the inverse of available capacity to account
613    /// for time lost to interrupts.
614    #[clap(long, default_value = "false")]
615    util_compensation: bool,
616
617    /// Enable netdev IRQ balancing. This is experimental and should be used with caution.
618    #[clap(long, default_value = "false")]
619    netdev_irq_balance: bool,
620
621    /// Disable queued wakeup optimization.
622    #[clap(long, default_value = "false")]
623    disable_queued_wakeup: bool,
624
625    /// Per-cpu kthreads are preempting by default. Make it not so.
626    #[clap(long, default_value = "false")]
627    disable_percpu_kthread_preempt: bool,
628
629    /// Only highpri (nice < 0) per-cpu kthreads are preempting by default.
630    /// Make every per-cpu kthread preempting. Meaningful only if
631    /// --disable-percpu-kthread-preempt is not set.
632    #[clap(long, default_value = "false")]
633    percpu_kthread_preempt_all: bool,
634
635    /// Print scheduler version and exit.
636    #[clap(short = 'V', long, action = clap::ArgAction::SetTrue)]
637    version: bool,
638
639    /// Optional run ID for tracking scheduler instances.
640    #[clap(long)]
641    run_id: Option<u64>,
642
643    /// Show descriptions for statistics.
644    #[clap(long)]
645    help_stats: bool,
646
647    /// Layer specification. See --help.
648    specs: Vec<String>,
649
650    /// Periodically force tasks in layers using the AvgRuntime match rule to reevaluate which layer they belong to. Default period of 2s.
651    /// turns this off.
652    #[clap(long, default_value = "2000")]
653    layer_refresh_ms_avgruntime: u64,
654
655    /// Set the path for pinning the task hint map.
656    #[clap(long, default_value = "")]
657    task_hint_map: String,
658
659    /// Print the config (after template expansion) and exit.
660    #[clap(long, default_value = "false")]
661    print_and_exit: bool,
662
663    /// Enable affinitized task to use hi fallback queue to get more CPU time.
664    #[clap(long, default_value = "")]
665    hi_fb_thread_name: String,
666
667    #[clap(flatten, next_help_heading = "Topology Options")]
668    topology: TopologyArgs,
669
670    #[clap(flatten, next_help_heading = "Libbpf Options")]
671    pub libbpf: LibbpfOpts,
672}
673
674// Cgroup event types for inter-thread communication
675#[derive(Debug, Clone)]
676enum CgroupEvent {
677    Created {
678        path: String,
679        cgroup_id: u64,    // inode number
680        match_bitmap: u64, // bitmap of matched regex rules
681    },
682    Removed {
683        path: String,
684        cgroup_id: u64, // inode number
685    },
686}
687
688fn read_total_cpu(reader: &fb_procfs::ProcReader) -> Result<fb_procfs::CpuStat> {
689    reader
690        .read_stat()
691        .context("Failed to read procfs")?
692        .total_cpu
693        .ok_or_else(|| anyhow!("Could not read total cpu stat in proc"))
694}
695
696fn calc_util(curr: &fb_procfs::CpuStat, prev: &fb_procfs::CpuStat) -> Result<f64> {
697    match (curr, prev) {
698        (
699            fb_procfs::CpuStat {
700                user_usec: Some(curr_user),
701                nice_usec: Some(curr_nice),
702                system_usec: Some(curr_system),
703                idle_usec: Some(curr_idle),
704                iowait_usec: Some(curr_iowait),
705                irq_usec: Some(curr_irq),
706                softirq_usec: Some(curr_softirq),
707                stolen_usec: Some(curr_stolen),
708                ..
709            },
710            fb_procfs::CpuStat {
711                user_usec: Some(prev_user),
712                nice_usec: Some(prev_nice),
713                system_usec: Some(prev_system),
714                idle_usec: Some(prev_idle),
715                iowait_usec: Some(prev_iowait),
716                irq_usec: Some(prev_irq),
717                softirq_usec: Some(prev_softirq),
718                stolen_usec: Some(prev_stolen),
719                ..
720            },
721        ) => {
722            let idle_usec = curr_idle.saturating_sub(*prev_idle);
723            let iowait_usec = curr_iowait.saturating_sub(*prev_iowait);
724            let user_usec = curr_user.saturating_sub(*prev_user);
725            let system_usec = curr_system.saturating_sub(*prev_system);
726            let nice_usec = curr_nice.saturating_sub(*prev_nice);
727            let irq_usec = curr_irq.saturating_sub(*prev_irq);
728            let softirq_usec = curr_softirq.saturating_sub(*prev_softirq);
729            let stolen_usec = curr_stolen.saturating_sub(*prev_stolen);
730
731            let busy_usec =
732                user_usec + system_usec + nice_usec + irq_usec + softirq_usec + stolen_usec;
733            let total_usec = idle_usec + busy_usec + iowait_usec;
734            if total_usec > 0 {
735                Ok(((busy_usec as f64) / (total_usec as f64)).clamp(0.0, 1.0))
736            } else {
737                Ok(1.0)
738            }
739        }
740        _ => bail!("Missing stats in cpustat"),
741    }
742}
743
744fn copy_into_cstr(dst: &mut [i8], src: &str) {
745    let cstr = CString::new(src).unwrap();
746    let bytes = unsafe { std::mem::transmute::<&[u8], &[i8]>(cstr.as_bytes_with_nul()) };
747    dst[0..bytes.len()].copy_from_slice(bytes);
748}
749
750#[allow(clippy::needless_range_loop)]
751fn read_cpu_ctxs(skel: &BpfSkel) -> Result<Vec<bpf_intf::cpu_ctx>> {
752    let mut cpu_ctxs = vec![];
753    let cpu_ctxs_vec = skel
754        .maps
755        .cpu_ctxs
756        .lookup_percpu(&0u32.to_ne_bytes(), libbpf_rs::MapFlags::ANY)
757        .context("Failed to lookup cpu_ctx")?
758        .unwrap();
759    for cpu in 0..*NR_CPUS_POSSIBLE {
760        cpu_ctxs.push(
761            *plain::from_bytes(cpu_ctxs_vec[cpu].as_slice())
762                .expect("cpu_ctx: short or misaligned buffer"),
763        );
764    }
765    Ok(cpu_ctxs)
766}
767
768#[derive(Clone, Debug)]
769struct BpfStats {
770    gstats: Vec<u64>,
771    lstats: Vec<Vec<u64>>,
772    lstats_sums: Vec<u64>,
773    llc_lstats: Vec<Vec<Vec<u64>>>, // [layer][llc][stat]
774}
775
776impl BpfStats {
777    #[allow(clippy::needless_range_loop)]
778    fn read(skel: &BpfSkel, cpu_ctxs: &[bpf_intf::cpu_ctx]) -> Self {
779        let nr_layers = skel.maps.rodata_data.as_ref().unwrap().nr_layers as usize;
780        let nr_llcs = skel.maps.rodata_data.as_ref().unwrap().nr_llcs as usize;
781        let mut gstats = vec![0u64; NR_GSTATS];
782        let mut lstats = vec![vec![0u64; NR_LSTATS]; nr_layers];
783        let mut llc_lstats = vec![vec![vec![0u64; NR_LLC_LSTATS]; nr_llcs]; nr_layers];
784
785        for cpu in 0..*NR_CPUS_POSSIBLE {
786            for (stat, value) in gstats.iter_mut().enumerate() {
787                *value += cpu_ctxs[cpu].gstats[stat];
788            }
789            for (layer, layer_stats) in lstats.iter_mut().enumerate() {
790                for (stat, value) in layer_stats.iter_mut().enumerate() {
791                    *value += cpu_ctxs[cpu].lstats[layer][stat];
792                }
793            }
794        }
795
796        let mut lstats_sums = vec![0u64; NR_LSTATS];
797        for layer_stats in lstats.iter() {
798            for (stat, value) in lstats_sums.iter_mut().enumerate() {
799                *value += layer_stats[stat];
800            }
801        }
802
803        for llc_id in 0..nr_llcs {
804            // XXX - This would be a lot easier if llc_ctx were in
805            // the bss. Unfortunately, kernel < v6.12 crashes and
806            // kernel >= v6.12 fails verification after such
807            // conversion due to seemingly verifier bugs. Convert to
808            // bss maps later.
809            let v = skel
810                .maps
811                .llc_data
812                .lookup(&(llc_id as u32).to_ne_bytes(), libbpf_rs::MapFlags::ANY)
813                .unwrap()
814                .unwrap();
815            let llcc: &bpf_intf::llc_ctx =
816                plain::from_bytes(v.as_slice()).expect("llc_ctx: short or misaligned buffer");
817
818            for (layer_id, layer_llc_stats) in llc_lstats.iter_mut().enumerate() {
819                for (stat_id, stat_value) in layer_llc_stats[llc_id].iter_mut().enumerate() {
820                    *stat_value = llcc.lstats[layer_id][stat_id];
821                }
822            }
823        }
824
825        Self {
826            gstats,
827            lstats,
828            lstats_sums,
829            llc_lstats,
830        }
831    }
832}
833
834impl<'b> Sub<&'b BpfStats> for &BpfStats {
835    type Output = BpfStats;
836
837    fn sub(self, rhs: &'b BpfStats) -> BpfStats {
838        let vec_sub = |l: &[u64], r: &[u64]| l.iter().zip(r.iter()).map(|(l, r)| *l - *r).collect();
839        BpfStats {
840            gstats: vec_sub(&self.gstats, &rhs.gstats),
841            lstats: self
842                .lstats
843                .iter()
844                .zip(rhs.lstats.iter())
845                .map(|(l, r)| vec_sub(l, r))
846                .collect(),
847            lstats_sums: vec_sub(&self.lstats_sums, &rhs.lstats_sums),
848            llc_lstats: self
849                .llc_lstats
850                .iter()
851                .zip(rhs.llc_lstats.iter())
852                .map(|(l_layer, r_layer)| {
853                    l_layer
854                        .iter()
855                        .zip(r_layer.iter())
856                        .map(|(l_llc, r_llc)| {
857                            let (l_llc, mut r_llc) = (l_llc.clone(), r_llc.clone());
858                            // Lat is not subtractable, take L side.
859                            r_llc[bpf_intf::llc_layer_stat_id_LLC_LSTAT_LAT as usize] = 0;
860                            vec_sub(&l_llc, &r_llc)
861                        })
862                        .collect()
863                })
864                .collect(),
865        }
866    }
867}
868
869#[derive(Clone, Debug)]
870struct Stats {
871    at: Instant,
872    elapsed: Duration,
873    topo: Arc<Topology>,
874    nr_layers: usize,
875    nr_layer_tasks: Vec<usize>,
876    layer_nr_node_pinned_tasks: Vec<Vec<u64>>,
877
878    total_util: f64, // Running AVG of sum of layer_utils
879    layer_utils: Vec<Vec<f64>>,
880    layer_node_pinned_utils: Vec<Vec<f64>>,
881    prev_layer_node_pinned_usages: Vec<Vec<u64>>,
882    layer_node_utils: Vec<Vec<f64>>,
883    prev_layer_node_usages: Vec<Vec<u64>>,
884    layer_node_duty_sums: Vec<Vec<f64>>, // Per-node per-layer duty in CPU units (EWMA)
885    prev_layer_node_duty_raw: Vec<Vec<u64>>, // Raw accumulated layer_duty_sum values
886
887    layer_membws: Vec<Vec<f64>>, // Estimated memory bandsidth consumption
888    prev_layer_membw_agg: Vec<Vec<u64>>, // Estimated aggregate membw consumption
889
890    cpu_busy: f64, // Read from /proc, maybe higher than total_util
891    prev_total_cpu: fb_procfs::CpuStat,
892    prev_pmu_resctrl_membw: (u64, u64), // (PMU-reported membw, resctrl-reported membw)
893
894    util_compensation: bool,
895    layer_utils_compensated: Vec<Vec<f64>>, // EWMA of per-CPU-scaled layer utils
896    prev_cpu_layer_usages: Vec<u64>,        // Per-CPU per-layer usages for computing deltas
897    prev_per_cpu_stats: BTreeMap<u32, fb_procfs::CpuStat>,
898
899    system_cpu_util_ewma: f64,       // 10s EWMA of system CPU utilization
900    layer_dsq_insert_ewma: Vec<f64>, // 10s EWMA of per-layer DSQ insertion ratio
901
902    bpf_stats: BpfStats,
903    prev_bpf_stats: BpfStats,
904
905    processing_dur: Duration,
906    prev_processing_dur: Duration,
907
908    layer_slice_us: Vec<u64>,
909
910    gpu_tasks_affinitized: u64,
911    gpu_task_affinitization_ms: u64,
912}
913
914impl Stats {
915    #[allow(clippy::needless_range_loop)]
916    fn read_layer_membw_agg(cpu_ctxs: &[bpf_intf::cpu_ctx], nr_layers: usize) -> Vec<Vec<u64>> {
917        let mut layer_membw_agg = vec![vec![0u64; NR_LAYER_USAGES]; nr_layers];
918
919        for cpu in 0..*NR_CPUS_POSSIBLE {
920            for (layer, layer_membw) in layer_membw_agg.iter_mut().enumerate() {
921                for (usage, value) in layer_membw.iter_mut().enumerate() {
922                    *value += cpu_ctxs[cpu].layer_membw_agg[layer][usage];
923                }
924            }
925        }
926
927        layer_membw_agg
928    }
929
930    #[allow(clippy::needless_range_loop)]
931    fn read_layer_node_pinned_usages(
932        cpu_ctxs: &[bpf_intf::cpu_ctx],
933        topo: &Topology,
934        nr_layers: usize,
935        nr_nodes: usize,
936    ) -> Vec<Vec<u64>> {
937        let mut usages = vec![vec![0u64; nr_nodes]; nr_layers];
938
939        for cpu in 0..*NR_CPUS_POSSIBLE {
940            let node = topo.all_cpus.get(&cpu).map_or(0, |c| c.node_id);
941            for (layer, layer_usages) in usages.iter_mut().enumerate().take(nr_layers) {
942                layer_usages[node] += cpu_ctxs[cpu].node_pinned_usage[layer];
943            }
944        }
945
946        usages
947    }
948
949    #[allow(clippy::needless_range_loop)]
950    fn read_layer_node_usages(
951        cpu_ctxs: &[bpf_intf::cpu_ctx],
952        topo: &Topology,
953        nr_layers: usize,
954        nr_nodes: usize,
955    ) -> Vec<Vec<u64>> {
956        let mut usages = vec![vec![0u64; nr_nodes]; nr_layers];
957
958        for cpu in 0..*NR_CPUS_POSSIBLE {
959            let node = topo.all_cpus.get(&cpu).map_or(0, |c| c.node_id);
960            for (layer, layer_usages) in usages.iter_mut().enumerate().take(nr_layers) {
961                for usage in 0..=LAYER_USAGE_SUM_UPTO {
962                    layer_usages[node] += cpu_ctxs[cpu].layer_usages[layer][usage];
963                }
964            }
965        }
966
967        usages
968    }
969
970    #[allow(clippy::needless_range_loop)]
971    fn read_layer_node_duty_raw(
972        cpu_ctxs: &[bpf_intf::cpu_ctx],
973        topo: &Topology,
974        nr_layers: usize,
975        nr_nodes: usize,
976    ) -> Vec<Vec<u64>> {
977        let mut sums = vec![vec![0u64; nr_nodes]; nr_layers];
978
979        for cpu in 0..*NR_CPUS_POSSIBLE {
980            let node = topo.all_cpus.get(&cpu).map_or(0, |c| c.node_id);
981            for (layer, layer_sums) in sums.iter_mut().enumerate().take(nr_layers) {
982                layer_sums[node] += cpu_ctxs[cpu].layer_duty_sum[layer];
983            }
984        }
985
986        sums
987    }
988
989    #[allow(clippy::needless_range_loop)]
990    fn read_per_cpu_layer_usages(cpu_ctxs: &[bpf_intf::cpu_ctx], nr_layers: usize) -> Vec<u64> {
991        let stride = nr_layers * NR_LAYER_USAGES;
992        let mut flat = vec![0u64; *NR_CPUS_POSSIBLE * stride];
993
994        for cpu in 0..*NR_CPUS_POSSIBLE {
995            let base = cpu * stride;
996            for layer in 0..nr_layers {
997                for usage in 0..NR_LAYER_USAGES {
998                    flat[base + layer * NR_LAYER_USAGES + usage] =
999                        cpu_ctxs[cpu].layer_usages[layer][usage];
1000                }
1001            }
1002        }
1003
1004        flat
1005    }
1006
1007    /// Use the membw reported by resctrl to normalize the values reported by hw counters.
1008    /// We have the following problem:
1009    /// 1) We want per-task memory bandwidth reporting. We cannot do this with resctrl, much
1010    ///    less transparently, since we would require different RMID for each task.
1011    /// 2) We want to directly use perf counters for tracking per-task memory bandwidth, but
1012    ///    we can't: Non-resctrl counters do not measure the right thing (e.g., they only measure
1013    ///    proxies like load operations),
1014    /// 3) Resctrl counters are not accessible directly so we cannot read them from the BPF side.
1015    ///
1016    /// Approximate per-task memory bandwidth using perf counters to measure _relative_ memory
1017    /// bandwidth usage.
1018    fn resctrl_read_total_membw() -> Result<u64> {
1019        let mut total_membw = 0u64;
1020        for entry in WalkDir::new("/sys/fs/resctrl/mon_data")
1021            .min_depth(1)
1022            .into_iter()
1023            .filter_map(Result::ok)
1024            .filter(|x| x.path().is_dir())
1025        {
1026            let mut path = entry.path().to_path_buf();
1027            path.push("mbm_total_bytes");
1028            total_membw += fs::read_to_string(path)?.trim().parse::<u64>()?;
1029        }
1030
1031        Ok(total_membw)
1032    }
1033
1034    fn new(
1035        skel: &mut BpfSkel,
1036        proc_reader: &fb_procfs::ProcReader,
1037        topo: Arc<Topology>,
1038        gpu_task_affinitizer: &GpuTaskAffinitizer,
1039        util_compensation: bool,
1040    ) -> Result<Self> {
1041        let nr_layers = skel.maps.rodata_data.as_ref().unwrap().nr_layers as usize;
1042        let nr_nodes = topo.nodes.len();
1043        let cpu_ctxs = read_cpu_ctxs(skel)?;
1044        let bpf_stats = BpfStats::read(skel, &cpu_ctxs);
1045        let pmu_membw = Self::read_layer_membw_agg(&cpu_ctxs, nr_layers);
1046
1047        Ok(Self {
1048            at: Instant::now(),
1049            elapsed: Default::default(),
1050
1051            topo: topo.clone(),
1052            nr_layers,
1053            nr_layer_tasks: vec![0; nr_layers],
1054            layer_nr_node_pinned_tasks: vec![vec![0; nr_nodes]; nr_layers],
1055
1056            total_util: 0.0,
1057            layer_utils: vec![vec![0.0; NR_LAYER_USAGES]; nr_layers],
1058            layer_node_pinned_utils: vec![vec![0.0; nr_nodes]; nr_layers],
1059            prev_layer_node_pinned_usages: Self::read_layer_node_pinned_usages(
1060                &cpu_ctxs, &topo, nr_layers, nr_nodes,
1061            ),
1062            layer_node_utils: vec![vec![0.0; nr_nodes]; nr_layers],
1063            prev_layer_node_usages: Self::read_layer_node_usages(
1064                &cpu_ctxs, &topo, nr_layers, nr_nodes,
1065            ),
1066            layer_node_duty_sums: vec![vec![0.0; nr_nodes]; nr_layers],
1067            prev_layer_node_duty_raw: Self::read_layer_node_duty_raw(
1068                &cpu_ctxs, &topo, nr_layers, nr_nodes,
1069            ),
1070            layer_membws: vec![vec![0.0; NR_LAYER_USAGES]; nr_layers],
1071            // This is not normalized because we don't have enough history to do so.
1072            // It should not matter too much, since the value is dropped on the first
1073            // iteration.
1074            prev_layer_membw_agg: pmu_membw,
1075            prev_pmu_resctrl_membw: (0, 0),
1076
1077            cpu_busy: 0.0,
1078            prev_total_cpu: read_total_cpu(proc_reader)?,
1079            util_compensation,
1080            layer_utils_compensated: vec![vec![0.0; NR_LAYER_USAGES]; nr_layers],
1081            prev_cpu_layer_usages: Self::read_per_cpu_layer_usages(&cpu_ctxs, nr_layers),
1082            prev_per_cpu_stats: BTreeMap::new(),
1083            system_cpu_util_ewma: 0.0,
1084            layer_dsq_insert_ewma: vec![0.0; nr_layers],
1085
1086            bpf_stats: bpf_stats.clone(),
1087            prev_bpf_stats: bpf_stats,
1088
1089            processing_dur: Default::default(),
1090            prev_processing_dur: Default::default(),
1091
1092            layer_slice_us: vec![0; nr_layers],
1093            gpu_tasks_affinitized: gpu_task_affinitizer.tasks_affinitized,
1094            gpu_task_affinitization_ms: gpu_task_affinitizer.last_task_affinitization_ms,
1095        })
1096    }
1097
1098    fn refresh(
1099        &mut self,
1100        skel: &mut BpfSkel,
1101        proc_reader: &fb_procfs::ProcReader,
1102        now: Instant,
1103        cur_processing_dur: Duration,
1104        gpu_task_affinitizer: &GpuTaskAffinitizer,
1105    ) -> Result<()> {
1106        let elapsed = now.duration_since(self.at);
1107        let elapsed_f64 = elapsed.as_secs_f64();
1108        let cpu_ctxs = read_cpu_ctxs(skel)?;
1109
1110        let layers = &skel.maps.bss_data.as_ref().unwrap().layers;
1111        let nr_layer_tasks: Vec<usize> = layers
1112            .iter()
1113            .take(self.nr_layers)
1114            .map(|layer| layer.nr_tasks as usize)
1115            .collect();
1116        let layer_nr_node_pinned_tasks: Vec<Vec<u64>> = layers
1117            .iter()
1118            .take(self.nr_layers)
1119            .map(|layer| {
1120                layer.node[..self.topo.nodes.len()]
1121                    .iter()
1122                    .map(|n| n.nr_pinned_tasks)
1123                    .collect()
1124            })
1125            .collect();
1126        let layer_slice_us: Vec<u64> = layers
1127            .iter()
1128            .take(self.nr_layers)
1129            .map(|layer| layer.slice_ns / 1000_u64)
1130            .collect();
1131
1132        let cur_layer_node_pinned_usages = Self::read_layer_node_pinned_usages(
1133            &cpu_ctxs,
1134            &self.topo,
1135            self.nr_layers,
1136            self.topo.nodes.len(),
1137        );
1138        let cur_layer_node_usages = Self::read_layer_node_usages(
1139            &cpu_ctxs,
1140            &self.topo,
1141            self.nr_layers,
1142            self.topo.nodes.len(),
1143        );
1144        let cur_layer_node_duty_raw = Self::read_layer_node_duty_raw(
1145            &cpu_ctxs,
1146            &self.topo,
1147            self.nr_layers,
1148            self.topo.nodes.len(),
1149        );
1150        let cur_layer_membw_agg = Self::read_layer_membw_agg(&cpu_ctxs, self.nr_layers);
1151
1152        // Memory BW normalization. It requires finding the delta according to perf, the delta
1153        // according to resctl, and finding the factor between them. This also helps in
1154        // determining whether this delta is stable.
1155        //
1156        // We scale only the raw PMC delta - the part of the counter incremented between two
1157        // periods. This is because the scaling factor is only valid for that time period.
1158        // Non-scaled samples are not comparable, while scaled ones are.
1159        let (pmu_prev, resctrl_prev) = self.prev_pmu_resctrl_membw;
1160        let pmu_cur: u64 = cur_layer_membw_agg
1161            .iter()
1162            .map(|membw_agg| membw_agg[LAYER_USAGE_OPEN] + membw_agg[LAYER_USAGE_OWNED])
1163            .sum();
1164        let resctrl_cur = Self::resctrl_read_total_membw()?;
1165        let factor = (resctrl_cur - resctrl_prev) as f64 / (pmu_cur - pmu_prev) as f64;
1166
1167        // Computes the runtime deltas and converts them from ns to s.
1168        let compute_diff = |cur_agg: &Vec<Vec<u64>>, prev_agg: &Vec<Vec<u64>>| {
1169            cur_agg
1170                .iter()
1171                .zip(prev_agg.iter())
1172                .map(|(cur, prev)| {
1173                    cur.iter()
1174                        .zip(prev.iter())
1175                        .map(|(c, p)| (c - p) as f64 / 1_000_000_000.0 / elapsed_f64)
1176                        .collect()
1177                })
1178                .collect()
1179        };
1180
1181        // Computes the total memory traffic done since last computation and normalizes to GBs.
1182        // We derive the rate of consumption elsewhere.
1183        let compute_mem_diff = |cur_agg: &Vec<Vec<u64>>, prev_agg: &Vec<Vec<u64>>| {
1184            cur_agg
1185                .iter()
1186                .zip(prev_agg.iter())
1187                .map(|(cur, prev)| {
1188                    cur.iter()
1189                        .zip(prev.iter())
1190                        .map(|(c, p)| (*c as i64 - *p as i64) as f64 / 1024_f64.powf(3.0))
1191                        .collect()
1192                })
1193                .collect()
1194        };
1195
1196        // Scale the raw value delta by the resctrl/pmc computed factor.
1197        let cur_layer_membw: Vec<Vec<f64>> =
1198            compute_mem_diff(&cur_layer_membw_agg, &self.prev_layer_membw_agg);
1199
1200        let cur_layer_membw: Vec<Vec<f64>> = cur_layer_membw
1201            .iter()
1202            .map(|x| x.iter().map(|x| *x * factor).collect())
1203            .collect();
1204
1205        let metric_decay =
1206            |cur_metric: Vec<Vec<f64>>, prev_metric: &Vec<Vec<f64>>, decay_rate: f64| {
1207                cur_metric
1208                    .iter()
1209                    .zip(prev_metric.iter())
1210                    .map(|(cur, prev)| {
1211                        cur.iter()
1212                            .zip(prev.iter())
1213                            .map(|(c, p)| {
1214                                let decay = decay_rate.powf(elapsed_f64);
1215                                p * decay + c * (1.0 - decay)
1216                            })
1217                            .collect()
1218                    })
1219                    .collect()
1220            };
1221
1222        let cur_node_pinned_utils: Vec<Vec<f64>> = compute_diff(
1223            &cur_layer_node_pinned_usages,
1224            &self.prev_layer_node_pinned_usages,
1225        );
1226        let layer_node_pinned_utils: Vec<Vec<f64>> = metric_decay(
1227            cur_node_pinned_utils,
1228            &self.layer_node_pinned_utils,
1229            *USAGE_DECAY,
1230        );
1231        let cur_node_utils: Vec<Vec<f64>> =
1232            compute_diff(&cur_layer_node_usages, &self.prev_layer_node_usages);
1233        let layer_node_utils: Vec<Vec<f64>> =
1234            metric_decay(cur_node_utils, &self.layer_node_utils, *USAGE_DECAY);
1235        let cur_node_duty: Vec<Vec<f64>> =
1236            compute_diff(&cur_layer_node_duty_raw, &self.prev_layer_node_duty_raw);
1237        let layer_node_duty_sums: Vec<Vec<f64>> =
1238            metric_decay(cur_node_duty, &self.layer_node_duty_sums, *USAGE_DECAY);
1239
1240        let layer_membws: Vec<Vec<f64>> = metric_decay(cur_layer_membw, &self.layer_membws, 0.0);
1241
1242        let proc_stat = proc_reader
1243            .read_stat()
1244            .context("Failed to read /proc/stat")?;
1245        let cur_total_cpu = proc_stat
1246            .total_cpu
1247            .ok_or_else(|| anyhow!("Could not read total cpu stat in proc"))?;
1248        let cpu_busy = calc_util(&cur_total_cpu, &self.prev_total_cpu)?;
1249
1250        // Calculate system CPU utilization EWMA (10 second window)
1251        const SYS_CPU_UTIL_EWMA_SECS: f64 = 10.0;
1252        let elapsed_f64 = elapsed.as_secs_f64();
1253        let alpha = elapsed_f64 / SYS_CPU_UTIL_EWMA_SECS.max(elapsed_f64);
1254        let system_cpu_util_ewma = alpha * cpu_busy + (1.0 - alpha) * self.system_cpu_util_ewma;
1255
1256        // Per-CPU scale factors: s[c] = Δt / (Δt - irq - softirq - stolen).
1257        // When compensation is off, all scales stay 1.0.
1258        let cur_per_cpu_stats = proc_stat.cpus_map.unwrap_or_default();
1259        let mut cpu_scales = vec![1.0f64; *NR_CPUS_POSSIBLE];
1260        if self.util_compensation {
1261            for (&cpu_id, cur_cpu_stat) in &cur_per_cpu_stats {
1262                let cpu = cpu_id as usize;
1263                if let Some(prev_cpu_stat) = self.prev_per_cpu_stats.get(&cpu_id)
1264                    && let (
1265                        fb_procfs::CpuStat {
1266                            user_usec: Some(cu),
1267                            nice_usec: Some(cn),
1268                            system_usec: Some(cs),
1269                            idle_usec: Some(ci),
1270                            iowait_usec: Some(cw),
1271                            irq_usec: Some(cq),
1272                            softirq_usec: Some(cf),
1273                            stolen_usec: Some(ct),
1274                            ..
1275                        },
1276                        fb_procfs::CpuStat {
1277                            user_usec: Some(pu),
1278                            nice_usec: Some(pn),
1279                            system_usec: Some(ps),
1280                            idle_usec: Some(pi),
1281                            iowait_usec: Some(pw),
1282                            irq_usec: Some(pq),
1283                            softirq_usec: Some(pf),
1284                            stolen_usec: Some(pt),
1285                            ..
1286                        },
1287                    ) = (cur_cpu_stat, prev_cpu_stat)
1288                {
1289                    let delta_total = cu.saturating_sub(*pu)
1290                        + cn.saturating_sub(*pn)
1291                        + cs.saturating_sub(*ps)
1292                        + ci.saturating_sub(*pi)
1293                        + cw.saturating_sub(*pw)
1294                        + cq.saturating_sub(*pq)
1295                        + cf.saturating_sub(*pf)
1296                        + ct.saturating_sub(*pt);
1297                    let overhead =
1298                        cq.saturating_sub(*pq) + cf.saturating_sub(*pf) + ct.saturating_sub(*pt);
1299                    let available = delta_total.saturating_sub(overhead);
1300                    cpu_scales[cpu] = if available > 0 {
1301                        (delta_total as f64 / available as f64).clamp(1.0, 20.0)
1302                    } else {
1303                        1.0
1304                    };
1305                }
1306            }
1307        }
1308
1309        // Single pass over all CPUs: build both raw and compensated layer
1310        // util streams. When compensation is off, all scales are 1.0 so
1311        // comp == raw naturally.
1312        let cur_cpu_layer_usages = Self::read_per_cpu_layer_usages(&cpu_ctxs, self.nr_layers);
1313        let stride = self.nr_layers * NR_LAYER_USAGES;
1314        let mut raw_sums = vec![vec![0.0f64; NR_LAYER_USAGES]; self.nr_layers];
1315        let mut scaled_sums = vec![vec![0.0f64; NR_LAYER_USAGES]; self.nr_layers];
1316        #[allow(clippy::needless_range_loop)]
1317        for cpu in 0..*NR_CPUS_POSSIBLE {
1318            let scale = cpu_scales[cpu];
1319            let base = cpu * stride;
1320            for layer in 0..self.nr_layers {
1321                for usage in 0..NR_LAYER_USAGES {
1322                    let idx = base + layer * NR_LAYER_USAGES + usage;
1323                    let delta =
1324                        cur_cpu_layer_usages[idx].saturating_sub(self.prev_cpu_layer_usages[idx]);
1325                    if delta > 0 {
1326                        let delta_f = delta as f64;
1327                        raw_sums[layer][usage] += delta_f;
1328                        scaled_sums[layer][usage] += delta_f * scale;
1329                    }
1330                }
1331            }
1332        }
1333        let normalize = |sums: Vec<Vec<f64>>| -> Vec<Vec<f64>> {
1334            sums.into_iter()
1335                .map(|layer_sums| {
1336                    layer_sums
1337                        .into_iter()
1338                        .map(|s| s / 1_000_000_000.0 / elapsed_f64)
1339                        .collect()
1340                })
1341                .collect()
1342        };
1343        let layer_utils: Vec<Vec<f64>> =
1344            metric_decay(normalize(raw_sums), &self.layer_utils, *USAGE_DECAY);
1345        let layer_utils_compensated: Vec<Vec<f64>> = metric_decay(
1346            normalize(scaled_sums),
1347            &self.layer_utils_compensated,
1348            *USAGE_DECAY,
1349        );
1350
1351        let cur_bpf_stats = BpfStats::read(skel, &cpu_ctxs);
1352        let bpf_stats = &cur_bpf_stats - &self.prev_bpf_stats;
1353
1354        // Calculate per-layer DSQ insertion EWMA (10 second window)
1355        const DSQ_INSERT_EWMA_SECS: f64 = 10.0;
1356        let dsq_alpha = elapsed_f64 / DSQ_INSERT_EWMA_SECS.max(elapsed_f64);
1357        let layer_dsq_insert_ewma: Vec<f64> = (0..self.nr_layers)
1358            .map(|layer_id| {
1359                let sel_local = bpf_stats.lstats[layer_id]
1360                    [bpf_intf::layer_stat_id_LSTAT_SEL_LOCAL as usize]
1361                    as f64;
1362                let enq_local = bpf_stats.lstats[layer_id]
1363                    [bpf_intf::layer_stat_id_LSTAT_ENQ_LOCAL as usize]
1364                    as f64;
1365                let enq_dsq = bpf_stats.lstats[layer_id]
1366                    [bpf_intf::layer_stat_id_LSTAT_ENQ_DSQ as usize]
1367                    as f64;
1368                let total_dispatches = sel_local + enq_local + enq_dsq;
1369
1370                let cur_ratio = if total_dispatches > 0.0 {
1371                    enq_dsq / total_dispatches
1372                } else {
1373                    0.0
1374                };
1375
1376                dsq_alpha * cur_ratio + (1.0 - dsq_alpha) * self.layer_dsq_insert_ewma[layer_id]
1377            })
1378            .collect();
1379
1380        let processing_dur = cur_processing_dur
1381            .checked_sub(self.prev_processing_dur)
1382            .unwrap();
1383
1384        *self = Self {
1385            at: now,
1386            elapsed,
1387            topo: self.topo.clone(),
1388            nr_layers: self.nr_layers,
1389            nr_layer_tasks,
1390            layer_nr_node_pinned_tasks,
1391
1392            total_util: layer_utils
1393                .iter()
1394                .map(|x| x.iter().take(LAYER_USAGE_SUM_UPTO + 1).sum::<f64>())
1395                .sum(),
1396            layer_utils,
1397            layer_node_pinned_utils,
1398            prev_layer_node_pinned_usages: cur_layer_node_pinned_usages,
1399            layer_node_utils,
1400            prev_layer_node_usages: cur_layer_node_usages,
1401            layer_node_duty_sums,
1402            prev_layer_node_duty_raw: cur_layer_node_duty_raw,
1403
1404            layer_membws,
1405            prev_layer_membw_agg: cur_layer_membw_agg,
1406            // Was updated during normalization.
1407            prev_pmu_resctrl_membw: (pmu_cur, resctrl_cur),
1408
1409            cpu_busy,
1410            prev_total_cpu: cur_total_cpu,
1411            util_compensation: self.util_compensation,
1412            layer_utils_compensated,
1413            prev_cpu_layer_usages: cur_cpu_layer_usages,
1414            prev_per_cpu_stats: cur_per_cpu_stats,
1415            system_cpu_util_ewma,
1416            layer_dsq_insert_ewma,
1417
1418            bpf_stats,
1419            prev_bpf_stats: cur_bpf_stats,
1420
1421            processing_dur,
1422            prev_processing_dur: cur_processing_dur,
1423
1424            layer_slice_us,
1425            gpu_tasks_affinitized: gpu_task_affinitizer.tasks_affinitized,
1426            gpu_task_affinitization_ms: gpu_task_affinitizer.last_task_affinitization_ms,
1427        };
1428        Ok(())
1429    }
1430}
1431
1432#[derive(Debug)]
1433struct Layer {
1434    name: String,
1435    kind: LayerKind,
1436    growth_algo: LayerGrowthAlgo,
1437    core_order: Vec<Vec<usize>>,
1438
1439    assigned_llcs: Vec<Vec<usize>>,
1440
1441    nr_cpus: usize,
1442    nr_llc_cpus: Vec<usize>,
1443    nr_node_cpus: Vec<usize>,
1444    cpus: Cpumask,
1445    allowed_cpus: Cpumask,
1446
1447    /// Per-node count of CPUs allocated for pinned demand.
1448    nr_pinned_cpus: Vec<usize>,
1449}
1450
1451fn get_kallsyms_addr(sym_name: &str) -> Result<u64> {
1452    fs::read_to_string("/proc/kallsyms")?
1453        .lines()
1454        .find(|line| line.contains(sym_name))
1455        .and_then(|line| line.split_whitespace().next())
1456        .and_then(|addr| u64::from_str_radix(addr, 16).ok())
1457        .ok_or_else(|| anyhow!("Symbol '{}' not found", sym_name))
1458}
1459
1460fn resolve_cpus_pct_range(
1461    cpus_range: &Option<(usize, usize)>,
1462    cpus_range_frac: &Option<(f64, f64)>,
1463    max_cpus: usize,
1464) -> Result<(usize, usize)> {
1465    match (cpus_range, cpus_range_frac) {
1466        (Some(_x), Some(_y)) => {
1467            bail!("cpus_range cannot be used with cpus_pct.");
1468        }
1469        (Some((cpus_range_min, cpus_range_max)), None) => Ok((*cpus_range_min, *cpus_range_max)),
1470        (None, Some((cpus_frac_min, cpus_frac_max))) => {
1471            if *cpus_frac_min < 0_f64
1472                || *cpus_frac_min > 1_f64
1473                || *cpus_frac_max < 0_f64
1474                || *cpus_frac_max > 1_f64
1475            {
1476                bail!("cpus_range_frac values must be between 0.0 and 1.0");
1477            }
1478            let cpus_min_count = ((max_cpus as f64) * cpus_frac_min).round_ties_even() as usize;
1479            let cpus_max_count = ((max_cpus as f64) * cpus_frac_max).round_ties_even() as usize;
1480            Ok((
1481                std::cmp::max(cpus_min_count, 1),
1482                std::cmp::min(std::cmp::max(cpus_max_count, 1), max_cpus),
1483            ))
1484        }
1485        (None, None) => Ok((0, max_cpus)),
1486    }
1487}
1488
1489fn update_peak_util(prev_peak: f64, cur_util: f64, half_life: Duration, elapsed: Duration) -> f64 {
1490    let decay = 0.5f64.powf(elapsed.as_secs_f64() / half_life.as_secs_f64());
1491    (prev_peak * decay).max(cur_util)
1492}
1493
1494fn calc_peak_aware_target_range(
1495    util: f64,
1496    peak_util: f64,
1497    util_range: (f64, f64),
1498) -> (usize, usize) {
1499    let low = (util / util_range.1).ceil() as usize;
1500    let high = ((util.max(peak_util) / util_range.0).floor() as usize).max(low);
1501    (low, high)
1502}
1503
1504impl Layer {
1505    fn new(spec: &LayerSpec, topo: &Topology, core_order: &Vec<Vec<usize>>) -> Result<Self> {
1506        let name = &spec.name;
1507        let kind = spec.kind.clone();
1508        let mut allowed_cpus = Cpumask::new();
1509        match &kind {
1510            LayerKind::Confined {
1511                cpus_range,
1512                cpus_range_frac,
1513                common: LayerCommon { nodes, llcs, .. },
1514                ..
1515            } => {
1516                let cpus_range =
1517                    resolve_cpus_pct_range(cpus_range, cpus_range_frac, topo.all_cpus.len())?;
1518                if cpus_range.0 > cpus_range.1 || cpus_range.1 == 0 {
1519                    bail!("invalid cpus_range {:?}", cpus_range);
1520                }
1521                if nodes.is_empty() && llcs.is_empty() {
1522                    allowed_cpus.set_all();
1523                } else {
1524                    // build up the cpus bitset
1525                    for (node_id, node) in &topo.nodes {
1526                        // first do the matching for nodes
1527                        if nodes.contains(node_id) {
1528                            for &id in node.all_cpus.keys() {
1529                                allowed_cpus.set_cpu(id)?;
1530                            }
1531                        }
1532                        // next match on any LLCs
1533                        for (llc_id, llc) in &node.llcs {
1534                            if llcs.contains(llc_id) {
1535                                for &id in llc.all_cpus.keys() {
1536                                    allowed_cpus.set_cpu(id)?;
1537                                }
1538                            }
1539                        }
1540                    }
1541                }
1542            }
1543            LayerKind::Grouped {
1544                common: LayerCommon { nodes, llcs, .. },
1545                ..
1546            }
1547            | LayerKind::Open {
1548                common: LayerCommon { nodes, llcs, .. },
1549                ..
1550            } => {
1551                if nodes.is_empty() && llcs.is_empty() {
1552                    allowed_cpus.set_all();
1553                } else {
1554                    // build up the cpus bitset
1555                    for (node_id, node) in &topo.nodes {
1556                        // first do the matching for nodes
1557                        if nodes.contains(node_id) {
1558                            for &id in node.all_cpus.keys() {
1559                                allowed_cpus.set_cpu(id)?;
1560                            }
1561                        }
1562                        // next match on any LLCs
1563                        for (llc_id, llc) in &node.llcs {
1564                            if llcs.contains(llc_id) {
1565                                for &id in llc.all_cpus.keys() {
1566                                    allowed_cpus.set_cpu(id)?;
1567                                }
1568                            }
1569                        }
1570                    }
1571                }
1572            }
1573        }
1574
1575        // Util can be above 1.0 for grouped layers if
1576        // util_includes_open_cputime is set.
1577        if let Some(util_range) = kind.util_range() {
1578            if util_range.0 < 0.0 || util_range.1 < 0.0 || util_range.0 >= util_range.1 {
1579                bail!("invalid util_range {:?}", util_range);
1580            }
1581        } else if kind.common().util_peak_half_life_ms != 0 {
1582            bail!("util_peak_half_life_ms requires util_range");
1583        }
1584
1585        let layer_growth_algo = kind.common().growth_algo.clone();
1586
1587        debug!(
1588            "layer: {} algo: {:?} core order: {:?}",
1589            name, &layer_growth_algo, core_order
1590        );
1591
1592        Ok(Self {
1593            name: name.into(),
1594            kind,
1595            growth_algo: layer_growth_algo,
1596            core_order: core_order.clone(),
1597
1598            assigned_llcs: vec![vec![]; topo.nodes.len()],
1599
1600            nr_cpus: 0,
1601            nr_llc_cpus: vec![0; topo.all_llcs.len()],
1602            nr_node_cpus: vec![0; topo.nodes.len()],
1603            cpus: Cpumask::new(),
1604            allowed_cpus,
1605
1606            nr_pinned_cpus: vec![0; topo.nodes.len()],
1607        })
1608    }
1609}
1610#[derive(Debug, Clone)]
1611struct NodeInfo {
1612    node_mask: nix::sched::CpuSet,
1613    _node_id: usize,
1614}
1615
1616#[derive(Debug)]
1617struct GpuTaskAffinitizer {
1618    // This struct tracks information necessary to numa affinitize
1619    // gpu tasks periodically when needed.
1620    gpu_devs_to_node_info: HashMap<u32, NodeInfo>,
1621    gpu_pids_to_devs: HashMap<Pid, u32>,
1622    last_process_time: Option<Instant>,
1623    sys: System,
1624    pid_map: HashMap<Pid, Vec<Pid>>,
1625    poll_interval: Duration,
1626    enable: bool,
1627    tasks_affinitized: u64,
1628    last_task_affinitization_ms: u64,
1629}
1630
1631impl GpuTaskAffinitizer {
1632    pub fn new(poll_interval: u64, enable: bool) -> GpuTaskAffinitizer {
1633        GpuTaskAffinitizer {
1634            gpu_devs_to_node_info: HashMap::new(),
1635            gpu_pids_to_devs: HashMap::new(),
1636            last_process_time: None,
1637            sys: System::default(),
1638            pid_map: HashMap::new(),
1639            poll_interval: Duration::from_secs(poll_interval),
1640            enable,
1641            tasks_affinitized: 0,
1642            last_task_affinitization_ms: 0,
1643        }
1644    }
1645
1646    fn find_one_cpu(&self, affinity: Vec<u64>) -> Result<u32> {
1647        for (chunk, &mask) in affinity.iter().enumerate() {
1648            let mut inner_offset: u64 = 1;
1649            for _ in 0..64 {
1650                if (mask & inner_offset) != 0 {
1651                    return Ok((64 * chunk + u64::trailing_zeros(inner_offset) as usize) as u32);
1652                }
1653                inner_offset <<= 1;
1654            }
1655        }
1656        anyhow::bail!("unable to get CPU from NVML bitmask");
1657    }
1658
1659    fn node_to_cpuset(&self, node: &scx_utils::Node) -> Result<CpuSet> {
1660        let mut cpuset = CpuSet::new();
1661        for cpu_id in node.all_cpus.keys() {
1662            cpuset.set(*cpu_id)?;
1663        }
1664        Ok(cpuset)
1665    }
1666
1667    fn init_dev_node_map(&mut self, topo: Arc<Topology>) -> Result<()> {
1668        let nvml = nvml()?;
1669        let device_count = nvml.device_count()?;
1670
1671        for idx in 0..device_count {
1672            let dev = nvml.device_by_index(idx)?;
1673            // For machines w/ up to 1024 CPUs.
1674            let cpu = dev.cpu_affinity(16)?;
1675            let ideal_cpu = self.find_one_cpu(cpu)?;
1676            if let Some(cpu) = topo.all_cpus.get(&(ideal_cpu as usize)) {
1677                self.gpu_devs_to_node_info.insert(
1678                    idx,
1679                    NodeInfo {
1680                        node_mask: self.node_to_cpuset(
1681                            topo.nodes.get(&cpu.node_id).expect("topo missing node"),
1682                        )?,
1683                        _node_id: cpu.node_id,
1684                    },
1685                );
1686            }
1687        }
1688        Ok(())
1689    }
1690
1691    fn update_gpu_pids(&mut self) -> Result<()> {
1692        let nvml = nvml()?;
1693        for i in 0..nvml.device_count()? {
1694            let device = nvml.device_by_index(i)?;
1695            for proc in device
1696                .running_compute_processes()?
1697                .into_iter()
1698                .chain(device.running_graphics_processes()?)
1699            {
1700                self.gpu_pids_to_devs.insert(Pid::from_u32(proc.pid), i);
1701            }
1702        }
1703        Ok(())
1704    }
1705
1706    fn update_process_info(&mut self) -> Result<()> {
1707        self.sys.refresh_processes_specifics(
1708            ProcessesToUpdate::All,
1709            true,
1710            ProcessRefreshKind::nothing(),
1711        );
1712        self.pid_map.clear();
1713        for (pid, proc_) in self.sys.processes() {
1714            if let Some(ppid) = proc_.parent() {
1715                self.pid_map.entry(ppid).or_default().push(*pid);
1716            }
1717        }
1718        Ok(())
1719    }
1720
1721    fn get_child_pids_and_tids(&self, root_pid: Pid) -> HashSet<Pid> {
1722        let mut work = VecDeque::from([root_pid]);
1723        let mut pids_and_tids: HashSet<Pid> = HashSet::new();
1724
1725        while let Some(pid) = work.pop_front() {
1726            if pids_and_tids.insert(pid) {
1727                if let Some(kids) = self.pid_map.get(&pid) {
1728                    work.extend(kids);
1729                }
1730                if let Some(proc_) = self.sys.process(pid)
1731                    && let Some(tasks) = proc_.tasks()
1732                {
1733                    pids_and_tids.extend(tasks.iter().copied());
1734                }
1735            }
1736        }
1737        pids_and_tids
1738    }
1739
1740    fn affinitize_gpu_pids(&mut self) -> Result<()> {
1741        if !self.enable {
1742            return Ok(());
1743        }
1744        for (pid, dev) in &self.gpu_pids_to_devs {
1745            let node_info = self
1746                .gpu_devs_to_node_info
1747                .get(dev)
1748                .expect("Unable to get gpu pid node mask");
1749            for child in self.get_child_pids_and_tids(*pid) {
1750                match nix::sched::sched_setaffinity(
1751                    nix::unistd::Pid::from_raw(child.as_u32() as i32),
1752                    &node_info.node_mask,
1753                ) {
1754                    Ok(_) => {
1755                        // Increment the global counter for successful affinitization
1756                        self.tasks_affinitized += 1;
1757                    }
1758                    Err(_) => {
1759                        debug!(
1760                            "Error affinitizing gpu pid {} to node {:#?}",
1761                            child.as_u32(),
1762                            node_info
1763                        );
1764                    }
1765                };
1766            }
1767        }
1768        Ok(())
1769    }
1770
1771    pub fn maybe_affinitize(&mut self) {
1772        if !self.enable {
1773            return;
1774        }
1775        let now = Instant::now();
1776
1777        if let Some(last_process_time) = self.last_process_time
1778            && (now - last_process_time) < self.poll_interval
1779        {
1780            return;
1781        }
1782
1783        match self.update_gpu_pids() {
1784            Ok(_) => {}
1785            Err(e) => {
1786                error!("Error updating GPU PIDs: {}", e);
1787            }
1788        };
1789        match self.update_process_info() {
1790            Ok(_) => {}
1791            Err(e) => {
1792                error!("Error updating process info to affinitize GPU PIDs: {}", e);
1793            }
1794        };
1795        match self.affinitize_gpu_pids() {
1796            Ok(_) => {}
1797            Err(e) => {
1798                error!("Error updating GPU PIDs: {}", e);
1799            }
1800        };
1801        self.last_process_time = Some(now);
1802        self.last_task_affinitization_ms = (Instant::now() - now).as_millis() as u64;
1803    }
1804
1805    pub fn init(&mut self, topo: Arc<Topology>) {
1806        if !self.enable || self.last_process_time.is_some() {
1807            return;
1808        }
1809
1810        match self.init_dev_node_map(topo) {
1811            Ok(_) => {}
1812            Err(e) => {
1813                error!("Error initializing gpu node dev map: {}", e);
1814            }
1815        };
1816        self.sys = System::new_all();
1817    }
1818}
1819
1820struct Scheduler<'a> {
1821    skel: BpfSkel<'a>,
1822    struct_ops: Option<libbpf_rs::Link>,
1823    layer_specs: Vec<LayerSpec>,
1824
1825    sched_intv: Duration,
1826    layer_refresh_intv: Duration,
1827
1828    cpu_pool: CpuPool,
1829    layers: Vec<Layer>,
1830    idle_qos_enabled: bool,
1831
1832    proc_reader: fb_procfs::ProcReader,
1833    sched_stats: Stats,
1834    layer_peak_utils: Vec<f64>,
1835
1836    cgroup_regexes: Option<HashMap<u32, Regex>>,
1837
1838    nr_layer_cpus_ranges: Vec<(usize, usize)>,
1839    xnuma_mig_src: Vec<Vec<bool>>,
1840    growth_denied: Vec<Vec<bool>>,
1841    processing_dur: Duration,
1842
1843    topo: Arc<Topology>,
1844    netdevs: BTreeMap<String, NetDev>,
1845    stats_server: StatsServer<StatsReq, StatsRes>,
1846    gpu_task_handler: GpuTaskAffinitizer,
1847}
1848
1849const DUTY_CYCLE_SCALE: f64 = (1u64 << 20) as f64;
1850const XNUMA_RATE_DAMPEN: f64 = 0.5;
1851
1852/// Result of xnuma water-fill computation for a single layer.
1853struct XnumaRates {
1854    /// rates[src][dst]: migration rate in duty-cycle-scaled units.
1855    rates: Vec<Vec<u64>>,
1856}
1857
1858/// Determine per-node migration source state with two-threshold hysteresis.
1859///
1860/// Each (layer, node) independently decides if it's a migration source.
1861/// Open (is_mig_src=true) requires all three:
1862///   1. load/alloc > threshold.1 (significant load)
1863///   2. surplus/alloc > delta.1 (significant imbalance)
1864///   3. growth_denied (allocation can't solve it)
1865///
1866/// Close (is_mig_src=false) when any one:
1867///   1. load/alloc < threshold.0 (load dropped)
1868///   2. surplus/alloc < delta.0 (imbalance resolved)
1869///   3. !growth_denied (growth succeeded)
1870fn xnuma_check_active(
1871    duty_sums: &[f64],
1872    allocs: &[usize],
1873    threshold: (f64, f64),
1874    threshold_delta: (f64, f64),
1875    growth_denied: &[bool],
1876    currently_active: &[bool],
1877) -> Vec<bool> {
1878    let nr_nodes = duty_sums.len();
1879    let total_duty: f64 = duty_sums.iter().sum();
1880    let total_alloc: f64 = allocs.iter().map(|&a| a as f64).sum();
1881    let eq_ratio = if total_alloc > 0.0 {
1882        total_duty / total_alloc
1883    } else {
1884        0.0
1885    };
1886
1887    let (thresh_lo, thresh_hi) = threshold;
1888    let (delta_lo, delta_hi) = threshold_delta;
1889
1890    let mut result = vec![false; nr_nodes];
1891    for nid in 0..nr_nodes {
1892        let alloc = allocs[nid] as f64;
1893        if alloc <= 0.0 {
1894            if duty_sums[nid] > 0.0 && growth_denied[nid] {
1895                result[nid] = true;
1896            }
1897            continue;
1898        }
1899
1900        let load_ratio = duty_sums[nid] / alloc;
1901        let surplus = duty_sums[nid] - eq_ratio * alloc;
1902        let surplus_ratio = surplus / alloc;
1903
1904        let should_activate =
1905            load_ratio > thresh_hi && surplus_ratio > delta_hi && growth_denied[nid];
1906        let should_deactivate =
1907            load_ratio < thresh_lo || surplus_ratio < delta_lo || !growth_denied[nid];
1908
1909        if should_activate {
1910            result[nid] = true;
1911        } else if should_deactivate {
1912            result[nid] = false;
1913        } else {
1914            result[nid] = currently_active[nid];
1915        }
1916    }
1917    result
1918}
1919
1920/// Compute water-fill migration rates for a single layer.
1921///
1922/// Finds the equalization ratio (water line) across all nodes, then
1923/// computes per-(src, dst) migration rates proportional to each source's
1924/// surplus and each destination's share of total deficit.
1925fn xnuma_compute_rates(duty_sums: &[f64], allocs: &[usize]) -> XnumaRates {
1926    let nr_nodes = duty_sums.len();
1927    let total_duty: f64 = duty_sums.iter().sum();
1928    let total_alloc: f64 = allocs.iter().map(|&a| a as f64).sum();
1929
1930    if total_alloc <= 0.0 {
1931        return XnumaRates {
1932            rates: vec![vec![0u64; nr_nodes]; nr_nodes],
1933        };
1934    }
1935
1936    let eq_ratio = total_duty / total_alloc;
1937
1938    let mut surpluses = vec![0.0f64; nr_nodes];
1939    let mut deficits = vec![0.0f64; nr_nodes];
1940    for nid in 0..nr_nodes {
1941        let expected = eq_ratio * allocs[nid] as f64;
1942        let delta = duty_sums[nid] - expected;
1943        if delta > 0.0 {
1944            surpluses[nid] = delta;
1945        } else {
1946            deficits[nid] = -delta;
1947        }
1948    }
1949
1950    let total_deficit: f64 = deficits.iter().sum();
1951
1952    let mut rates = vec![vec![0u64; nr_nodes]; nr_nodes];
1953    for src in 0..nr_nodes {
1954        for dst in 0..nr_nodes {
1955            if src == dst || total_deficit <= 0.0 || surpluses[src] <= 0.0 {
1956                continue;
1957            }
1958            // Dampen: transfer half the surplus per cycle so convergence
1959            // is gradual rather than a single-step overcorrection.
1960            let migration = surpluses[src] * deficits[dst] / total_deficit * XNUMA_RATE_DAMPEN;
1961            rates[src][dst] = (migration * DUTY_CYCLE_SCALE) as u64;
1962        }
1963    }
1964
1965    XnumaRates { rates }
1966}
1967
1968impl<'a> Scheduler<'a> {
1969    fn init_layers(
1970        skel: &mut OpenBpfSkel,
1971        specs: &[LayerSpec],
1972        topo: &Topology,
1973    ) -> Result<HashMap<u32, Regex>> {
1974        skel.maps.rodata_data.as_mut().unwrap().nr_layers = specs.len() as u32;
1975        let mut perf_set = false;
1976
1977        let mut layer_iteration_order = (0..specs.len()).collect::<Vec<_>>();
1978        let mut layer_weights: Vec<usize> = vec![];
1979        let mut cgroup_regex_id = 0;
1980        let mut cgroup_regexes = HashMap::new();
1981
1982        for (spec_i, spec) in specs.iter().enumerate() {
1983            let layer = &mut skel.maps.bss_data.as_mut().unwrap().layers[spec_i];
1984
1985            for (or_i, or) in spec.matches.iter().enumerate() {
1986                for (and_i, and) in or.iter().enumerate() {
1987                    let mt = &mut layer.matches[or_i].matches[and_i];
1988
1989                    // Rules are allowlist-based by default
1990                    mt.exclude.write(false);
1991
1992                    match and {
1993                        LayerMatch::CgroupPrefix(prefix) => {
1994                            mt.kind = bpf_intf::layer_match_kind_MATCH_CGROUP_PREFIX as i32;
1995                            copy_into_cstr(&mut mt.cgroup_prefix, prefix.as_str());
1996                        }
1997                        LayerMatch::CgroupSuffix(suffix) => {
1998                            mt.kind = bpf_intf::layer_match_kind_MATCH_CGROUP_SUFFIX as i32;
1999                            copy_into_cstr(&mut mt.cgroup_suffix, suffix.as_str());
2000                        }
2001                        LayerMatch::CgroupRegex(regex_str) => {
2002                            if cgroup_regex_id >= bpf_intf::consts_MAX_CGROUP_REGEXES {
2003                                bail!(
2004                                    "Too many cgroup regex rules. Maximum allowed: {}",
2005                                    bpf_intf::consts_MAX_CGROUP_REGEXES
2006                                );
2007                            }
2008
2009                            // CgroupRegex matching handled in userspace via cgroup watcher
2010                            mt.kind = bpf_intf::layer_match_kind_MATCH_CGROUP_REGEX as i32;
2011                            mt.cgroup_regex_id = cgroup_regex_id;
2012
2013                            let regex = Regex::new(regex_str).with_context(|| {
2014                                format!("Invalid regex '{}' in layer '{}'", regex_str, spec.name)
2015                            })?;
2016                            cgroup_regexes.insert(cgroup_regex_id, regex);
2017                            cgroup_regex_id += 1;
2018                        }
2019                        LayerMatch::CgroupContains(substr) => {
2020                            mt.kind = bpf_intf::layer_match_kind_MATCH_CGROUP_CONTAINS as i32;
2021                            copy_into_cstr(&mut mt.cgroup_substr, substr.as_str());
2022                        }
2023                        LayerMatch::CommPrefix(prefix) => {
2024                            mt.kind = bpf_intf::layer_match_kind_MATCH_COMM_PREFIX as i32;
2025                            copy_into_cstr(&mut mt.comm_prefix, prefix.as_str());
2026                        }
2027                        LayerMatch::CommPrefixExclude(prefix) => {
2028                            mt.kind = bpf_intf::layer_match_kind_MATCH_COMM_PREFIX as i32;
2029                            mt.exclude.write(true);
2030                            copy_into_cstr(&mut mt.comm_prefix, prefix.as_str());
2031                        }
2032                        LayerMatch::PcommPrefix(prefix) => {
2033                            mt.kind = bpf_intf::layer_match_kind_MATCH_PCOMM_PREFIX as i32;
2034                            copy_into_cstr(&mut mt.pcomm_prefix, prefix.as_str());
2035                        }
2036                        LayerMatch::PcommPrefixExclude(prefix) => {
2037                            mt.kind = bpf_intf::layer_match_kind_MATCH_PCOMM_PREFIX as i32;
2038                            mt.exclude.write(true);
2039                            copy_into_cstr(&mut mt.pcomm_prefix, prefix.as_str());
2040                        }
2041                        LayerMatch::NiceAbove(nice) => {
2042                            mt.kind = bpf_intf::layer_match_kind_MATCH_NICE_ABOVE as i32;
2043                            mt.nice = *nice;
2044                        }
2045                        LayerMatch::NiceBelow(nice) => {
2046                            mt.kind = bpf_intf::layer_match_kind_MATCH_NICE_BELOW as i32;
2047                            mt.nice = *nice;
2048                        }
2049                        LayerMatch::NiceEquals(nice) => {
2050                            mt.kind = bpf_intf::layer_match_kind_MATCH_NICE_EQUALS as i32;
2051                            mt.nice = *nice;
2052                        }
2053                        LayerMatch::UIDEquals(user_id) => {
2054                            mt.kind = bpf_intf::layer_match_kind_MATCH_USER_ID_EQUALS as i32;
2055                            mt.user_id = *user_id;
2056                        }
2057                        LayerMatch::GIDEquals(group_id) => {
2058                            mt.kind = bpf_intf::layer_match_kind_MATCH_GROUP_ID_EQUALS as i32;
2059                            mt.group_id = *group_id;
2060                        }
2061                        LayerMatch::PIDEquals(pid) => {
2062                            mt.kind = bpf_intf::layer_match_kind_MATCH_PID_EQUALS as i32;
2063                            mt.pid = *pid;
2064                        }
2065                        LayerMatch::PPIDEquals(ppid) => {
2066                            mt.kind = bpf_intf::layer_match_kind_MATCH_PPID_EQUALS as i32;
2067                            mt.ppid = *ppid;
2068                        }
2069                        LayerMatch::TGIDEquals(tgid) => {
2070                            mt.kind = bpf_intf::layer_match_kind_MATCH_TGID_EQUALS as i32;
2071                            mt.tgid = *tgid;
2072                        }
2073                        LayerMatch::NSPIDEquals(nsid, pid) => {
2074                            mt.kind = bpf_intf::layer_match_kind_MATCH_NSPID_EQUALS as i32;
2075                            mt.nsid = *nsid;
2076                            mt.pid = *pid;
2077                        }
2078                        LayerMatch::NSEquals(nsid) => {
2079                            mt.kind = bpf_intf::layer_match_kind_MATCH_NS_EQUALS as i32;
2080                            mt.nsid = *nsid as u64;
2081                        }
2082                        LayerMatch::CmdJoin(joincmd) => {
2083                            mt.kind = bpf_intf::layer_match_kind_MATCH_SCXCMD_JOIN as i32;
2084                            copy_into_cstr(&mut mt.comm_prefix, joincmd);
2085                        }
2086                        LayerMatch::IsGroupLeader(polarity) => {
2087                            mt.kind = bpf_intf::layer_match_kind_MATCH_IS_GROUP_LEADER as i32;
2088                            mt.is_group_leader.write(*polarity);
2089                        }
2090                        LayerMatch::IsKthread(polarity) => {
2091                            mt.kind = bpf_intf::layer_match_kind_MATCH_IS_KTHREAD as i32;
2092                            mt.is_kthread.write(*polarity);
2093                        }
2094                        LayerMatch::UsedGpuTid(polarity) => {
2095                            mt.kind = bpf_intf::layer_match_kind_MATCH_USED_GPU_TID as i32;
2096                            mt.used_gpu_tid.write(*polarity);
2097                        }
2098                        LayerMatch::UsedGpuPid(polarity) => {
2099                            mt.kind = bpf_intf::layer_match_kind_MATCH_USED_GPU_PID as i32;
2100                            mt.used_gpu_pid.write(*polarity);
2101                        }
2102                        LayerMatch::AvgRuntime(min, max) => {
2103                            mt.kind = bpf_intf::layer_match_kind_MATCH_AVG_RUNTIME as i32;
2104                            mt.min_avg_runtime_us = *min;
2105                            mt.max_avg_runtime_us = *max;
2106                        }
2107                        LayerMatch::HintEquals(hint) => {
2108                            mt.kind = bpf_intf::layer_match_kind_MATCH_HINT_EQUALS as i32;
2109                            mt.hint = *hint;
2110                        }
2111                        LayerMatch::SystemCpuUtilBelow(threshold) => {
2112                            mt.kind = bpf_intf::layer_match_kind_MATCH_SYSTEM_CPU_UTIL_BELOW as i32;
2113                            mt.system_cpu_util_below = (*threshold * 10000.0) as u64;
2114                        }
2115                        LayerMatch::DsqInsertBelow(threshold) => {
2116                            mt.kind = bpf_intf::layer_match_kind_MATCH_DSQ_INSERT_BELOW as i32;
2117                            mt.dsq_insert_below = (*threshold * 10000.0) as u64;
2118                        }
2119                        LayerMatch::NumaNode(node_id) => {
2120                            if *node_id as usize >= topo.nodes.len() {
2121                                bail!(
2122                                    "Spec {:?} has invalid NUMA node ID {} (available nodes: 0-{})",
2123                                    spec.name,
2124                                    node_id,
2125                                    topo.nodes.len() - 1
2126                                );
2127                            }
2128                            mt.kind = bpf_intf::layer_match_kind_MATCH_NUMA_NODE as i32;
2129                            mt.numa_node_id = *node_id;
2130                        }
2131                    }
2132                }
2133                layer.matches[or_i].nr_match_ands = or.len() as i32;
2134            }
2135
2136            layer.nr_match_ors = spec.matches.len() as u32;
2137            layer.kind = spec.kind.as_bpf_enum();
2138
2139            {
2140                let LayerCommon {
2141                    min_exec_us,
2142                    yield_ignore,
2143                    perf,
2144                    preempt,
2145                    preempt_first,
2146                    exclusive,
2147                    skip_remote_node,
2148                    prev_over_idle_core,
2149                    growth_algo,
2150                    slice_us,
2151                    fifo,
2152                    weight,
2153                    disallow_open_after_us,
2154                    disallow_preempt_after_us,
2155                    xllc_mig_min_us,
2156                    placement,
2157                    member_expire_ms,
2158                    ..
2159                } = spec.kind.common();
2160
2161                layer.slice_ns = *slice_us * 1000;
2162                layer.fifo.write(*fifo);
2163                layer.min_exec_ns = min_exec_us * 1000;
2164                layer.yield_step_ns = if *yield_ignore > 0.999 {
2165                    0
2166                } else if *yield_ignore < 0.001 {
2167                    layer.slice_ns
2168                } else {
2169                    (layer.slice_ns as f64 * (1.0 - *yield_ignore)) as u64
2170                };
2171                let mut layer_name: String = spec.name.clone();
2172                layer_name.truncate(MAX_LAYER_NAME);
2173                copy_into_cstr(&mut layer.name, layer_name.as_str());
2174                layer.preempt.write(*preempt);
2175                layer.preempt_first.write(*preempt_first);
2176                layer.excl.write(*exclusive);
2177                layer.skip_remote_node.write(*skip_remote_node);
2178                layer.prev_over_idle_core.write(*prev_over_idle_core);
2179                layer.growth_algo = growth_algo.as_bpf_enum();
2180                layer.weight = *weight;
2181                layer.member_expire_ms = *member_expire_ms;
2182                layer.disallow_open_after_ns = match disallow_open_after_us.unwrap() {
2183                    v if v == u64::MAX => v,
2184                    v => v * 1000,
2185                };
2186                layer.disallow_preempt_after_ns = match disallow_preempt_after_us.unwrap() {
2187                    v if v == u64::MAX => v,
2188                    v => v * 1000,
2189                };
2190                layer.xllc_mig_min_ns = (xllc_mig_min_us * 1000.0) as u64;
2191                layer_weights.push(layer.weight.try_into().unwrap());
2192                layer.perf = u32::try_from(*perf)?;
2193
2194                let task_place = |place: u32| crate::types::layer_task_place(place);
2195                layer.task_place = match placement {
2196                    LayerPlacement::Standard => {
2197                        task_place(bpf_intf::layer_task_place_PLACEMENT_STD)
2198                    }
2199                    LayerPlacement::Sticky => {
2200                        task_place(bpf_intf::layer_task_place_PLACEMENT_STICK)
2201                    }
2202                    LayerPlacement::Floating => {
2203                        task_place(bpf_intf::layer_task_place_PLACEMENT_FLOAT)
2204                    }
2205                };
2206            }
2207
2208            layer.is_protected.write(match spec.kind {
2209                LayerKind::Open { .. } => false,
2210                LayerKind::Confined { protected, .. } | LayerKind::Grouped { protected, .. } => {
2211                    protected
2212                }
2213            });
2214
2215            layer.idle_confined.write(match spec.kind {
2216                LayerKind::Grouped { idle_confined, .. } => idle_confined,
2217                _ => false,
2218            });
2219
2220            match &spec.cpuset {
2221                Some(mask) => {
2222                    Self::update_cpumask(mask, &mut layer.cpuset);
2223                    layer.has_cpuset.write(true);
2224                }
2225                None => {
2226                    for i in 0..layer.cpuset.len() {
2227                        layer.cpuset[i] = u8::MAX;
2228                    }
2229                    layer.has_cpuset.write(false);
2230                }
2231            };
2232
2233            perf_set |= layer.perf > 0;
2234        }
2235
2236        layer_iteration_order.sort_by(|i, j| layer_weights[*i].cmp(&layer_weights[*j]));
2237        for (idx, layer_idx) in layer_iteration_order.iter().enumerate() {
2238            skel.maps
2239                .rodata_data
2240                .as_mut()
2241                .unwrap()
2242                .layer_iteration_order[idx] = *layer_idx as u32;
2243        }
2244
2245        if perf_set && !compat::ksym_exists("scx_bpf_cpuperf_set")? {
2246            warn!("cpufreq support not available, ignoring perf configurations");
2247        }
2248
2249        Ok(cgroup_regexes)
2250    }
2251
2252    fn init_nodes(skel: &mut OpenBpfSkel, _opts: &Opts, topo: &Topology) {
2253        skel.maps.rodata_data.as_mut().unwrap().nr_nodes = topo.nodes.len() as u32;
2254        skel.maps.rodata_data.as_mut().unwrap().nr_llcs = 0;
2255
2256        for (&node_id, node) in &topo.nodes {
2257            debug!("configuring node {}, LLCs {:?}", node_id, node.llcs.len());
2258            skel.maps.rodata_data.as_mut().unwrap().nr_llcs += node.llcs.len() as u32;
2259            let raw_numa_slice = node.span.as_raw_slice();
2260            let node_cpumask_slice =
2261                &mut skel.maps.rodata_data.as_mut().unwrap().numa_cpumasks[node_id];
2262            let (left, _) = node_cpumask_slice.split_at_mut(raw_numa_slice.len());
2263            left.clone_from_slice(raw_numa_slice);
2264            debug!(
2265                "node {} mask: {:?}",
2266                node_id,
2267                skel.maps.rodata_data.as_ref().unwrap().numa_cpumasks[node_id]
2268            );
2269
2270            for llc in node.llcs.values() {
2271                debug!("configuring llc {:?} for node {:?}", llc.id, node_id);
2272                skel.maps.rodata_data.as_mut().unwrap().llc_numa_id_map[llc.id] = node_id as u32;
2273            }
2274        }
2275
2276        for cpu in topo.all_cpus.values() {
2277            skel.maps.rodata_data.as_mut().unwrap().cpu_llc_id_map[cpu.id] = cpu.llc_id as u32;
2278        }
2279    }
2280
2281    fn init_cpu_prox_map(topo: &Topology, cpu_ctxs: &mut [bpf_intf::cpu_ctx]) {
2282        let radiate = |mut vec: Vec<usize>, center_id: usize| -> Vec<usize> {
2283            vec.sort_by_key(|&id| (center_id as i32 - id as i32).abs());
2284            vec
2285        };
2286        let radiate_cpu =
2287            |mut vec: Vec<usize>, center_cpu: usize, center_core: usize| -> Vec<usize> {
2288                vec.sort_by_key(|&id| {
2289                    (
2290                        (center_core as i32 - topo.all_cpus.get(&id).unwrap().core_id as i32).abs(),
2291                        (center_cpu as i32 - id as i32).abs(),
2292                    )
2293                });
2294                vec
2295            };
2296
2297        for (&cpu_id, cpu) in &topo.all_cpus {
2298            // Collect the spans.
2299            let mut core_span = topo.all_cores[&cpu.core_id].span.clone();
2300            let llc_span = &topo.all_llcs[&cpu.llc_id].span;
2301            let node_span = &topo.nodes[&cpu.node_id].span;
2302            let sys_span = &topo.span;
2303
2304            // Make the spans exclusive.
2305            let sys_span = sys_span.and(&node_span.not());
2306            let node_span = node_span.and(&llc_span.not());
2307            let llc_span = llc_span.and(&core_span.not());
2308            core_span.clear_cpu(cpu_id).unwrap();
2309
2310            // Convert them into arrays.
2311            let mut sys_order: Vec<usize> = sys_span.iter().collect();
2312            let mut node_order: Vec<usize> = node_span.iter().collect();
2313            let mut llc_order: Vec<usize> = llc_span.iter().collect();
2314            let mut core_order: Vec<usize> = core_span.iter().collect();
2315
2316            // Shuffle them so that different CPUs follow different orders.
2317            // Each CPU radiates in both directions based on the cpu id and
2318            // radiates out to the closest cores based on core ids.
2319
2320            sys_order = radiate_cpu(sys_order, cpu_id, cpu.core_id);
2321            node_order = radiate(node_order, cpu.node_id);
2322            llc_order = radiate_cpu(llc_order, cpu_id, cpu.core_id);
2323            core_order = radiate_cpu(core_order, cpu_id, cpu.core_id);
2324
2325            // Concatenate and record the topology boundaries.
2326            let mut order: Vec<usize> = vec![];
2327            let mut idx: usize = 0;
2328
2329            idx += 1;
2330            order.push(cpu_id);
2331
2332            idx += core_order.len();
2333            order.append(&mut core_order);
2334            let core_end = idx;
2335
2336            idx += llc_order.len();
2337            order.append(&mut llc_order);
2338            let llc_end = idx;
2339
2340            idx += node_order.len();
2341            order.append(&mut node_order);
2342            let node_end = idx;
2343
2344            idx += sys_order.len();
2345            order.append(&mut sys_order);
2346            let sys_end = idx;
2347
2348            debug!(
2349                "CPU[{}] proximity map[{}/{}/{}/{}]: {:?}",
2350                cpu_id, core_end, llc_end, node_end, sys_end, &order
2351            );
2352
2353            // Record in cpu_ctx.
2354            let pmap = &mut cpu_ctxs[cpu_id].prox_map;
2355            for (i, &cpu) in order.iter().enumerate() {
2356                pmap.cpus[i] = cpu as u16;
2357            }
2358            pmap.core_end = core_end as u32;
2359            pmap.llc_end = llc_end as u32;
2360            pmap.node_end = node_end as u32;
2361            pmap.sys_end = sys_end as u32;
2362        }
2363    }
2364
2365    fn convert_cpu_ctxs(cpu_ctxs: Vec<bpf_intf::cpu_ctx>) -> Vec<Vec<u8>> {
2366        cpu_ctxs
2367            .iter()
2368            .map(|cpu_ctx| unsafe { plain::as_bytes(cpu_ctx) }.to_vec())
2369            .collect()
2370    }
2371
2372    fn init_cpus(skel: &BpfSkel, layer_specs: &[LayerSpec], topo: &Topology) -> Result<()> {
2373        let key = (0_u32).to_ne_bytes();
2374        let mut cpu_ctxs: Vec<bpf_intf::cpu_ctx> = vec![];
2375        let cpu_ctxs_vec = skel
2376            .maps
2377            .cpu_ctxs
2378            .lookup_percpu(&key, libbpf_rs::MapFlags::ANY)
2379            .context("Failed to lookup cpu_ctx")?
2380            .unwrap();
2381
2382        let op_layers: Vec<u32> = layer_specs
2383            .iter()
2384            .enumerate()
2385            .filter(|(_idx, spec)| match &spec.kind {
2386                LayerKind::Open { .. } => spec.kind.common().preempt,
2387                _ => false,
2388            })
2389            .map(|(idx, _)| idx as u32)
2390            .collect();
2391        let on_layers: Vec<u32> = layer_specs
2392            .iter()
2393            .enumerate()
2394            .filter(|(_idx, spec)| match &spec.kind {
2395                LayerKind::Open { .. } => !spec.kind.common().preempt,
2396                _ => false,
2397            })
2398            .map(|(idx, _)| idx as u32)
2399            .collect();
2400        let gp_layers: Vec<u32> = layer_specs
2401            .iter()
2402            .enumerate()
2403            .filter(|(_idx, spec)| match &spec.kind {
2404                LayerKind::Grouped { .. } => spec.kind.common().preempt,
2405                _ => false,
2406            })
2407            .map(|(idx, _)| idx as u32)
2408            .collect();
2409        let gn_layers: Vec<u32> = layer_specs
2410            .iter()
2411            .enumerate()
2412            .filter(|(_idx, spec)| match &spec.kind {
2413                LayerKind::Grouped { .. } => !spec.kind.common().preempt,
2414                _ => false,
2415            })
2416            .map(|(idx, _)| idx as u32)
2417            .collect();
2418
2419        // FIXME - this incorrectly assumes all possible CPUs are consecutive.
2420        for cpu in 0..*NR_CPUS_POSSIBLE {
2421            cpu_ctxs.push(
2422                *plain::from_bytes(cpu_ctxs_vec[cpu].as_slice())
2423                    .expect("cpu_ctx: short or misaligned buffer"),
2424            );
2425
2426            let topo_cpu = topo.all_cpus.get(&cpu).unwrap();
2427            let is_big = topo_cpu.core_type == CoreType::Big { turbo: true };
2428            cpu_ctxs[cpu].cpu = cpu as i32;
2429            cpu_ctxs[cpu].layer_id = MAX_LAYERS as u32;
2430            cpu_ctxs[cpu].is_big = is_big;
2431
2432            fastrand::seed(cpu as u64);
2433
2434            let mut ogp_order = op_layers.clone();
2435            ogp_order.append(&mut gp_layers.clone());
2436            fastrand::shuffle(&mut ogp_order);
2437
2438            let mut ogn_order = on_layers.clone();
2439            ogn_order.append(&mut gn_layers.clone());
2440            fastrand::shuffle(&mut ogn_order);
2441
2442            let mut op_order = op_layers.clone();
2443            fastrand::shuffle(&mut op_order);
2444
2445            let mut on_order = on_layers.clone();
2446            fastrand::shuffle(&mut on_order);
2447
2448            let mut gp_order = gp_layers.clone();
2449            fastrand::shuffle(&mut gp_order);
2450
2451            let mut gn_order = gn_layers.clone();
2452            fastrand::shuffle(&mut gn_order);
2453
2454            for i in 0..MAX_LAYERS {
2455                cpu_ctxs[cpu].ogp_layer_order[i] =
2456                    ogp_order.get(i).cloned().unwrap_or(MAX_LAYERS as u32);
2457                cpu_ctxs[cpu].ogn_layer_order[i] =
2458                    ogn_order.get(i).cloned().unwrap_or(MAX_LAYERS as u32);
2459
2460                cpu_ctxs[cpu].op_layer_order[i] =
2461                    op_order.get(i).cloned().unwrap_or(MAX_LAYERS as u32);
2462                cpu_ctxs[cpu].on_layer_order[i] =
2463                    on_order.get(i).cloned().unwrap_or(MAX_LAYERS as u32);
2464                cpu_ctxs[cpu].gp_layer_order[i] =
2465                    gp_order.get(i).cloned().unwrap_or(MAX_LAYERS as u32);
2466                cpu_ctxs[cpu].gn_layer_order[i] =
2467                    gn_order.get(i).cloned().unwrap_or(MAX_LAYERS as u32);
2468            }
2469        }
2470
2471        Self::init_cpu_prox_map(topo, &mut cpu_ctxs);
2472
2473        skel.maps
2474            .cpu_ctxs
2475            .update_percpu(
2476                &key,
2477                &Self::convert_cpu_ctxs(cpu_ctxs),
2478                libbpf_rs::MapFlags::ANY,
2479            )
2480            .context("Failed to update cpu_ctx")?;
2481
2482        Ok(())
2483    }
2484
2485    fn init_single_prox_map_per_llc(
2486        skel: &mut BpfSkel,
2487        topo: &Topology,
2488        prox_map_idx: &usize,
2489    ) -> Result<()> {
2490        for (&llc_id, llc) in &topo.all_llcs {
2491            // Collect the orders.
2492            let mut node_order: Vec<usize> =
2493                topo.nodes[&llc.node_id].llcs.keys().cloned().collect();
2494            let mut sys_order: Vec<usize> = topo.all_llcs.keys().cloned().collect();
2495
2496            // Make the orders exclusive.
2497            sys_order.retain(|id| !node_order.contains(id));
2498            node_order.retain(|&id| id != llc_id);
2499
2500            // Shuffle so that different LLCs follow different orders. See
2501            // init_cpu_prox_map().
2502            fastrand::seed((*prox_map_idx as u64) << 32 | llc_id as u64);
2503            fastrand::shuffle(&mut sys_order);
2504            fastrand::shuffle(&mut node_order);
2505
2506            // Concatenate and record the node boundary.
2507            let mut order: Vec<usize> = vec![];
2508            let mut idx: usize = 0;
2509
2510            idx += 1;
2511            order.push(llc_id);
2512
2513            idx += node_order.len();
2514            order.append(&mut node_order);
2515            let node_end = idx;
2516
2517            idx += sys_order.len();
2518            order.append(&mut sys_order);
2519            let sys_end = idx;
2520
2521            debug!(
2522                "LLC[{}] proximity map {}[{}/{}]: {:?}",
2523                llc_id, prox_map_idx, node_end, sys_end, &order
2524            );
2525
2526            // Record in llc_ctx.
2527            //
2528            // XXX - This would be a lot easier if llc_ctx were in the bss.
2529            // See BpfStats::read().
2530            let key = (llc_id as u32).to_ne_bytes();
2531            let v = skel
2532                .maps
2533                .llc_data
2534                .lookup(&key, libbpf_rs::MapFlags::ANY)
2535                .unwrap()
2536                .unwrap();
2537            let mut llcc: bpf_intf::llc_ctx =
2538                *plain::from_bytes(v.as_slice()).expect("llc_ctx: short or misaligned buffer");
2539
2540            let pmap = &mut llcc.prox_maps[*prox_map_idx];
2541            for (i, &llc_id) in order.iter().enumerate() {
2542                pmap.llcs[i] = llc_id as u16;
2543            }
2544            pmap.node_end = node_end as u32;
2545            pmap.sys_end = sys_end as u32;
2546
2547            skel.maps.llc_data.update(
2548                &key,
2549                unsafe { plain::as_bytes(&llcc) },
2550                libbpf_rs::MapFlags::ANY,
2551            )?
2552        }
2553
2554        Ok(())
2555    }
2556
2557    fn init_llc_prox_map(skel: &mut BpfSkel, topo: &Topology) -> Result<()> {
2558        let num_proximity_maps = bpf_intf::consts_NUM_PROXIMITY_MAPS as usize;
2559        for prox_map_idx in 0..num_proximity_maps {
2560            Self::init_single_prox_map_per_llc(skel, topo, &prox_map_idx)?;
2561        }
2562
2563        Ok(())
2564    }
2565
2566    fn init_node_prox_map(skel: &mut BpfSkel, topo: &Topology) -> Result<()> {
2567        for (&node_id, node) in &topo.nodes {
2568            let mut order: Vec<(usize, usize)> = node
2569                .distance
2570                .iter()
2571                .enumerate()
2572                .filter(|&(nid, _)| nid != node_id && topo.nodes.contains_key(&nid))
2573                .map(|(nid, &dist)| (nid, dist))
2574                .collect();
2575            order.sort_by_key(|&(_, dist)| dist);
2576
2577            let key = (node_id as u32).to_ne_bytes();
2578
2579            // The map entry may not exist yet — create a zeroed one.
2580            let v = skel.maps.node_data.lookup(&key, libbpf_rs::MapFlags::ANY);
2581            let mut nodec: bpf_intf::node_ctx = match v {
2582                Ok(Some(v)) => {
2583                    *plain::from_bytes(v.as_slice()).expect("node_ctx: short or misaligned buffer")
2584                }
2585                _ => unsafe { MaybeUninit::zeroed().assume_init() },
2586            };
2587
2588            let pmap = &mut nodec.prox_map;
2589            for (i, &(nid, _)) in order.iter().enumerate() {
2590                pmap.nodes[i] = nid as u16;
2591            }
2592            pmap.sys_end = order.len() as u32;
2593
2594            let order_vec: Vec<_> = order.iter().map(|(n, d)| (*n, *d)).collect();
2595            debug!(
2596                "NODE[{}] prox_map[{}]: {:?}",
2597                node_id, pmap.sys_end, order_vec
2598            );
2599
2600            skel.maps.node_data.update(
2601                &key,
2602                unsafe { plain::as_bytes(&nodec) },
2603                libbpf_rs::MapFlags::ANY,
2604            )?;
2605        }
2606        Ok(())
2607    }
2608
2609    fn init_node_ctx(skel: &mut BpfSkel, topo: &Topology, nr_layers: usize) -> Result<()> {
2610        let all_layers: Vec<u32> = (0..nr_layers as u32).collect();
2611        let node_empty_layers: Vec<Vec<u32>> =
2612            (0..topo.nodes.len()).map(|_| all_layers.clone()).collect();
2613        Self::refresh_node_ctx(skel, topo, &node_empty_layers, true);
2614        Ok(())
2615    }
2616
2617    fn init(
2618        opts: &'a Opts,
2619        layer_specs: &[LayerSpec],
2620        open_object: &'a mut MaybeUninit<OpenObject>,
2621        hint_to_layer_map: &HashMap<u64, HintLayerInfo>,
2622        membw_tracking: bool,
2623    ) -> Result<Self> {
2624        let nr_layers = layer_specs.len();
2625        let mut disable_topology = opts.disable_topology.unwrap_or(false);
2626
2627        let mut topo = if disable_topology {
2628            Topology::with_flattened_llc_node()?
2629        } else if opts.topology.virt_llc.is_some() {
2630            Topology::with_args(&opts.topology)?
2631        } else {
2632            Topology::new()?
2633        };
2634
2635        // Memory-only nodes, such as GPU or CXL memory nodes, never run
2636        // tasks but would count against MAX_NUMA_NODES.
2637        topo.split_cpuless_nodes();
2638        if topo.nodes.len() > bpf_intf::consts_MAX_NUMA_NODES as usize {
2639            bail!(
2640                "{} NUMA nodes exceed the maximum of {}",
2641                topo.nodes.len(),
2642                bpf_intf::consts_MAX_NUMA_NODES
2643            );
2644        }
2645        let topo = Arc::new(topo);
2646
2647        /*
2648         * FIXME: scx_layered incorrectly assumes that node, LLC and CPU IDs
2649         * are consecutive. Verify that they are on this system and bail if
2650         * not. It's lucky that core ID is not used anywhere as core IDs are
2651         * not consecutive on some Ryzen CPUs.
2652         */
2653        if topo.nodes.keys().enumerate().any(|(i, &k)| i != k) {
2654            bail!("Holes in node IDs detected: {:?}", topo.nodes.keys());
2655        }
2656        if topo.all_llcs.keys().enumerate().any(|(i, &k)| i != k) {
2657            bail!("Holes in LLC IDs detected: {:?}", topo.all_llcs.keys());
2658        }
2659        if topo.all_cpus.keys().enumerate().any(|(i, &k)| i != k) {
2660            bail!("Holes in CPU IDs detected: {:?}", topo.all_cpus.keys());
2661        }
2662
2663        let netdevs = if opts.netdev_irq_balance {
2664            let devs = read_netdevs()?;
2665            let total_irqs: usize = devs.values().map(|d| d.irqs.len()).sum();
2666            let breakdown = devs
2667                .iter()
2668                .map(|(iface, d)| format!("{iface}={}", d.irqs.len()))
2669                .collect::<Vec<_>>()
2670                .join(", ");
2671            info!(
2672                "Netdev IRQ balancing enabled: overriding {total_irqs} IRQ{} \
2673                 across {} interface{} [{breakdown}]",
2674                if total_irqs == 1 { "" } else { "s" },
2675                devs.len(),
2676                if devs.len() == 1 { "" } else { "s" },
2677            );
2678            devs
2679        } else {
2680            BTreeMap::new()
2681        };
2682
2683        if !disable_topology {
2684            if topo.nodes.len() == 1 && topo.nodes[&0].llcs.len() == 1 {
2685                disable_topology = true;
2686            };
2687            info!(
2688                "Topology awareness not specified, selecting {} based on hardware",
2689                if disable_topology {
2690                    "disabled"
2691                } else {
2692                    "enabled"
2693                }
2694            );
2695        };
2696
2697        let cpu_pool = CpuPool::new(topo.clone(), opts.allow_partial_core)?;
2698
2699        // If disabling topology awareness clear out any set NUMA/LLC configs and
2700        // it will fallback to using all cores.
2701        let layer_specs: Vec<_> = if disable_topology {
2702            info!("Disabling topology awareness");
2703            layer_specs
2704                .iter()
2705                .cloned()
2706                .map(|mut s| {
2707                    s.kind.common_mut().nodes.clear();
2708                    s.kind.common_mut().llcs.clear();
2709                    s
2710                })
2711                .collect()
2712        } else {
2713            layer_specs.to_vec()
2714        };
2715
2716        // Validate that spec node/LLC references exist in the topology.
2717        for spec in layer_specs.iter() {
2718            let mut seen = BTreeSet::new();
2719            for &node_id in spec.nodes().iter() {
2720                if !topo.nodes.contains_key(&node_id) {
2721                    bail!(
2722                        "layer {:?}: nodes references node {} which does not \
2723                         exist in the topology (available: {:?})",
2724                        spec.name,
2725                        node_id,
2726                        topo.nodes.keys().collect::<Vec<_>>()
2727                    );
2728                }
2729                if !seen.insert(node_id) {
2730                    bail!(
2731                        "layer {:?}: nodes contains duplicate node {}",
2732                        spec.name,
2733                        node_id
2734                    );
2735                }
2736            }
2737
2738            seen.clear();
2739            for &llc_id in spec.llcs().iter() {
2740                if !topo.all_llcs.contains_key(&llc_id) {
2741                    bail!(
2742                        "layer {:?}: llcs references LLC {} which does not \
2743                         exist in the topology (available: {:?})",
2744                        spec.name,
2745                        llc_id,
2746                        topo.all_llcs.keys().collect::<Vec<_>>()
2747                    );
2748                }
2749                if !seen.insert(llc_id) {
2750                    bail!(
2751                        "layer {:?}: llcs contains duplicate LLC {}",
2752                        spec.name,
2753                        llc_id
2754                    );
2755                }
2756            }
2757        }
2758
2759        for spec in layer_specs.iter() {
2760            let has_numa_node_match = spec
2761                .matches
2762                .iter()
2763                .flatten()
2764                .any(|m| matches!(m, LayerMatch::NumaNode(_)));
2765            let has_node_spread_algo = matches!(
2766                spec.kind.common().growth_algo,
2767                LayerGrowthAlgo::NodeSpread
2768                    | LayerGrowthAlgo::NodeSpreadReverse
2769                    | LayerGrowthAlgo::NodeSpreadRandom
2770            );
2771            if has_numa_node_match && has_node_spread_algo {
2772                bail!(
2773                    "layer {:?}: NumaNode matcher cannot be combined with {:?} \
2774                     growth algorithm. NodeSpread* allocates CPUs equally across \
2775                     ALL NUMA nodes, but NumaNode restricts tasks to one node's \
2776                     CPUs — CPUs on other nodes are wasted and utilization \
2777                     will never exceed 1/numa_nodes. Use a non-spread algorithm \
2778                     (e.g. Linear, Topo) instead.",
2779                    spec.name,
2780                    spec.kind.common().growth_algo
2781                );
2782            }
2783        }
2784
2785        // Check kernel features
2786        init_libbpf_logging(None);
2787        let kfuncs_in_syscall = scx_bpf_compat::kfuncs_supported_in_syscall()?;
2788        if !kfuncs_in_syscall {
2789            warn!("Using slow path: kfuncs not supported in syscall programs (a8e03b6bbb2c ∉ ker)");
2790        }
2791
2792        // Open the BPF prog first for verification.
2793        let debug_level = if opts.log_level.contains("trace") {
2794            2
2795        } else if opts.log_level.contains("debug") {
2796            1
2797        } else {
2798            0
2799        };
2800        let mut skel_builder = BpfSkelBuilder::default();
2801        skel_builder.obj_builder.debug(debug_level > 1);
2802
2803        info!(
2804            "Running scx_layered (build ID: {})",
2805            build_id::full_version(env!("CARGO_PKG_VERSION"))
2806        );
2807        let open_opts = opts.libbpf.clone().into_bpf_open_opts();
2808        let mut skel = scx_ops_open!(skel_builder, open_object, layered, open_opts)?;
2809
2810        // No memory BW tracking by default
2811        skel.progs.scx_pmu_switch_tc.set_autoload(membw_tracking);
2812        skel.progs.scx_pmu_tick_tc.set_autoload(membw_tracking);
2813
2814        let mut loaded_kprobes = HashSet::new();
2815
2816        // enable autoloads for conditionally loaded things
2817        // immediately after creating skel (because this is always before loading)
2818        if opts.enable_gpu_support {
2819            // by default, enable open if gpu support is enabled.
2820            // open has been observed to be relatively cheap to kprobe.
2821            if opts.gpu_kprobe_level >= 1 {
2822                compat::cond_kprobe_load("nvidia_open", &skel.progs.kprobe_nvidia_open)?;
2823                loaded_kprobes.insert("nvidia_open");
2824            }
2825            // enable the rest progressively based upon how often they are called
2826            // for observed workloads
2827            if opts.gpu_kprobe_level >= 2 {
2828                compat::cond_kprobe_load("nvidia_mmap", &skel.progs.kprobe_nvidia_mmap)?;
2829                loaded_kprobes.insert("nvidia_mmap");
2830            }
2831            if opts.gpu_kprobe_level >= 3 {
2832                compat::cond_kprobe_load("nvidia_poll", &skel.progs.kprobe_nvidia_poll)?;
2833                loaded_kprobes.insert("nvidia_poll");
2834            }
2835        }
2836
2837        let ext_sched_class_addr = get_kallsyms_addr("ext_sched_class");
2838        let idle_sched_class_addr = get_kallsyms_addr("idle_sched_class");
2839
2840        let event = if membw_tracking {
2841            setup_membw_tracking(&mut skel)?
2842        } else {
2843            0
2844        };
2845
2846        let rodata = skel.maps.rodata_data.as_mut().unwrap();
2847
2848        if let (Ok(ext_addr), Ok(idle_addr)) = (ext_sched_class_addr, idle_sched_class_addr) {
2849            rodata.ext_sched_class_addr = ext_addr;
2850            rodata.idle_sched_class_addr = idle_addr;
2851        } else {
2852            warn!(
2853                "Unable to get sched_class addresses from /proc/kallsyms, disabling skip_preempt."
2854            );
2855        }
2856
2857        rodata.slice_ns = scx_enums.SCX_SLICE_DFL;
2858        rodata.max_exec_ns = 20 * scx_enums.SCX_SLICE_DFL;
2859
2860        // Initialize skel according to @opts.
2861        skel.struct_ops.layered_mut().exit_dump_len = opts.exit_dump_len;
2862
2863        if !opts.disable_queued_wakeup {
2864            match *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP {
2865                0 => info!("Kernel does not support queued wakeup optimization"),
2866                v => skel.struct_ops.layered_mut().flags |= v,
2867            }
2868        }
2869
2870        rodata.percpu_kthread_preempt = !opts.disable_percpu_kthread_preempt;
2871        rodata.percpu_kthread_preempt_all =
2872            !opts.disable_percpu_kthread_preempt && opts.percpu_kthread_preempt_all;
2873        rodata.debug = debug_level as u32;
2874        rodata.slice_ns = opts.slice_us * 1000;
2875        rodata.max_exec_ns = if opts.max_exec_us > 0 {
2876            opts.max_exec_us * 1000
2877        } else {
2878            opts.slice_us * 1000 * 20
2879        };
2880        rodata.nr_cpu_ids = *NR_CPU_IDS as u32;
2881        rodata.nr_possible_cpus = *NR_CPUS_POSSIBLE as u32;
2882        rodata.smt_enabled = topo.smt_enabled;
2883        rodata.has_little_cores = topo.has_little_cores();
2884        rodata.antistall_sec = opts.antistall_sec;
2885        rodata.monitor_disable = opts.monitor_disable;
2886        rodata.lo_fb_wait_ns = opts.lo_fb_wait_us * 1000;
2887        rodata.lo_fb_share_ppk = ((opts.lo_fb_share * 1024.0) as u32).clamp(1, 1024);
2888        rodata.enable_antistall = !opts.disable_antistall;
2889        rodata.enable_match_debug = opts.enable_match_debug;
2890        rodata.enable_gpu_support = opts.enable_gpu_support;
2891        rodata.kfuncs_supported_in_syscall = kfuncs_in_syscall;
2892
2893        for (cpu, sib) in topo.sibling_cpus().iter().enumerate() {
2894            rodata.__sibling_cpu[cpu] = *sib;
2895        }
2896        for cpu in topo.all_cpus.keys() {
2897            rodata.all_cpus[cpu / 8] |= 1 << (cpu % 8);
2898        }
2899
2900        rodata.nr_op_layers = layer_specs
2901            .iter()
2902            .filter(|spec| match &spec.kind {
2903                LayerKind::Open { .. } => spec.kind.common().preempt,
2904                _ => false,
2905            })
2906            .count() as u32;
2907        rodata.nr_on_layers = layer_specs
2908            .iter()
2909            .filter(|spec| match &spec.kind {
2910                LayerKind::Open { .. } => !spec.kind.common().preempt,
2911                _ => false,
2912            })
2913            .count() as u32;
2914        rodata.nr_gp_layers = layer_specs
2915            .iter()
2916            .filter(|spec| match &spec.kind {
2917                LayerKind::Grouped { .. } => spec.kind.common().preempt,
2918                _ => false,
2919            })
2920            .count() as u32;
2921        rodata.nr_gn_layers = layer_specs
2922            .iter()
2923            .filter(|spec| match &spec.kind {
2924                LayerKind::Grouped { .. } => !spec.kind.common().preempt,
2925                _ => false,
2926            })
2927            .count() as u32;
2928        rodata.nr_excl_layers = layer_specs
2929            .iter()
2930            .filter(|spec| spec.kind.common().exclusive)
2931            .count() as u32;
2932
2933        let mut min_open = u64::MAX;
2934        let mut min_preempt = u64::MAX;
2935
2936        for spec in layer_specs.iter() {
2937            if let LayerKind::Open { common, .. } = &spec.kind {
2938                min_open = min_open.min(common.disallow_open_after_us.unwrap());
2939                min_preempt = min_preempt.min(common.disallow_preempt_after_us.unwrap());
2940            }
2941        }
2942
2943        rodata.min_open_layer_disallow_open_after_ns = match min_open {
2944            u64::MAX => *DFL_DISALLOW_OPEN_AFTER_US,
2945            v => v,
2946        };
2947        rodata.min_open_layer_disallow_preempt_after_ns = match min_preempt {
2948            u64::MAX => *DFL_DISALLOW_PREEMPT_AFTER_US,
2949            v => v,
2950        };
2951
2952        // We set the pin path before loading the skeleton. This will ensure
2953        // libbpf creates and pins the map, or reuses the pinned map fd for us,
2954        // so that we can keep reusing the older map already pinned on scheduler
2955        // restarts.
2956        let layered_task_hint_map_path = &opts.task_hint_map;
2957        let hint_map = &mut skel.maps.scx_layered_task_hint_map;
2958        // Only set pin path if a path is provided.
2959        if !layered_task_hint_map_path.is_empty() {
2960            hint_map.set_pin_path(layered_task_hint_map_path).unwrap();
2961            rodata.task_hint_map_enabled = true;
2962        }
2963
2964        if !opts.hi_fb_thread_name.is_empty() {
2965            let bpf_hi_fb_thread_name = &mut rodata.hi_fb_thread_name;
2966            copy_into_cstr(bpf_hi_fb_thread_name, opts.hi_fb_thread_name.as_str());
2967            rodata.enable_hi_fb_thread_name_match = true;
2968        }
2969
2970        let cgroup_regexes = Self::init_layers(&mut skel, &layer_specs, &topo)?;
2971        skel.maps.rodata_data.as_mut().unwrap().nr_cgroup_regexes = cgroup_regexes.len() as u32;
2972        Self::init_nodes(&mut skel, opts, &topo);
2973
2974        let mut skel = scx_ops_load!(skel, layered, uei)?;
2975
2976        // Populate the mapping of hints to layer IDs for faster lookups
2977        if !hint_to_layer_map.is_empty() {
2978            for (k, v) in hint_to_layer_map.iter() {
2979                let key: u32 = *k as u32;
2980
2981                // Create hint_layer_info struct
2982                let mut info_bytes = vec![0u8; std::mem::size_of::<bpf_intf::hint_layer_info>()];
2983                let info_ptr = info_bytes.as_mut_ptr() as *mut bpf_intf::hint_layer_info;
2984                unsafe {
2985                    (*info_ptr).layer_id = v.layer_id as u32;
2986                    (*info_ptr).system_cpu_util_below = match v.system_cpu_util_below {
2987                        Some(threshold) => (threshold * 10000.0) as u64,
2988                        None => u64::MAX, // disabled sentinel
2989                    };
2990                    (*info_ptr).dsq_insert_below = match v.dsq_insert_below {
2991                        Some(threshold) => (threshold * 10000.0) as u64,
2992                        None => u64::MAX, // disabled sentinel
2993                    };
2994                }
2995
2996                skel.maps.hint_to_layer_id_map.update(
2997                    &key.to_ne_bytes(),
2998                    &info_bytes,
2999                    libbpf_rs::MapFlags::ANY,
3000                )?;
3001            }
3002        }
3003
3004        if membw_tracking {
3005            create_perf_fds(&mut skel, event)?;
3006        }
3007
3008        let mut layers = vec![];
3009        let layer_growth_orders =
3010            LayerGrowthAlgo::layer_core_orders(&cpu_pool, &layer_specs, &topo)?;
3011        for (idx, spec) in layer_specs.iter().enumerate() {
3012            let growth_order = layer_growth_orders
3013                .get(&idx)
3014                .with_context(|| "layer has no growth order".to_string())?;
3015            layers.push(Layer::new(spec, &topo, growth_order)?);
3016        }
3017
3018        let mut idle_qos_enabled = layers
3019            .iter()
3020            .any(|layer| layer.kind.common().idle_resume_us.unwrap_or(0) > 0);
3021        if idle_qos_enabled && !cpu_idle_resume_latency_supported() {
3022            warn!("idle_resume_us not supported, ignoring");
3023            idle_qos_enabled = false;
3024        }
3025
3026        Self::init_cpus(&skel, &layer_specs, &topo)?;
3027        Self::init_llc_prox_map(&mut skel, &topo)?;
3028        Self::init_node_prox_map(&mut skel, &topo)?;
3029        Self::init_node_ctx(&mut skel, &topo, nr_layers)?;
3030
3031        // Other stuff.
3032        let proc_reader = fb_procfs::ProcReader::new();
3033
3034        // Handle setup if layered is running in a pid namespace.
3035        let input = ProgramInput {
3036            ..Default::default()
3037        };
3038        let prog = &mut skel.progs.initialize_pid_namespace;
3039
3040        let _ = prog.test_run(input);
3041
3042        // XXX If we try to refresh the cpumasks here before attaching, we
3043        // sometimes (non-deterministically) don't see the updated values in
3044        // BPF. It would be better to update the cpumasks here before we
3045        // attach, but the value will quickly converge anyways so it's not a
3046        // huge problem in the interim until we figure it out.
3047
3048        // Allow all tasks to open and write to BPF task hint map, now that
3049        // we should have it pinned at the desired location.
3050        if !layered_task_hint_map_path.is_empty() {
3051            let path = CString::new(layered_task_hint_map_path.as_bytes()).unwrap();
3052            let mode: libc::mode_t = 0o666;
3053            unsafe {
3054                if libc::chmod(path.as_ptr(), mode) != 0 {
3055                    trace!("'chmod' to 666 of task hint map failed, continuing...");
3056                }
3057            }
3058        }
3059
3060        // Attach.
3061        let struct_ops = scx_ops_attach!(skel, layered)?;
3062
3063        // Turn on installed kprobes
3064        if opts.enable_gpu_support {
3065            if loaded_kprobes.contains("nvidia_open") {
3066                compat::cond_kprobe_attach("nvidia_open", &skel.progs.kprobe_nvidia_open)?;
3067            }
3068            if loaded_kprobes.contains("nvidia_mmap") {
3069                compat::cond_kprobe_attach("nvidia_mmap", &skel.progs.kprobe_nvidia_mmap)?;
3070            }
3071            if loaded_kprobes.contains("nvidia_poll") {
3072                compat::cond_kprobe_attach("nvidia_poll", &skel.progs.kprobe_nvidia_poll)?;
3073            }
3074        }
3075
3076        let stats_server = StatsServer::new(stats::server_data()).launch()?;
3077        let mut gpu_task_handler =
3078            GpuTaskAffinitizer::new(opts.gpu_affinitize_secs, opts.enable_gpu_affinitize);
3079        gpu_task_handler.init(topo.clone());
3080
3081        let sched = Self {
3082            struct_ops: Some(struct_ops),
3083            layer_specs,
3084
3085            sched_intv: Duration::from_secs_f64(opts.interval),
3086            layer_refresh_intv: Duration::from_millis(opts.layer_refresh_ms_avgruntime),
3087
3088            cpu_pool,
3089            layers,
3090            idle_qos_enabled,
3091
3092            sched_stats: Stats::new(
3093                &mut skel,
3094                &proc_reader,
3095                topo.clone(),
3096                &gpu_task_handler,
3097                opts.util_compensation,
3098            )?,
3099            layer_peak_utils: vec![0.0; nr_layers],
3100
3101            cgroup_regexes: Some(cgroup_regexes),
3102            nr_layer_cpus_ranges: vec![(0, 0); nr_layers],
3103            xnuma_mig_src: vec![vec![false; topo.nodes.len()]; nr_layers],
3104            growth_denied: vec![vec![false; topo.nodes.len()]; nr_layers],
3105            processing_dur: Default::default(),
3106
3107            proc_reader,
3108            skel,
3109
3110            topo,
3111            netdevs,
3112            stats_server,
3113            gpu_task_handler,
3114        };
3115
3116        info!("Layered Scheduler Attached. Run `scx_layered --monitor` for metrics.");
3117
3118        Ok(sched)
3119    }
3120
3121    fn update_cpumask(mask: &Cpumask, bpfmask: &mut [u8]) {
3122        for cpu in 0..mask.len() {
3123            if mask.test_cpu(cpu) {
3124                bpfmask[cpu / 8] |= 1 << (cpu % 8);
3125            } else {
3126                bpfmask[cpu / 8] &= !(1 << (cpu % 8));
3127            }
3128        }
3129    }
3130
3131    fn update_bpf_layer_cpumask(layer: &Layer, bpf_layer: &mut types::layer) {
3132        trace!("[{}] Updating BPF CPUs: {}", layer.name, &layer.cpus);
3133        Self::update_cpumask(&layer.cpus, &mut bpf_layer.cpus);
3134
3135        bpf_layer.nr_cpus = layer.nr_cpus as u32;
3136        for (llc_id, &nr_llc_cpus) in layer.nr_llc_cpus.iter().enumerate() {
3137            bpf_layer.nr_llc_cpus[llc_id] = nr_llc_cpus as u32;
3138        }
3139        for (node_id, &nr_node_cpus) in layer.nr_node_cpus.iter().enumerate() {
3140            bpf_layer.node[node_id].nr_cpus = nr_node_cpus as u32;
3141        }
3142
3143        bpf_layer.refresh_cpus = 1;
3144    }
3145
3146    fn update_netdev_cpumasks(&mut self) -> Result<()> {
3147        let available_cpus = self.cpu_pool.available_cpus();
3148        if available_cpus.is_empty() {
3149            return Ok(());
3150        }
3151
3152        for (iface, netdev) in self.netdevs.iter_mut() {
3153            // Without a known NUMA node, consider all the CPUs local.
3154            let node_cpus = match netdev.node() {
3155                Some(id) => self
3156                    .topo
3157                    .nodes
3158                    .values()
3159                    .find(|n| n.id == id)
3160                    .ok_or_else(|| anyhow!("Failed to get netdev node"))?
3161                    .span
3162                    .clone(),
3163                None => self.topo.span.clone(),
3164            };
3165            for (irq, irqmask) in netdev.irqs.iter_mut() {
3166                irqmask.clear_all();
3167                for cpu in available_cpus.iter() {
3168                    if !node_cpus.test_cpu(cpu) {
3169                        continue;
3170                    }
3171                    let _ = irqmask.set_cpu(cpu);
3172                }
3173                // If no CPUs are available in the node then spread the load across the node
3174                if irqmask.weight() == 0 {
3175                    for cpu in node_cpus.iter() {
3176                        let _ = irqmask.set_cpu(cpu);
3177                    }
3178                }
3179                trace!("{} updating irq {} cpumask {:?}", iface, irq, irqmask);
3180            }
3181            netdev.apply_cpumasks()?;
3182            debug!(
3183                "{iface}: applied affinity override to {} IRQ{}",
3184                netdev.irqs.len(),
3185                if netdev.irqs.len() == 1 { "" } else { "s" },
3186            );
3187        }
3188
3189        Ok(())
3190    }
3191
3192    fn clamp_target_by_membw(
3193        &self,
3194        layer: &Layer,
3195        membw_limit: f64,
3196        membw: f64,
3197        curtarget: u64,
3198    ) -> usize {
3199        let ncpu: u64 = layer.cpus.weight() as u64;
3200        let membw = (membw * 1024_f64.powf(3.0)).round() as u64;
3201        let membw_limit = (membw_limit * 1024_f64.powf(3.0)).round() as u64;
3202        let last_membw_percpu = membw.checked_div(ncpu).unwrap_or(0);
3203
3204        // Either there is no memory bandwidth limit set, or the counters
3205        // are not fully initialized yet. Just return the current target.
3206        if membw_limit == 0 || last_membw_percpu == 0 {
3207            return curtarget as usize;
3208        }
3209
3210        (membw_limit / last_membw_percpu) as usize
3211    }
3212
3213    /// Decompose per-layer CPU targets into per-node pinned demand and
3214    /// unpinned demand for unified_alloc(). Uses layer_node_pinned_utils
3215    /// to split each layer's target. All outputs are in alloc units.
3216    fn calc_raw_demands(&self, targets: &[(usize, usize)]) -> Vec<LayerDemand> {
3217        let au = self.cpu_pool.alloc_unit();
3218        let pinned_utils = &self.sched_stats.layer_node_pinned_utils;
3219        let nr_nodes = self.topo.nodes.len();
3220
3221        targets
3222            .iter()
3223            .enumerate()
3224            .map(|(idx, &(target, _min))| {
3225                let layer = &self.layers[idx];
3226                let weight = layer.kind.common().weight as usize;
3227
3228                // Open layers don't participate in allocation.
3229                if matches!(layer.kind, LayerKind::Open { .. }) {
3230                    return LayerDemand {
3231                        raw_pinned: vec![0; nr_nodes],
3232                        raw_unpinned: 0,
3233                        weight,
3234                        spread: false,
3235                    };
3236                }
3237
3238                // Only NodeSpread* keeps strict equal-per-node placement
3239                // via the spread:bool path.  RoundRobin migrated to a
3240                // single-tier node_groups (balanced via cap-weighted
3241                // water_fill in place_unpinned, no bottleneck cap).
3242                let spread = matches!(
3243                    layer.growth_algo,
3244                    LayerGrowthAlgo::NodeSpread
3245                        | LayerGrowthAlgo::NodeSpreadReverse
3246                        | LayerGrowthAlgo::NodeSpreadRandom
3247                );
3248
3249                let util_high = match &layer.kind {
3250                    LayerKind::Confined { util_range, .. }
3251                    | LayerKind::Grouped { util_range, .. } => util_range.1,
3252                    _ => 1.0,
3253                };
3254
3255                // Convert per-node pinned utilization to CPU demand.
3256                let mut raw_pinned = vec![0usize; nr_nodes];
3257                for n in 0..nr_nodes {
3258                    let pu = pinned_utils[idx][n];
3259                    if pu < 0.01 {
3260                        continue;
3261                    }
3262                    // Check this layer has allowed_cpus on this node.
3263                    let node_span = &self.topo.nodes[&n].span;
3264                    if layer.allowed_cpus.and(node_span).is_empty() {
3265                        continue;
3266                    }
3267                    let cpus = (pu / util_high).ceil() as usize;
3268                    // Round up to alloc units.
3269                    let units = cpus.div_ceil(au);
3270                    raw_pinned[n] = units;
3271                }
3272
3273                // Unpinned = remainder of the target.
3274                let target_units = target.div_ceil(au);
3275                let pinned_units: usize = raw_pinned.iter().sum();
3276                let raw_unpinned = target_units.saturating_sub(pinned_units);
3277
3278                LayerDemand {
3279                    raw_pinned,
3280                    raw_unpinned,
3281                    weight,
3282                    spread,
3283                }
3284            })
3285            .collect()
3286    }
3287
3288    /// Calculate how many CPUs each layer would like to have if there were
3289    /// no competition. When util_compensation is enabled, compensated
3290    /// utilization (scaled for irq/softirq/stolen overhead) is used
3291    /// instead of raw utilization. The CPU range is determined by
3292    /// applying the inverse of util_range and capping by cpus_range.
3293    /// If the current allocation is within the acceptable range, no
3294    /// change is made. Returns (target, min) pair for each layer.
3295    fn calc_target_nr_cpus(&mut self) -> Vec<(usize, usize)> {
3296        let nr_cpus = self.cpu_pool.topo.all_cpus.len();
3297        let membws = &self.sched_stats.layer_membws;
3298
3299        let mut records: Vec<(u64, u64, u64, u64, usize, usize, usize)> = vec![];
3300        let mut targets: Vec<(usize, usize)> = vec![];
3301
3302        for (idx, layer) in self.layers.iter().enumerate() {
3303            targets.push(match &layer.kind {
3304                LayerKind::Confined {
3305                    util_range,
3306                    cpus_range,
3307                    cpus_range_frac,
3308                    membw_gb,
3309                    ..
3310                }
3311                | LayerKind::Grouped {
3312                    util_range,
3313                    cpus_range,
3314                    cpus_range_frac,
3315                    membw_gb,
3316                    ..
3317                } => {
3318                    let cpus_range =
3319                        resolve_cpus_pct_range(cpus_range, cpus_range_frac, nr_cpus).unwrap();
3320
3321                    // A grouped layer can choose to include open cputime
3322                    // for sizing. Also, as an empty layer can only get CPU
3323                    // time through fallback (counted as owned) or open
3324                    // execution, add open cputime for empty layers.
3325                    let (owned, open) = {
3326                        let utils = if self.sched_stats.util_compensation {
3327                            &self.sched_stats.layer_utils_compensated[idx]
3328                        } else {
3329                            &self.sched_stats.layer_utils[idx]
3330                        };
3331                        (utils[LAYER_USAGE_OWNED], utils[LAYER_USAGE_OPEN])
3332                    };
3333
3334                    let membw_owned = membws[idx][LAYER_USAGE_OWNED];
3335                    let membw_open = membws[idx][LAYER_USAGE_OPEN];
3336
3337                    let mut util = owned;
3338                    let mut membw = membw_owned;
3339                    if layer.kind.util_includes_open_cputime() || layer.nr_cpus == 0 {
3340                        util += open;
3341                        membw += membw_open;
3342                    }
3343
3344                    let util = if util < 0.01 { 0.0 } else { util };
3345
3346                    let peak_util = if layer.kind.common().util_peak_half_life_ms == 0 {
3347                        util
3348                    } else {
3349                        update_peak_util(
3350                            self.layer_peak_utils[idx],
3351                            util,
3352                            Duration::from_millis(layer.kind.common().util_peak_half_life_ms),
3353                            self.sched_stats.elapsed,
3354                        )
3355                    };
3356                    self.layer_peak_utils[idx] = peak_util;
3357                    let (low, high) = calc_peak_aware_target_range(util, peak_util, *util_range);
3358
3359                    let membw_limit = match membw_gb {
3360                        Some(membw_limit) => *membw_limit,
3361                        None => 0.0,
3362                    };
3363
3364                    trace!(
3365                        "layer {0} (membw, membw_limit): ({membw} gi_b, {membw_limit} gi_b)",
3366                        layer.name
3367                    );
3368
3369                    let target = layer.cpus.weight().clamp(low, high);
3370
3371                    records.push((
3372                        (owned * 100.0) as u64,
3373                        (open * 100.0) as u64,
3374                        (util * 100.0) as u64,
3375                        (peak_util * 100.0) as u64,
3376                        low,
3377                        high,
3378                        target,
3379                    ));
3380
3381                    let target = target.clamp(cpus_range.0, cpus_range.1);
3382                    let membw_target =
3383                        self.clamp_target_by_membw(layer, membw_limit, membw, target as u64);
3384
3385                    trace!("CPU target pre- and post-membw adjustment: {target} -> {membw_target}");
3386
3387                    // If there's no way to drop our memory usage down enough,
3388                    // pin the target CPUs to low and drop a warning.
3389                    if membw_target < cpus_range.0 {
3390                        warn!("cannot satisfy memory bw limit for layer {}", layer.name);
3391                        warn!("membw_target {membw_target} low {}", cpus_range.0);
3392                    };
3393
3394                    // Memory bandwidth target cannot override imposed limits or bump
3395                    // the target above what CPU usage-based throttling requires.
3396                    let target = membw_target.clamp(cpus_range.0, target);
3397
3398                    (target, cpus_range.0)
3399                }
3400                LayerKind::Open { .. } => (0, 0),
3401            });
3402        }
3403
3404        trace!(
3405            "(owned, open, util, peak, low, high, target): {:?}",
3406            &records
3407        );
3408        targets
3409    }
3410
3411    // Figure out a tuple (LLCs, extra_cpus) in terms of the target CPUs
3412    // computed by weighted_target_nr_cpus. Returns the number of full LLCs
3413    // occupied by a layer, and any extra cores needed beyond those LLCs.
3414    fn compute_target_llcs(target: usize, node_llcs: &BTreeMap<usize, Arc<Llc>>) -> (usize, usize) {
3415        let mut remaining = target;
3416        let mut full = 0;
3417        for llc in node_llcs.values() {
3418            let cpus_in_llc = llc.span.weight();
3419            if remaining >= cpus_in_llc {
3420                full += 1;
3421                remaining -= cpus_in_llc;
3422            } else {
3423                let mut extra = 0;
3424                for core in llc.cores.values() {
3425                    if remaining == 0 {
3426                        break;
3427                    }
3428                    extra += 1;
3429                    remaining = remaining.saturating_sub(core.cpus.len());
3430                }
3431                return (full, extra);
3432            }
3433        }
3434        (full, 0)
3435    }
3436
3437    // Recalculate the core order for layers using StickyDynamic growth
3438    // algorithm. Uses per-node targets from unified_alloc() to decide how
3439    // many LLCs each layer gets on each node, then builds core_order and
3440    // applies CPU changes.
3441    fn recompute_layer_core_order(
3442        &mut self,
3443        layer_targets: &[(usize, usize)],
3444        layer_allocs: &[LayerAlloc],
3445        au: usize,
3446    ) -> Result<bool> {
3447        let nr_nodes = self.topo.nodes.len();
3448
3449        // Phase 1 — Free per-node: return excess LLCs to cpu_pool.
3450        debug!(
3451            " free: before pass: free_llcs={:?}",
3452            self.cpu_pool.free_llcs
3453        );
3454        for &(idx, _) in layer_targets.iter().rev() {
3455            let layer = &mut self.layers[idx];
3456
3457            if layer.growth_algo != LayerGrowthAlgo::StickyDynamic {
3458                continue;
3459            }
3460
3461            let alloc = &layer_allocs[idx];
3462
3463            for n in 0..nr_nodes {
3464                let assigned_on_n = layer.assigned_llcs[n].len();
3465                let node = self.topo.nodes.get(&n).unwrap();
3466                let target_full_n =
3467                    Self::compute_target_llcs(alloc.node_target(n) * au, &node.llcs).0;
3468                let mut to_free = assigned_on_n.saturating_sub(target_full_n);
3469
3470                debug!(
3471                    " free: layer={} node={} assigned={} target_full={} to_free={}",
3472                    layer.name, n, assigned_on_n, target_full_n, to_free,
3473                );
3474
3475                while to_free > 0 {
3476                    if let Some(llc) = layer.assigned_llcs[n].pop() {
3477                        self.cpu_pool.return_llc(llc);
3478                        to_free -= 1;
3479                        debug!(" layer={} freed_llc={} from node={}", layer.name, llc, n);
3480                    } else {
3481                        break;
3482                    }
3483                }
3484            }
3485        }
3486        debug!(" free: after pass: free_llcs={:?}", self.cpu_pool.free_llcs);
3487
3488        // Phase 2 — Acquire per-node: claim LLCs from cpu_pool.
3489        for &(idx, _) in layer_targets.iter().rev() {
3490            let layer = &mut self.layers[idx];
3491
3492            if layer.growth_algo != LayerGrowthAlgo::StickyDynamic {
3493                continue;
3494            }
3495
3496            let alloc = &layer_allocs[idx];
3497
3498            for n in 0..nr_nodes {
3499                let cur_on_n = layer.assigned_llcs[n].len();
3500                let node = self.topo.nodes.get(&n).unwrap();
3501                let target_full_n =
3502                    Self::compute_target_llcs(alloc.node_target(n) * au, &node.llcs).0;
3503                let mut to_alloc = target_full_n.saturating_sub(cur_on_n);
3504
3505                debug!(
3506                    " alloc: layer={} node={} cur={} target_full={} to_alloc={} free={}",
3507                    layer.name,
3508                    n,
3509                    cur_on_n,
3510                    target_full_n,
3511                    to_alloc,
3512                    self.cpu_pool.free_llcs.get(&n).map_or(0, |v| v.len()),
3513                );
3514
3515                while to_alloc > 0 {
3516                    if let Some(llc) = self.cpu_pool.take_llc_from_node(n) {
3517                        layer.assigned_llcs[n].push(llc);
3518                        to_alloc -= 1;
3519                        debug!(" layer={} alloc_llc={} on node={}", layer.name, llc, n);
3520                    } else {
3521                        break;
3522                    }
3523                }
3524            }
3525
3526            debug!(
3527                " alloc: layer={} assigned_llcs={:?}",
3528                layer.name, layer.assigned_llcs
3529            );
3530        }
3531
3532        // Phase 3 — Spillover per-node: consume extra cores from free LLCs.
3533        for &(idx, _) in layer_targets.iter() {
3534            let layer = &mut self.layers[idx];
3535
3536            if layer.growth_algo != LayerGrowthAlgo::StickyDynamic {
3537                continue;
3538            }
3539
3540            layer.core_order = vec![Vec::new(); nr_nodes];
3541            let alloc = &layer_allocs[idx];
3542
3543            for n in 0..nr_nodes {
3544                let node = self.topo.nodes.get(&n).unwrap();
3545                let mut extra = Self::compute_target_llcs(alloc.node_target(n) * au, &node.llcs).1;
3546
3547                if let Some(node_llcs) = self.cpu_pool.free_llcs.get_mut(&n) {
3548                    for entry in node_llcs.iter_mut() {
3549                        if extra == 0 {
3550                            break;
3551                        }
3552                        let llc_id = entry.0;
3553                        let llc = self.topo.all_llcs.get(&llc_id).unwrap();
3554                        let avail = llc.cores.len() - entry.1;
3555                        let mut used = extra.min(avail);
3556                        let cores_to_add = used;
3557
3558                        let shift = entry.1;
3559                        entry.1 += used;
3560
3561                        for core in llc.cores.iter().skip(shift) {
3562                            if used == 0 {
3563                                break;
3564                            }
3565                            layer.core_order[n].push(core.1.id);
3566                            used -= 1;
3567                        }
3568
3569                        extra -= cores_to_add;
3570                    }
3571                }
3572            }
3573
3574            for node_cores in &mut layer.core_order {
3575                node_cores.reverse();
3576            }
3577        }
3578
3579        // Reset consumed entries in free LLCs.
3580        for node_llcs in self.cpu_pool.free_llcs.values_mut() {
3581            for entry in node_llcs.iter_mut() {
3582                entry.1 = 0;
3583            }
3584        }
3585
3586        // Phase 4 — Build core_order: append cores from assigned LLCs.
3587        for &(idx, _) in layer_targets.iter() {
3588            let layer = &mut self.layers[idx];
3589
3590            if layer.growth_algo != LayerGrowthAlgo::StickyDynamic {
3591                continue;
3592            }
3593
3594            let all_assigned: HashSet<usize> =
3595                layer.assigned_llcs.iter().flatten().copied().collect();
3596
3597            for core in self.topo.all_cores.iter() {
3598                let llc_id = core.1.llc_id;
3599                if all_assigned.contains(&llc_id) {
3600                    let nid = core.1.node_id;
3601                    layer.core_order[nid].push(core.1.id);
3602                }
3603            }
3604            for node_cores in &mut layer.core_order {
3605                node_cores.reverse();
3606            }
3607
3608            debug!(
3609                " alloc: layer={} core_order={:?}",
3610                layer.name, layer.core_order
3611            );
3612        }
3613
3614        // Phase 5 — Apply CPU changes for StickyDynamic layers.
3615        // Two phases: first free per-node, then allocate per-node.
3616        let mut updated = false;
3617
3618        // Free excess CPUs per-node.
3619        for &(idx, _) in layer_targets.iter() {
3620            let layer = &mut self.layers[idx];
3621
3622            if layer.growth_algo != LayerGrowthAlgo::StickyDynamic {
3623                continue;
3624            }
3625
3626            for n in 0..nr_nodes {
3627                let mut node_target = Cpumask::new();
3628                for &core_id in &layer.core_order[n] {
3629                    if let Some(core) = self.topo.all_cores.get(&core_id) {
3630                        node_target |= &core.span;
3631                    }
3632                }
3633                node_target &= &layer.allowed_cpus;
3634
3635                let node_span = &self.topo.nodes[&n].span;
3636                let node_cur = layer.cpus.and(node_span);
3637                let cpus_to_free = node_cur.and(&node_target.not());
3638
3639                if cpus_to_free.weight() > 0 {
3640                    debug!(
3641                        " apply: layer={} freeing CPUs on node {}: {}",
3642                        layer.name, n, cpus_to_free
3643                    );
3644                    layer.cpus &= &cpus_to_free.not();
3645                    layer.nr_cpus -= cpus_to_free.weight();
3646                    for cpu in cpus_to_free.iter() {
3647                        layer.nr_llc_cpus[self.cpu_pool.topo.all_cpus[&cpu].llc_id] -= 1;
3648                        layer.nr_node_cpus[n] -= 1;
3649                    }
3650                    self.cpu_pool.free(&cpus_to_free)?;
3651                    updated = true;
3652                }
3653            }
3654        }
3655
3656        // Allocate needed CPUs per-node.
3657        for &(idx, _) in layer_targets.iter() {
3658            let layer = &mut self.layers[idx];
3659
3660            if layer.growth_algo != LayerGrowthAlgo::StickyDynamic {
3661                continue;
3662            }
3663
3664            for n in 0..nr_nodes {
3665                let mut node_target = Cpumask::new();
3666                for &core_id in &layer.core_order[n] {
3667                    if let Some(core) = self.topo.all_cores.get(&core_id) {
3668                        node_target |= &core.span;
3669                    }
3670                }
3671                node_target &= &layer.allowed_cpus;
3672
3673                let available_cpus = self.cpu_pool.available_cpus();
3674                let desired_to_alloc = node_target.and(&layer.cpus.clone().not());
3675                let cpus_to_alloc = desired_to_alloc.clone().and(&available_cpus);
3676
3677                if desired_to_alloc.weight() > cpus_to_alloc.weight() {
3678                    debug!(
3679                        " apply: layer={} node {} wanted to alloc {} CPUs but only {} available",
3680                        layer.name,
3681                        n,
3682                        desired_to_alloc.weight(),
3683                        cpus_to_alloc.weight()
3684                    );
3685                }
3686
3687                if cpus_to_alloc.weight() > 0 {
3688                    debug!(
3689                        " apply: layer={} allocating CPUs on node {}: {}",
3690                        layer.name, n, cpus_to_alloc
3691                    );
3692                    layer.cpus |= &cpus_to_alloc;
3693                    layer.nr_cpus += cpus_to_alloc.weight();
3694                    for cpu in cpus_to_alloc.iter() {
3695                        layer.nr_llc_cpus[self.cpu_pool.topo.all_cpus[&cpu].llc_id] += 1;
3696                        layer.nr_node_cpus[n] += 1;
3697                    }
3698                    self.cpu_pool.mark_allocated(&cpus_to_alloc)?;
3699                    updated = true;
3700                }
3701            }
3702
3703            debug!(
3704                " apply: layer={} final cpus.weight()={} nr_cpus={}",
3705                layer.name,
3706                layer.cpus.weight(),
3707                layer.nr_cpus
3708            );
3709        }
3710
3711        Ok(updated)
3712    }
3713
3714    fn refresh_node_ctx(
3715        skel: &mut BpfSkel,
3716        topo: &Topology,
3717        node_empty_layers: &[Vec<u32>],
3718        init: bool,
3719    ) {
3720        for &nid in topo.nodes.keys() {
3721            let mut arg: bpf_intf::refresh_node_ctx_arg =
3722                unsafe { MaybeUninit::zeroed().assume_init() };
3723            arg.node_id = nid as u32;
3724            arg.init = init as u32;
3725
3726            let empty = &node_empty_layers[nid];
3727            arg.nr_empty_layer_ids = empty.len() as u32;
3728            for (i, &lid) in empty.iter().enumerate() {
3729                arg.empty_layer_ids[i] = lid;
3730            }
3731            for i in empty.len()..MAX_LAYERS {
3732                arg.empty_layer_ids[i] = MAX_LAYERS as u32;
3733            }
3734
3735            if init {
3736                // Per-node LLC list — static topology, only set during init.
3737                let node = &topo.nodes[&nid];
3738                let llcs: Vec<u32> = node.llcs.keys().map(|&id| id as u32).collect();
3739                arg.nr_llcs = llcs.len() as u32;
3740                for (i, &llc_id) in llcs.iter().enumerate() {
3741                    arg.llcs[i] = llc_id;
3742                }
3743            }
3744
3745            let input = ProgramInput {
3746                context_in: Some(unsafe { plain::as_mut_bytes(&mut arg) }),
3747                ..Default::default()
3748            };
3749            let _ = skel.progs.refresh_node_ctx.test_run(input);
3750        }
3751    }
3752
3753    fn refresh_cpumasks(&mut self) -> Result<()> {
3754        let layer_is_open = |layer: &Layer| matches!(layer.kind, LayerKind::Open { .. });
3755
3756        let mut updated = false;
3757        let raw_targets = self.calc_target_nr_cpus();
3758        let au = self.cpu_pool.alloc_unit();
3759        let total_cpus = self.cpu_pool.topo.all_cpus.len();
3760
3761        // Dampen shrink: only drop halfway per cycle to avoid unnecessary
3762        // changes. There's some dampening built into util metrics but slow
3763        // down freeing further. This is solely based on intuition. Drop or
3764        // update according to real-world behavior.
3765        let targets: Vec<(usize, usize)> = raw_targets
3766            .iter()
3767            .enumerate()
3768            .map(|(idx, &(target, min))| {
3769                let cur = self.layers[idx].nr_cpus;
3770                if target < cur {
3771                    let dampened = cur - (cur - target).div_ceil(2);
3772                    (dampened.max(min), min)
3773                } else {
3774                    (target, min)
3775                }
3776            })
3777            .collect();
3778
3779        // Build demands for unified_alloc and compute per-node allocations.
3780        let demands = self.calc_raw_demands(&targets);
3781        let nr_nodes = self.topo.nodes.len();
3782        let node_caps: Vec<usize> = self
3783            .topo
3784            .nodes
3785            .values()
3786            .map(|n| n.span.weight() / au)
3787            .collect();
3788        let all_layer_nodes: Vec<&[usize]> = self
3789            .layer_specs
3790            .iter()
3791            .map(|s| s.nodes().as_slice())
3792            .collect();
3793        let norders: Vec<Vec<usize>> = (0..self.layers.len())
3794            .map(|idx| {
3795                layer_core_growth::node_order(
3796                    self.layer_specs[idx].nodes(),
3797                    &self.topo,
3798                    idx,
3799                    &all_layer_nodes,
3800                )
3801            })
3802            .collect();
3803        let node_groups: Vec<Vec<Vec<usize>>> = (0..self.layers.len())
3804            .map(|idx| {
3805                layer_core_growth::node_groups(
3806                    self.layer_specs[idx].nodes(),
3807                    &self.topo,
3808                    idx,
3809                    &all_layer_nodes,
3810                    &self.layer_specs[idx].kind.common().growth_algo,
3811                )
3812            })
3813            .collect();
3814        let layer_allocs = unified_alloc(total_cpus / au, &node_caps, &demands, &node_groups);
3815
3816        // Convert allocations back to CPU counts. Shrink dampening is
3817        // already applied to the targets fed into unified_alloc above.
3818        let cpu_targets: Vec<usize> = layer_allocs.iter().map(|a| a.total() * au).collect();
3819
3820        // Snapshot per-layer CPU counts for ALLOC debug logging.
3821        let prev_nr_cpus: Vec<usize> = self.layers.iter().map(|l| l.nr_cpus).collect();
3822
3823        let mut ascending: Vec<(usize, usize)> = cpu_targets.iter().copied().enumerate().collect();
3824        ascending.sort_by_key(|a| a.1);
3825
3826        // Snapshot per-layer per-node CPU counts before allocation changes.
3827        let prev_node_cpus: Vec<Vec<usize>> =
3828            self.layers.iter().map(|l| l.nr_node_cpus.clone()).collect();
3829
3830        // Per-node SD allocation requires multiple LLCs. On flat topologies
3831        // (single LLC, e.g. VMs with topology disabled), SD layers fall through
3832        // to the non-SD grow/shrink loops below.
3833        let use_sd_alloc = self.topo.all_llcs.len() > 1;
3834        let sticky_dynamic_updated = if use_sd_alloc {
3835            self.recompute_layer_core_order(&ascending, &layer_allocs, au)?
3836        } else {
3837            false
3838        };
3839        updated |= sticky_dynamic_updated;
3840
3841        // Update BPF cpumasks for StickyDynamic layers if they were updated
3842        if sticky_dynamic_updated {
3843            for (idx, layer) in self.layers.iter().enumerate() {
3844                if layer.growth_algo == LayerGrowthAlgo::StickyDynamic {
3845                    Self::update_bpf_layer_cpumask(
3846                        layer,
3847                        &mut self.skel.maps.bss_data.as_mut().unwrap().layers[idx],
3848                    );
3849                }
3850            }
3851        }
3852
3853        // Shrink per-node: free excess CPUs from each node.
3854        for &(idx, _target) in ascending.iter().rev() {
3855            let layer = &mut self.layers[idx];
3856            if layer_is_open(layer) {
3857                continue;
3858            }
3859
3860            // Skip StickyDynamic layers when per-node SD allocation is active.
3861            if layer.growth_algo == LayerGrowthAlgo::StickyDynamic && use_sd_alloc {
3862                continue;
3863            }
3864
3865            let alloc = &layer_allocs[idx];
3866            let mut freed = false;
3867
3868            for n in 0..nr_nodes {
3869                let desired = alloc.node_target(n) * au;
3870                let mut to_free = layer.nr_node_cpus[n].saturating_sub(desired);
3871                let node_span = &self.topo.nodes[&n].span;
3872
3873                while to_free > 0 {
3874                    let node_cands = layer.cpus.and(node_span);
3875                    let cpus_to_free = match self
3876                        .cpu_pool
3877                        .next_to_free(&node_cands, layer.core_order[n].iter().rev())?
3878                    {
3879                        Some(ret) => ret,
3880                        None => break,
3881                    };
3882                    let nr = cpus_to_free.weight();
3883                    trace!(
3884                        "[{}] freeing CPUs on node {}: {}",
3885                        layer.name, n, &cpus_to_free
3886                    );
3887                    layer.cpus &= &cpus_to_free.not();
3888                    layer.nr_cpus -= nr;
3889                    for cpu in cpus_to_free.iter() {
3890                        let node_id = self.cpu_pool.topo.all_cpus[&cpu].node_id;
3891                        layer.nr_llc_cpus[self.cpu_pool.topo.all_cpus[&cpu].llc_id] -= 1;
3892                        layer.nr_node_cpus[node_id] -= 1;
3893                        layer.nr_pinned_cpus[node_id] =
3894                            layer.nr_pinned_cpus[node_id].min(layer.nr_node_cpus[node_id]);
3895                    }
3896                    self.cpu_pool.free(&cpus_to_free)?;
3897                    to_free = to_free.saturating_sub(nr);
3898                    freed = true;
3899                }
3900            }
3901
3902            if freed {
3903                Self::update_bpf_layer_cpumask(
3904                    layer,
3905                    &mut self.skel.maps.bss_data.as_mut().unwrap().layers[idx],
3906                );
3907                updated = true;
3908            }
3909        }
3910
3911        // Grow layers per-node using allocations from unified_alloc.
3912        for &(idx, _target) in &ascending {
3913            let layer = &mut self.layers[idx];
3914
3915            if layer_is_open(layer) {
3916                continue;
3917            }
3918
3919            // Skip StickyDynamic layers when per-node SD allocation is active.
3920            if layer.growth_algo == LayerGrowthAlgo::StickyDynamic && use_sd_alloc {
3921                continue;
3922            }
3923
3924            let alloc = &layer_allocs[idx];
3925            let norder = &norders[idx];
3926            let mut alloced = false;
3927
3928            for &node_id in norder.iter() {
3929                let node_target = alloc.node_target(node_id) * au;
3930                let cur_node = layer.nr_node_cpus[node_id];
3931                if node_target <= cur_node {
3932                    continue;
3933                }
3934                let pinned_target = alloc.pinned[node_id] * au;
3935                let mut nr_to_alloc = node_target - cur_node;
3936                let node_span = &self.topo.nodes[&node_id].span;
3937                let node_allowed = layer.allowed_cpus.and(node_span);
3938
3939                while nr_to_alloc > 0 {
3940                    let nr_alloced = match self.cpu_pool.alloc_cpus(
3941                        &node_allowed,
3942                        &layer.core_order[node_id],
3943                        nr_to_alloc,
3944                    ) {
3945                        Some(new_cpus) => {
3946                            let nr = new_cpus.weight();
3947                            layer.cpus |= &new_cpus;
3948                            layer.nr_cpus += nr;
3949                            for cpu in new_cpus.iter() {
3950                                layer.nr_llc_cpus[self.cpu_pool.topo.all_cpus[&cpu].llc_id] += 1;
3951                                let nid = self.cpu_pool.topo.all_cpus[&cpu].node_id;
3952                                layer.nr_node_cpus[nid] += 1;
3953                                if layer.nr_pinned_cpus[nid] < pinned_target {
3954                                    layer.nr_pinned_cpus[nid] += 1;
3955                                }
3956                            }
3957                            nr
3958                        }
3959                        None => 0,
3960                    };
3961                    if nr_alloced == 0 {
3962                        break;
3963                    }
3964                    alloced = true;
3965                    nr_to_alloc -= nr_alloced.min(nr_to_alloc);
3966                }
3967            }
3968
3969            if alloced {
3970                Self::update_bpf_layer_cpumask(
3971                    layer,
3972                    &mut self.skel.maps.bss_data.as_mut().unwrap().layers[idx],
3973                );
3974                updated = true;
3975            }
3976        }
3977
3978        // Tell BPF whether all CPUs are allocated.  When the system is
3979        // fully allocated, idle_confined layers have nowhere to grow, so
3980        // they should be allowed to use open (unprotected) idle CPUs from
3981        // other layers.
3982        let total_allocated: usize = self.layers.iter().map(|l| l.nr_cpus).sum();
3983        let fully_allocated = total_allocated >= total_cpus;
3984        for (idx, layer) in self.layers.iter().enumerate() {
3985            if !layer_is_open(layer) {
3986                self.skel.maps.bss_data.as_mut().unwrap().layers[idx]
3987                    .fully_allocated
3988                    .write(fully_allocated);
3989            }
3990        }
3991
3992        // Recompute growth_denied: for each node, check whether the
3993        // unpinned portion of the layer warranted growth (unpinned util /
3994        // util_high > unpinned CPUs) but the node didn't gain CPUs.  Only
3995        // unpinned matters because pinned tasks can't migrate cross-node.
3996        let node_utils = &self.sched_stats.layer_node_utils;
3997        let pinned_utils = &self.sched_stats.layer_node_pinned_utils;
3998        for (idx, layer) in self.layers.iter().enumerate() {
3999            self.growth_denied[idx].fill(false);
4000            let util_high = match layer.kind.util_range() {
4001                Some((_, high)) => high,
4002                None => continue,
4003            };
4004            for n in 0..nr_nodes {
4005                let unpinned_util = (node_utils[idx][n] - pinned_utils[idx][n]).max(0.0);
4006                let unpinned_cpus_needed = unpinned_util / util_high;
4007                let unpinned_cpus_have =
4008                    layer.nr_node_cpus[n].saturating_sub(layer.nr_pinned_cpus[n]) as f64;
4009                let wanted = unpinned_cpus_needed > unpinned_cpus_have;
4010                let got = layer.nr_node_cpus[n] > prev_node_cpus[idx][n];
4011                if wanted && !got {
4012                    self.growth_denied[idx][n] = true;
4013                }
4014            }
4015        }
4016
4017        // Log per-layer allocation changes.
4018        if updated {
4019            for (idx, layer) in self.layers.iter().enumerate() {
4020                if layer_is_open(layer) {
4021                    continue;
4022                }
4023                let prev = prev_nr_cpus[idx];
4024                let cur = layer.nr_cpus;
4025                if prev != cur {
4026                    debug!(
4027                        "ALLOC {} algo={:?} cpus:{}→{} mask={:x}",
4028                        layer.name, layer.growth_algo, prev, cur, layer.cpus,
4029                    );
4030                }
4031            }
4032            debug!(
4033                "ALLOC pool_available={}",
4034                self.cpu_pool.available_cpus().weight()
4035            );
4036        }
4037
4038        // Log allocation changes.
4039        if updated {
4040            let nr_nodes = self.topo.nodes.len();
4041            for (idx, layer) in self.layers.iter().enumerate() {
4042                if layer_is_open(layer) {
4043                    continue;
4044                }
4045                let prev = &prev_node_cpus[idx];
4046                let cur = &layer.nr_node_cpus;
4047                if prev == cur {
4048                    continue;
4049                }
4050                let per_node: String = (0..nr_nodes)
4051                    .map(|n| format!("n{}:{}→{}", n, prev[n], cur[n]))
4052                    .collect::<Vec<_>>()
4053                    .join(" ");
4054                let prev_total: usize = prev.iter().sum();
4055                let cur_total: usize = cur[..nr_nodes].iter().sum();
4056                let target: String = (0..nr_nodes)
4057                    .map(|n| format!("n{}:{}", n, layer_allocs[idx].node_target(n) * au))
4058                    .collect::<Vec<_>>()
4059                    .join(" ");
4060                debug!(
4061                    "ALLOC {} algo={:?} {} total:{}→{} target:[{}] mask={:x}",
4062                    layer.name,
4063                    layer.growth_algo,
4064                    per_node,
4065                    prev_total,
4066                    cur_total,
4067                    target,
4068                    layer.cpus,
4069                );
4070            }
4071            debug!(
4072                "ALLOC pool_available={}",
4073                self.cpu_pool.available_cpus().weight()
4074            );
4075        }
4076
4077        // Give the rest to the open layers.
4078        if updated {
4079            for (idx, layer) in self.layers.iter_mut().enumerate() {
4080                if !layer_is_open(layer) {
4081                    continue;
4082                }
4083
4084                let bpf_layer = &mut self.skel.maps.bss_data.as_mut().unwrap().layers[idx];
4085                let available_cpus = self.cpu_pool.available_cpus().and(&layer.allowed_cpus);
4086                let nr_available_cpus = available_cpus.weight();
4087
4088                // Open layers need the intersection of allowed cpus and
4089                // available cpus. Recompute per-LLC and per-node counts
4090                // since open layers bypass alloc/free.
4091                layer.cpus = available_cpus;
4092                layer.nr_cpus = nr_available_cpus;
4093                for llc in self.cpu_pool.topo.all_llcs.values() {
4094                    layer.nr_llc_cpus[llc.id] = layer.cpus.and(&llc.span).weight();
4095                }
4096                for node in self.cpu_pool.topo.nodes.values() {
4097                    layer.nr_node_cpus[node.id] = layer.cpus.and(&node.span).weight();
4098                    layer.nr_pinned_cpus[node.id] = 0;
4099                }
4100                Self::update_bpf_layer_cpumask(layer, bpf_layer);
4101            }
4102
4103            for (&node_id, &cpu) in &self.cpu_pool.fallback_cpus {
4104                self.skel.maps.bss_data.as_mut().unwrap().fallback_cpus[node_id] = cpu as u32;
4105            }
4106
4107            for (lidx, layer) in self.layers.iter().enumerate() {
4108                self.nr_layer_cpus_ranges[lidx] = (
4109                    self.nr_layer_cpus_ranges[lidx].0.min(layer.nr_cpus),
4110                    self.nr_layer_cpus_ranges[lidx].1.max(layer.nr_cpus),
4111                );
4112            }
4113
4114            // Trigger updates on the BPF side.
4115            let input = ProgramInput {
4116                ..Default::default()
4117            };
4118            let prog = &mut self.skel.progs.refresh_layer_cpumasks;
4119            let _ = prog.test_run(input);
4120
4121            // Update per-node empty layer IDs via BPF prog.
4122            let nr_nodes = self.topo.nodes.len();
4123            let node_empty_layers: Vec<Vec<u32>> = (0..nr_nodes)
4124                .map(|nid| {
4125                    self.layers
4126                        .iter()
4127                        .enumerate()
4128                        .filter(|(_lidx, layer)| layer.nr_node_cpus[nid] == 0)
4129                        .map(|(lidx, _)| lidx as u32)
4130                        .collect()
4131                })
4132                .collect();
4133            Self::refresh_node_ctx(&mut self.skel, &self.topo, &node_empty_layers, false);
4134        }
4135
4136        if let Err(e) = self.update_netdev_cpumasks() {
4137            warn!("Failed to update netdev IRQ cpumasks: {:#}", e);
4138        }
4139        Ok(())
4140    }
4141
4142    fn refresh_idle_qos(&mut self) -> Result<()> {
4143        if !self.idle_qos_enabled {
4144            return Ok(());
4145        }
4146
4147        let mut cpu_idle_qos = vec![0; *NR_CPU_IDS];
4148        for layer in self.layers.iter() {
4149            let idle_resume_us = layer.kind.common().idle_resume_us.unwrap_or(0) as i32;
4150            for cpu in layer.cpus.iter() {
4151                cpu_idle_qos[cpu] = idle_resume_us;
4152            }
4153        }
4154
4155        for (cpu, idle_resume_usec) in cpu_idle_qos.iter().enumerate() {
4156            update_cpu_idle_resume_latency(cpu, *idle_resume_usec)?;
4157        }
4158
4159        Ok(())
4160    }
4161
4162    fn refresh_xnuma(&mut self) {
4163        let nr_nodes = self.topo.nodes.len();
4164        if nr_nodes <= 1 {
4165            return;
4166        }
4167
4168        let duty_sums = &self.sched_stats.layer_node_duty_sums;
4169
4170        for (layer_idx, spec) in self.layer_specs.iter().enumerate() {
4171            let common = spec.kind.common();
4172            let threshold = common.xnuma_threshold;
4173            let threshold_delta = common.xnuma_threshold_delta;
4174            let bpf_layer = &mut self.skel.maps.bss_data.as_mut().unwrap().layers[layer_idx];
4175
4176            if threshold.0 <= 0.0 && threshold.1 <= 0.0 {
4177                // Off — all open, infinite budget
4178                for src in 0..nr_nodes {
4179                    bpf_layer.node[src].xnuma_is_mig_src.write(true);
4180                    for dst in 0..nr_nodes {
4181                        bpf_layer.node[src].xnuma[dst].rate = u64::MAX;
4182                    }
4183                }
4184                self.xnuma_mig_src[layer_idx].fill(false);
4185                continue;
4186            }
4187
4188            let layer = &self.layers[layer_idx];
4189            let is_mig_src = xnuma_check_active(
4190                &duty_sums[layer_idx],
4191                &layer.nr_node_cpus,
4192                threshold,
4193                threshold_delta,
4194                &self.growth_denied[layer_idx],
4195                &self.xnuma_mig_src[layer_idx],
4196            );
4197
4198            self.xnuma_mig_src[layer_idx] = is_mig_src.clone();
4199
4200            let result = xnuma_compute_rates(&duty_sums[layer_idx], &layer.nr_node_cpus);
4201
4202            // Write rates before is_mig_src so BPF sees valid rates
4203            // when the gate activates.
4204            for src in 0..nr_nodes {
4205                for dst in 0..nr_nodes {
4206                    bpf_layer.node[src].xnuma[dst].rate = result.rates[src][dst];
4207                }
4208            }
4209            for (nid, is_src) in is_mig_src.iter().enumerate().take(nr_nodes) {
4210                bpf_layer.node[nid].xnuma_is_mig_src.write(*is_src);
4211            }
4212        }
4213    }
4214
4215    fn step(&mut self) -> Result<()> {
4216        let started_at = Instant::now();
4217        self.sched_stats.refresh(
4218            &mut self.skel,
4219            &self.proc_reader,
4220            started_at,
4221            self.processing_dur,
4222            &self.gpu_task_handler,
4223        )?;
4224
4225        // Update BPF with EWMA values
4226        self.skel
4227            .maps
4228            .bss_data
4229            .as_mut()
4230            .unwrap()
4231            .system_cpu_util_ewma = (self.sched_stats.system_cpu_util_ewma * 10000.0) as u64;
4232
4233        for layer_id in 0..self.sched_stats.nr_layers {
4234            self.skel
4235                .maps
4236                .bss_data
4237                .as_mut()
4238                .unwrap()
4239                .layer_dsq_insert_ewma[layer_id] =
4240                (self.sched_stats.layer_dsq_insert_ewma[layer_id] * 10000.0) as u64;
4241        }
4242
4243        self.refresh_cpumasks()?;
4244        self.refresh_xnuma();
4245        self.refresh_idle_qos()?;
4246        self.gpu_task_handler.maybe_affinitize();
4247        self.processing_dur += Instant::now().duration_since(started_at);
4248        Ok(())
4249    }
4250
4251    fn generate_sys_stats(
4252        &mut self,
4253        stats: &Stats,
4254        cpus_ranges: &mut [(usize, usize)],
4255    ) -> Result<SysStats> {
4256        let bstats = &stats.bpf_stats;
4257        let mut sys_stats = SysStats::new(stats, bstats, &self.cpu_pool.fallback_cpus)?;
4258
4259        for (lidx, (spec, layer)) in self.layer_specs.iter().zip(self.layers.iter()).enumerate() {
4260            let layer_stats = LayerStats::new(
4261                lidx,
4262                layer,
4263                stats,
4264                bstats,
4265                cpus_ranges[lidx],
4266                self.xnuma_mig_src[lidx].iter().any(|&a| a),
4267            );
4268            sys_stats.layers.insert(spec.name.to_string(), layer_stats);
4269            cpus_ranges[lidx] = (layer.nr_cpus, layer.nr_cpus);
4270        }
4271
4272        Ok(sys_stats)
4273    }
4274
4275    // Helper function to process a cgroup creation (common logic for walkdir and inotify)
4276    fn process_cgroup_creation(
4277        path: &Path,
4278        cgroup_regexes: &HashMap<u32, Regex>,
4279        cgroup_path_to_id: &mut HashMap<String, u64>,
4280        sender: &crossbeam::channel::Sender<CgroupEvent>,
4281    ) {
4282        let path_str = path.to_string_lossy().to_string();
4283
4284        // Get cgroup ID (inode number)
4285        let cgroup_id = std::fs::metadata(path)
4286            .map(|metadata| {
4287                use std::os::unix::fs::MetadataExt;
4288                metadata.ino()
4289            })
4290            .unwrap_or(0);
4291
4292        // Build match bitmap by testing against CgroupRegex rules
4293        let mut match_bitmap = 0u64;
4294        for (rule_id, regex) in cgroup_regexes {
4295            if regex.is_match(&path_str) {
4296                match_bitmap |= 1u64 << rule_id;
4297            }
4298        }
4299
4300        // Store in hash
4301        cgroup_path_to_id.insert(path_str.clone(), cgroup_id);
4302
4303        // Send event
4304        if let Err(e) = sender.send(CgroupEvent::Created {
4305            path: path_str,
4306            cgroup_id,
4307            match_bitmap,
4308        }) {
4309            error!("Failed to send cgroup creation event: {}", e);
4310        }
4311    }
4312
4313    fn start_cgroup_watcher(
4314        shutdown: Arc<AtomicBool>,
4315        cgroup_regexes: HashMap<u32, Regex>,
4316    ) -> Result<Receiver<CgroupEvent>> {
4317        let mut inotify = Inotify::init().context("Failed to initialize inotify")?;
4318        let mut wd_to_path = HashMap::new();
4319
4320        // Create crossbeam channel for cgroup events (bounded to prevent memory issues)
4321        let (sender, receiver) = crossbeam::channel::bounded::<CgroupEvent>(1024);
4322
4323        // Watch for directory creation and deletion events
4324        let root_wd = inotify
4325            .watches()
4326            .add("/sys/fs/cgroup", WatchMask::CREATE | WatchMask::DELETE)
4327            .context("Failed to add watch for /sys/fs/cgroup")?;
4328        wd_to_path.insert(root_wd, PathBuf::from("/sys/fs/cgroup"));
4329
4330        // Also recursively watch existing directories for new subdirectories
4331        Self::add_recursive_watches(&mut inotify, &mut wd_to_path, Path::new("/sys/fs/cgroup"))?;
4332
4333        // Spawn watcher thread
4334        std::thread::spawn(move || {
4335            let mut buffer = [0; 4096];
4336            let inotify_fd = inotify.as_raw_fd();
4337            // Maintain hash of cgroup path -> cgroup ID (inode number)
4338            let mut cgroup_path_to_id = HashMap::<String, u64>::new();
4339
4340            // Populate existing cgroups
4341            for entry in WalkDir::new("/sys/fs/cgroup")
4342                .into_iter()
4343                .filter_map(|e| e.ok())
4344                .filter(|e| e.file_type().is_dir())
4345            {
4346                let path = entry.path();
4347                Self::process_cgroup_creation(
4348                    path,
4349                    &cgroup_regexes,
4350                    &mut cgroup_path_to_id,
4351                    &sender,
4352                );
4353            }
4354
4355            while !shutdown.load(Ordering::Relaxed) {
4356                // Use select to wait for events with a 100ms timeout
4357                let ready = unsafe {
4358                    let mut read_fds: libc::fd_set = std::mem::zeroed();
4359                    libc::FD_ZERO(&mut read_fds);
4360                    libc::FD_SET(inotify_fd, &mut read_fds);
4361
4362                    let mut timeout = libc::timeval {
4363                        tv_sec: 0,
4364                        tv_usec: 100_000, // 100ms
4365                    };
4366
4367                    libc::select(
4368                        inotify_fd + 1,
4369                        &mut read_fds,
4370                        std::ptr::null_mut(),
4371                        std::ptr::null_mut(),
4372                        &mut timeout,
4373                    )
4374                };
4375
4376                if ready <= 0 {
4377                    // Timeout or error, continue loop to check shutdown
4378                    continue;
4379                }
4380
4381                // Read events non-blocking
4382                let events = match inotify.read_events(&mut buffer) {
4383                    Ok(events) => events,
4384                    Err(e) => {
4385                        error!("Error reading inotify events: {}", e);
4386                        break;
4387                    }
4388                };
4389
4390                for event in events {
4391                    if !event.mask.contains(inotify::EventMask::CREATE)
4392                        && !event.mask.contains(inotify::EventMask::DELETE)
4393                    {
4394                        continue;
4395                    }
4396
4397                    let name = match event.name {
4398                        Some(name) => name,
4399                        None => continue,
4400                    };
4401
4402                    let parent_path = match wd_to_path.get(&event.wd) {
4403                        Some(parent) => parent,
4404                        None => {
4405                            warn!("Unknown watch descriptor: {:?}", event.wd);
4406                            continue;
4407                        }
4408                    };
4409
4410                    let path = parent_path.join(name.to_string_lossy().as_ref());
4411
4412                    if event.mask.contains(inotify::EventMask::CREATE) {
4413                        if !path.is_dir() {
4414                            continue;
4415                        }
4416
4417                        Self::process_cgroup_creation(
4418                            &path,
4419                            &cgroup_regexes,
4420                            &mut cgroup_path_to_id,
4421                            &sender,
4422                        );
4423
4424                        // Add watch for this new directory
4425                        match inotify
4426                            .watches()
4427                            .add(&path, WatchMask::CREATE | WatchMask::DELETE)
4428                        {
4429                            Ok(wd) => {
4430                                wd_to_path.insert(wd, path.clone());
4431                            }
4432                            Err(e) => {
4433                                warn!(
4434                                    "Failed to add watch for new cgroup {}: {}",
4435                                    path.display(),
4436                                    e
4437                                );
4438                            }
4439                        }
4440                    } else if event.mask.contains(inotify::EventMask::DELETE) {
4441                        let path_str = path.to_string_lossy().to_string();
4442
4443                        // Get cgroup ID from our hash (since the directory is gone, we can't stat it)
4444                        let cgroup_id = cgroup_path_to_id.remove(&path_str).unwrap_or(0);
4445
4446                        // Send removal event to main thread
4447                        if let Err(e) = sender.send(CgroupEvent::Removed {
4448                            path: path_str,
4449                            cgroup_id,
4450                        }) {
4451                            error!("Failed to send cgroup removal event: {}", e);
4452                        }
4453
4454                        // Find and remove the watch descriptor for this path
4455                        let wd_to_remove = wd_to_path.iter().find_map(|(wd, watched_path)| {
4456                            if watched_path == &path {
4457                                Some(wd.clone())
4458                            } else {
4459                                None
4460                            }
4461                        });
4462                        if let Some(wd) = wd_to_remove {
4463                            wd_to_path.remove(&wd);
4464                        }
4465                    }
4466                }
4467            }
4468        });
4469
4470        Ok(receiver)
4471    }
4472
4473    fn add_recursive_watches(
4474        inotify: &mut Inotify,
4475        wd_to_path: &mut HashMap<inotify::WatchDescriptor, PathBuf>,
4476        path: &Path,
4477    ) -> Result<()> {
4478        for entry in WalkDir::new(path)
4479            .into_iter()
4480            .filter_map(|e| e.ok())
4481            .filter(|e| e.file_type().is_dir())
4482            .skip(1)
4483        {
4484            let entry_path = entry.path();
4485            // Add watch for this directory
4486            match inotify
4487                .watches()
4488                .add(entry_path, WatchMask::CREATE | WatchMask::DELETE)
4489            {
4490                Ok(wd) => {
4491                    wd_to_path.insert(wd, entry_path.to_path_buf());
4492                }
4493                Err(e) => {
4494                    debug!("Failed to add watch for {}: {}", entry_path.display(), e);
4495                }
4496            }
4497        }
4498        Ok(())
4499    }
4500
4501    fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
4502        let (res_ch, req_ch) = self.stats_server.channels();
4503        let mut next_sched_at = Instant::now() + self.sched_intv;
4504        let enable_layer_refresh = !self.layer_refresh_intv.is_zero();
4505        let mut next_layer_refresh_at = Instant::now() + self.layer_refresh_intv;
4506        let mut cpus_ranges = HashMap::<ThreadId, Vec<(usize, usize)>>::new();
4507
4508        // Start the cgroup watcher only if there are CgroupRegex rules
4509        let cgroup_regexes = self.cgroup_regexes.take().unwrap();
4510        let cgroup_event_rx = if !cgroup_regexes.is_empty() {
4511            Some(Self::start_cgroup_watcher(
4512                shutdown.clone(),
4513                cgroup_regexes,
4514            )?)
4515        } else {
4516            None
4517        };
4518
4519        while !shutdown.load(Ordering::Relaxed) && !uei_exited!(&self.skel, uei) {
4520            let now = Instant::now();
4521
4522            if now >= next_sched_at {
4523                self.step()?;
4524                while next_sched_at < now {
4525                    next_sched_at += self.sched_intv;
4526                }
4527            }
4528
4529            if enable_layer_refresh && now >= next_layer_refresh_at {
4530                self.skel
4531                    .maps
4532                    .bss_data
4533                    .as_mut()
4534                    .unwrap()
4535                    .layer_refresh_seq_avgruntime += 1;
4536                while next_layer_refresh_at < now {
4537                    next_layer_refresh_at += self.layer_refresh_intv;
4538                }
4539            }
4540
4541            // Handle both stats requests and cgroup events with timeout
4542            let timeout_duration = next_sched_at.saturating_duration_since(Instant::now());
4543            let never_rx = crossbeam::channel::never();
4544            let cgroup_rx = cgroup_event_rx.as_ref().unwrap_or(&never_rx);
4545
4546            select! {
4547                recv(req_ch) -> msg => match msg {
4548                    Ok(StatsReq::Hello(tid)) => {
4549                        cpus_ranges.insert(
4550                            tid,
4551                            self.layers.iter().map(|l| (l.nr_cpus, l.nr_cpus)).collect(),
4552                        );
4553                        let stats =
4554                            Stats::new(&mut self.skel, &self.proc_reader, self.topo.clone(), &self.gpu_task_handler, self.sched_stats.util_compensation)?;
4555                        res_ch.send(StatsRes::Hello(Box::new(stats)))?;
4556                    }
4557                    Ok(StatsReq::Refresh(tid, mut stats)) => {
4558                        // Propagate self's layer cpu ranges into each stat's.
4559                        for i in 0..self.nr_layer_cpus_ranges.len() {
4560                            for ranges in cpus_ranges.values_mut() {
4561                                ranges[i] = (
4562                                    ranges[i].0.min(self.nr_layer_cpus_ranges[i].0),
4563                                    ranges[i].1.max(self.nr_layer_cpus_ranges[i].1),
4564                                );
4565                            }
4566                            self.nr_layer_cpus_ranges[i] =
4567                                (self.layers[i].nr_cpus, self.layers[i].nr_cpus);
4568                        }
4569
4570                        stats.refresh(
4571                            &mut self.skel,
4572                            &self.proc_reader,
4573                            now,
4574                            self.processing_dur,
4575                            &self.gpu_task_handler,
4576                        )?;
4577                        let sys_stats =
4578                            self.generate_sys_stats(&stats, cpus_ranges.get_mut(&tid).unwrap())?;
4579                        res_ch.send(StatsRes::Refreshed(Box::new((*stats, sys_stats))))?;
4580                    }
4581                    Ok(StatsReq::Bye(tid)) => {
4582                        cpus_ranges.remove(&tid);
4583                        res_ch.send(StatsRes::Bye)?;
4584                    }
4585                    Err(e) => Err(e)?,
4586                },
4587
4588                recv(cgroup_rx) -> event => match event {
4589                    Ok(CgroupEvent::Created { path, cgroup_id, match_bitmap }) => {
4590                        // Insert into BPF map
4591                        self.skel.maps.cgroup_match_bitmap.update(
4592                            &cgroup_id.to_ne_bytes(),
4593                            &match_bitmap.to_ne_bytes(),
4594                            libbpf_rs::MapFlags::ANY,
4595                        ).with_context(|| format!(
4596                            "Failed to insert cgroup {}({}) into BPF map. Cgroup map may be full \
4597                             (max 16384 entries). Aborting.",
4598                            cgroup_id, path
4599                        ))?;
4600
4601                        debug!("Added cgroup {} to BPF map with bitmap 0x{:x}", cgroup_id, match_bitmap);
4602                    }
4603                    Ok(CgroupEvent::Removed { path, cgroup_id }) => {
4604                        // Delete from BPF map
4605                        if let Err(e) = self.skel.maps.cgroup_match_bitmap.delete(&cgroup_id.to_ne_bytes()) {
4606                            warn!("Failed to delete cgroup {} from BPF map: {}", cgroup_id, e);
4607                        } else {
4608                            debug!("Removed cgroup {}({}) from BPF map", cgroup_id, path);
4609                        }
4610                    }
4611                    Err(e) => {
4612                        error!("Error receiving cgroup event: {}", e);
4613                    }
4614                },
4615
4616                recv(crossbeam::channel::after(timeout_duration)) -> _ => {
4617                    // Timeout - continue main loop
4618                }
4619            }
4620        }
4621
4622        let _ = self.struct_ops.take();
4623        uei_report!(&self.skel, uei)
4624    }
4625}
4626
4627impl Drop for Scheduler<'_> {
4628    fn drop(&mut self) {
4629        info!("Unregister {SCHEDULER_NAME} scheduler");
4630
4631        if !self.netdevs.is_empty() {
4632            for (iface, netdev) in &self.netdevs {
4633                if let Err(e) = netdev.restore_cpumasks() {
4634                    warn!("Failed to restore {iface} IRQ affinity: {e}");
4635                }
4636            }
4637            info!("Restored original netdev IRQ affinity");
4638        }
4639
4640        if let Some(struct_ops) = self.struct_ops.take() {
4641            drop(struct_ops);
4642        }
4643    }
4644}
4645
4646fn write_example_file(path: &str) -> Result<()> {
4647    let mut f = fs::OpenOptions::new()
4648        .create_new(true)
4649        .write(true)
4650        .open(path)?;
4651    Ok(f.write_all(serde_json::to_string_pretty(&*EXAMPLE_CONFIG)?.as_bytes())?)
4652}
4653
4654struct HintLayerInfo {
4655    layer_id: usize,
4656    system_cpu_util_below: Option<f64>,
4657    dsq_insert_below: Option<f64>,
4658}
4659
4660fn verify_layer_specs(specs: &[LayerSpec]) -> Result<HashMap<u64, HintLayerInfo>> {
4661    let mut hint_to_layer_map = HashMap::<u64, (usize, String, Option<f64>, Option<f64>)>::new();
4662
4663    let nr_specs = specs.len();
4664    if nr_specs == 0 {
4665        bail!("No layer spec");
4666    }
4667    if nr_specs > MAX_LAYERS {
4668        bail!("Too many layer specs");
4669    }
4670
4671    for (idx, spec) in specs.iter().enumerate() {
4672        if idx < nr_specs - 1 {
4673            if spec.matches.is_empty() {
4674                bail!("Non-terminal spec {:?} has NULL matches", spec.name);
4675            }
4676        } else if spec.matches.len() != 1 || !spec.matches[0].is_empty() {
4677            bail!("Terminal spec {:?} must have an empty match", spec.name);
4678        }
4679
4680        if spec.matches.len() > MAX_LAYER_MATCH_ORS {
4681            bail!(
4682                "Spec {:?} has too many ({}) OR match blocks",
4683                spec.name,
4684                spec.matches.len()
4685            );
4686        }
4687
4688        for (ands_idx, ands) in spec.matches.iter().enumerate() {
4689            if ands.len() > NR_LAYER_MATCH_KINDS {
4690                bail!(
4691                    "Spec {:?}'s {}th OR block has too many ({}) match conditions",
4692                    spec.name,
4693                    ands_idx,
4694                    ands.len()
4695                );
4696            }
4697            let mut hint_equals_cnt = 0;
4698            let mut system_cpu_util_below_cnt = 0;
4699            let mut dsq_insert_below_cnt = 0;
4700            let mut hint_value: Option<u64> = None;
4701            let mut system_cpu_util_threshold: Option<f64> = None;
4702            let mut dsq_insert_threshold: Option<f64> = None;
4703            for one in ands.iter() {
4704                match one {
4705                    LayerMatch::CgroupPrefix(prefix) => {
4706                        if prefix.len() > MAX_PATH {
4707                            bail!("Spec {:?} has too long a cgroup prefix", spec.name);
4708                        }
4709                    }
4710                    LayerMatch::CgroupSuffix(suffix) => {
4711                        if suffix.len() > MAX_PATH {
4712                            bail!("Spec {:?} has too long a cgroup suffix", spec.name);
4713                        }
4714                    }
4715                    LayerMatch::CgroupContains(substr) => {
4716                        if substr.len() > MAX_PATH {
4717                            bail!("Spec {:?} has too long a cgroup substr", spec.name);
4718                        }
4719                    }
4720                    LayerMatch::CommPrefix(prefix) => {
4721                        if prefix.len() > MAX_COMM {
4722                            bail!("Spec {:?} has too long a comm prefix", spec.name);
4723                        }
4724                    }
4725                    LayerMatch::PcommPrefix(prefix) => {
4726                        if prefix.len() > MAX_COMM {
4727                            bail!("Spec {:?} has too long a process name prefix", spec.name);
4728                        }
4729                    }
4730                    LayerMatch::SystemCpuUtilBelow(threshold) => {
4731                        if *threshold < 0.0 || *threshold > 1.0 {
4732                            bail!(
4733                                "Spec {:?} has SystemCpuUtilBelow threshold outside the range [0.0, 1.0]",
4734                                spec.name
4735                            );
4736                        }
4737                        system_cpu_util_threshold = Some(*threshold);
4738                        system_cpu_util_below_cnt += 1;
4739                    }
4740                    LayerMatch::DsqInsertBelow(threshold) => {
4741                        if *threshold < 0.0 || *threshold > 1.0 {
4742                            bail!(
4743                                "Spec {:?} has DsqInsertBelow threshold outside the range [0.0, 1.0]",
4744                                spec.name
4745                            );
4746                        }
4747                        dsq_insert_threshold = Some(*threshold);
4748                        dsq_insert_below_cnt += 1;
4749                    }
4750                    LayerMatch::HintEquals(hint) => {
4751                        if *hint > 1024 {
4752                            bail!(
4753                                "Spec {:?} has hint value outside the range [0, 1024]",
4754                                spec.name
4755                            );
4756                        }
4757                        hint_value = Some(*hint);
4758                        hint_equals_cnt += 1;
4759                    }
4760                    _ => {}
4761                }
4762            }
4763            if hint_equals_cnt > 1 {
4764                bail!("Only 1 HintEquals match permitted per AND block");
4765            }
4766            let high_freq_matcher_cnt = system_cpu_util_below_cnt + dsq_insert_below_cnt;
4767            if high_freq_matcher_cnt > 0 {
4768                if hint_equals_cnt != 1 {
4769                    bail!(
4770                        "High-frequency matchers (SystemCpuUtilBelow, DsqInsertBelow) must be used with one HintEquals"
4771                    );
4772                }
4773                if system_cpu_util_below_cnt > 1 {
4774                    bail!("Only 1 SystemCpuUtilBelow match permitted per AND block");
4775                }
4776                if dsq_insert_below_cnt > 1 {
4777                    bail!("Only 1 DsqInsertBelow match permitted per AND block");
4778                }
4779                if ands.len() != hint_equals_cnt + system_cpu_util_below_cnt + dsq_insert_below_cnt
4780                {
4781                    bail!(
4782                        "High-frequency matchers must be used only with HintEquals (no other matchers)"
4783                    );
4784                }
4785            } else if hint_equals_cnt == 1 && ands.len() != 1 {
4786                bail!("HintEquals match cannot be in conjunction with other matches");
4787            }
4788
4789            // Insert hint into map if present
4790            if let Some(hint) = hint_value {
4791                if let Some((layer_id, name, _, _)) = hint_to_layer_map.get(&hint) {
4792                    if *layer_id != idx {
4793                        bail!(
4794                            "Spec {:?} has hint value ({}) that is already mapped to Spec {:?}",
4795                            spec.name,
4796                            hint,
4797                            name
4798                        );
4799                    }
4800                } else {
4801                    hint_to_layer_map.insert(
4802                        hint,
4803                        (
4804                            idx,
4805                            spec.name.clone(),
4806                            system_cpu_util_threshold,
4807                            dsq_insert_threshold,
4808                        ),
4809                    );
4810                }
4811            }
4812        }
4813
4814        match spec.kind {
4815            LayerKind::Confined {
4816                cpus_range,
4817                util_range,
4818                ..
4819            }
4820            | LayerKind::Grouped {
4821                cpus_range,
4822                util_range,
4823                ..
4824            } => {
4825                if let Some((cpus_min, cpus_max)) = cpus_range
4826                    && cpus_min > cpus_max
4827                {
4828                    bail!(
4829                        "Spec {:?} has invalid cpus_range({}, {})",
4830                        spec.name,
4831                        cpus_min,
4832                        cpus_max
4833                    );
4834                }
4835                if util_range.0 >= util_range.1 {
4836                    bail!(
4837                        "Spec {:?} has invalid util_range ({}, {})",
4838                        spec.name,
4839                        util_range.0,
4840                        util_range.1
4841                    );
4842                }
4843            }
4844            _ => {}
4845        }
4846    }
4847
4848    Ok(hint_to_layer_map
4849        .into_iter()
4850        .map(|(k, v)| {
4851            (
4852                k,
4853                HintLayerInfo {
4854                    layer_id: v.0,
4855                    system_cpu_util_below: v.2,
4856                    dsq_insert_below: v.3,
4857                },
4858            )
4859        })
4860        .collect())
4861}
4862
4863fn name_suffix(cgroup: &str, len: usize) -> String {
4864    let suffixlen = std::cmp::min(len, cgroup.len());
4865    let suffixrev: String = cgroup.chars().rev().take(suffixlen).collect();
4866
4867    suffixrev.chars().rev().collect()
4868}
4869
4870fn traverse_sysfs(dir: &Path) -> Result<Vec<PathBuf>> {
4871    let mut paths = vec![];
4872
4873    if !dir.is_dir() {
4874        panic!("path {:?} does not correspond to directory", dir);
4875    }
4876
4877    let direntries = fs::read_dir(dir)?;
4878
4879    for entry in direntries {
4880        let path = entry?.path();
4881        if path.is_dir() {
4882            paths.append(&mut traverse_sysfs(&path)?);
4883            paths.push(path);
4884        }
4885    }
4886
4887    Ok(paths)
4888}
4889
4890fn find_cpumask(cgroup: &str) -> Cpumask {
4891    let mut path = String::from(cgroup);
4892    path.push_str("/cpuset.cpus.effective");
4893
4894    let description = fs::read_to_string(&mut path).unwrap();
4895
4896    Cpumask::from_cpulist(&description).unwrap()
4897}
4898
4899fn expand_template(rule: &LayerMatch) -> Result<Vec<(LayerMatch, Cpumask)>> {
4900    match rule {
4901        LayerMatch::CgroupSuffix(suffix) => Ok(traverse_sysfs(Path::new("/sys/fs/cgroup"))?
4902            .into_iter()
4903            .map(|cgroup| String::from(cgroup.to_str().expect("could not parse cgroup path")))
4904            .filter(|cgroup| cgroup.ends_with(suffix))
4905            .map(|cgroup| {
4906                (
4907                    {
4908                        let mut slashterminated = cgroup.clone();
4909                        slashterminated.push('/');
4910                        LayerMatch::CgroupSuffix(name_suffix(&slashterminated, 64))
4911                    },
4912                    find_cpumask(&cgroup),
4913                )
4914            })
4915            .collect()),
4916        LayerMatch::CgroupRegex(expr) => Ok(traverse_sysfs(Path::new("/sys/fs/cgroup"))?
4917            .into_iter()
4918            .map(|cgroup| String::from(cgroup.to_str().expect("could not parse cgroup path")))
4919            .filter(|cgroup| {
4920                let re = Regex::new(expr).unwrap();
4921                re.is_match(cgroup)
4922            })
4923            .map(|cgroup| {
4924                (
4925                    // Here we convert the regex match into a suffix match because we still need to
4926                    // do the matching on the bpf side and doing a regex match in bpf isn't
4927                    // easily done.
4928                    {
4929                        let mut slashterminated = cgroup.clone();
4930                        slashterminated.push('/');
4931                        LayerMatch::CgroupSuffix(name_suffix(&slashterminated, 64))
4932                    },
4933                    find_cpumask(&cgroup),
4934                )
4935            })
4936            .collect()),
4937        _ => panic!("Unimplemented template enum {:?}", rule),
4938    }
4939}
4940
4941fn create_perf_fds(skel: &mut BpfSkel, event: u64) -> Result<()> {
4942    let mut attr = perf::bindings::perf_event_attr {
4943        size: std::mem::size_of::<perf::bindings::perf_event_attr>() as u32,
4944        type_: perf::bindings::PERF_TYPE_RAW,
4945        config: event,
4946        sample_type: 0u64,
4947        ..Default::default()
4948    };
4949    attr.__bindgen_anon_1.sample_period = 0u64;
4950    attr.set_disabled(0);
4951
4952    let perf_events_map = &skel.maps.scx_pmu_map;
4953    let map_fd = unsafe { libbpf_sys::bpf_map__fd(perf_events_map.as_libbpf_object().as_ptr()) };
4954
4955    let mut failures = 0u64;
4956
4957    for cpu in 0..*NR_CPUS_POSSIBLE {
4958        let fd = unsafe { perf::perf_event_open(&mut attr as *mut _, -1, cpu as i32, -1, 0) };
4959        if fd < 0 {
4960            failures += 1;
4961            trace!(
4962                "perf_event_open failed cpu={cpu} errno={}",
4963                std::io::Error::last_os_error()
4964            );
4965            continue;
4966        }
4967
4968        let key = cpu as u32;
4969        let val = fd as u32;
4970        let ret = unsafe {
4971            libbpf_sys::bpf_map_update_elem(
4972                map_fd,
4973                &key as *const _ as *const _,
4974                &val as *const _ as *const _,
4975                0,
4976            )
4977        };
4978        if ret != 0 {
4979            trace!("bpf_map_update_elem failed cpu={cpu} fd={fd} ret={ret}");
4980        } else {
4981            trace!("mapped cpu={cpu} -> fd={fd}");
4982        }
4983    }
4984
4985    if failures > 0 {
4986        println!("membw tracking: failed to install {failures} counters");
4987        // Keep going, do not fail the scheduler for this
4988    }
4989
4990    Ok(())
4991}
4992
4993// Set up the counters
4994fn setup_membw_tracking(skel: &mut OpenBpfSkel) -> Result<u64> {
4995    let pmumanager = PMUManager::new()?;
4996    let codename = &pmumanager.codename as &str;
4997
4998    let pmuspec = match codename {
4999        "amdzen1" | "amdzen2" | "amdzen3" => {
5000            trace!("found AMD codename {codename}");
5001            pmumanager.pmus.get("ls_any_fills_from_sys.mem_io_local")
5002        }
5003        "amdzen4" | "amdzen5" => {
5004            trace!("found AMD codename {codename}");
5005            pmumanager.pmus.get("ls_any_fills_from_sys.dram_io_all")
5006        }
5007
5008        "haswell" | "broadwell" | "broadwellde" | "broadwellx" | "skylake" | "skylakex"
5009        | "cascadelakex" | "arrowlake" | "meteorlake" | "sapphirerapids" | "emeraldrapids"
5010        | "graniterapids" => {
5011            trace!("found Intel codename {codename}");
5012            pmumanager.pmus.get("LONGEST_LAT_CACHE.MISS")
5013        }
5014
5015        _ => {
5016            trace!("found unknown codename {codename}");
5017            None
5018        }
5019    };
5020
5021    let spec = pmuspec.ok_or("not_found").unwrap();
5022    let config = (spec.umask << 8) | spec.event[0];
5023
5024    // Install the counter in the BPF map
5025    skel.maps.rodata_data.as_mut().unwrap().membw_event = config;
5026
5027    Ok(config)
5028}
5029
5030#[clap_main::clap_main]
5031fn main(opts: Opts) -> Result<()> {
5032    if opts.version {
5033        println!(
5034            "scx_layered {}",
5035            build_id::full_version(env!("CARGO_PKG_VERSION"))
5036        );
5037        return Ok(());
5038    }
5039
5040    if opts.help_stats {
5041        stats::server_data().describe_meta(&mut std::io::stdout(), None)?;
5042        return Ok(());
5043    }
5044
5045    let env_filter = EnvFilter::try_from_default_env()
5046        .or_else(|_| match EnvFilter::try_new(&opts.log_level) {
5047            Ok(filter) => Ok(filter),
5048            Err(e) => {
5049                eprintln!(
5050                    "invalid log envvar: {}, using info, err is: {}",
5051                    opts.log_level, e
5052                );
5053                EnvFilter::try_new("info")
5054            }
5055        })
5056        .unwrap_or_else(|_| EnvFilter::new("info"));
5057
5058    match tracing_subscriber::fmt()
5059        .with_env_filter(env_filter)
5060        .with_target(true)
5061        .with_thread_ids(true)
5062        .with_file(true)
5063        .with_line_number(true)
5064        .try_init()
5065    {
5066        Ok(()) => {}
5067        Err(e) => eprintln!("failed to init logger: {}", e),
5068    }
5069
5070    if opts.verbose > 0 {
5071        warn!("Setting verbose via -v is deprecated and will be an error in future releases.");
5072    }
5073
5074    if opts.no_load_frac_limit {
5075        warn!("--no-load-frac-limit is deprecated and noop");
5076    }
5077    if opts.layer_preempt_weight_disable != 0.0 {
5078        warn!("--layer-preempt-weight-disable is deprecated and noop");
5079    }
5080    if opts.layer_growth_weight_disable != 0.0 {
5081        warn!("--layer-growth-weight-disable is deprecated and noop");
5082    }
5083    if opts.local_llc_iteration {
5084        warn!("--local_llc_iteration is deprecated and noop");
5085    }
5086
5087    debug!("opts={:?}", &opts);
5088
5089    if let Some(run_id) = opts.run_id {
5090        info!("scx_layered run_id: {}", run_id);
5091    }
5092
5093    let shutdown = Arc::new(AtomicBool::new(false));
5094    let shutdown_clone = shutdown.clone();
5095    ctrlc::set_handler(move || {
5096        shutdown_clone.store(true, Ordering::Relaxed);
5097    })
5098    .context("Error setting Ctrl-C handler")?;
5099
5100    if let Some(intv) = opts.monitor.or(opts.stats) {
5101        let shutdown_copy = shutdown.clone();
5102        let stats_columns = opts.stats_columns;
5103        let stats_no_llc = opts.stats_no_llc;
5104        let jh = std::thread::spawn(move || {
5105            match stats::monitor(
5106                Duration::from_secs_f64(intv),
5107                shutdown_copy,
5108                stats_columns,
5109                stats_no_llc,
5110            ) {
5111                Ok(_) => {
5112                    debug!("stats monitor thread finished successfully")
5113                }
5114                Err(error_object) => {
5115                    warn!(
5116                        "stats monitor thread finished because of an error {}",
5117                        error_object
5118                    )
5119                }
5120            }
5121        });
5122        if opts.monitor.is_some() {
5123            let _ = jh.join();
5124            return Ok(());
5125        }
5126    }
5127
5128    if let Some(path) = &opts.example {
5129        write_example_file(path)?;
5130        return Ok(());
5131    }
5132
5133    let mut layer_config = match opts.run_example {
5134        true => EXAMPLE_CONFIG.clone(),
5135        false => LayerConfig { specs: vec![] },
5136    };
5137
5138    for (idx, input) in opts.specs.iter().enumerate() {
5139        let specs = LayerSpec::parse(input)
5140            .context(format!("Failed to parse specs[{}] ({:?})", idx, input))?;
5141
5142        for spec in specs {
5143            match spec.template {
5144                Some(ref rule) => {
5145                    let matches = expand_template(rule)?;
5146                    // in the absence of matching cgroups, have template layers
5147                    // behave as non-template layers do.
5148                    if matches.is_empty() {
5149                        layer_config.specs.push(spec);
5150                    } else {
5151                        for (mt, mask) in matches {
5152                            let mut genspec = spec.clone();
5153
5154                            genspec.cpuset = Some(mask);
5155
5156                            // Push the new "and" rule into each "or" term.
5157                            for orterm in &mut genspec.matches {
5158                                orterm.push(mt.clone());
5159                            }
5160
5161                            match &mt {
5162                                LayerMatch::CgroupSuffix(cgroup) => genspec.name.push_str(cgroup),
5163                                _ => bail!("Template match has unexpected type"),
5164                            }
5165
5166                            // Push the generated layer into the config
5167                            layer_config.specs.push(genspec);
5168                        }
5169                    }
5170                }
5171
5172                None => {
5173                    layer_config.specs.push(spec);
5174                }
5175            }
5176        }
5177    }
5178
5179    for spec in layer_config.specs.iter_mut() {
5180        let common = spec.kind.common_mut();
5181
5182        if common.slice_us == 0 {
5183            common.slice_us = opts.slice_us;
5184        }
5185
5186        if common.weight == 0 {
5187            common.weight = DEFAULT_LAYER_WEIGHT;
5188        }
5189        common.weight = common.weight.clamp(MIN_LAYER_WEIGHT, MAX_LAYER_WEIGHT);
5190
5191        if common.preempt {
5192            if common.disallow_open_after_us.is_some() {
5193                warn!(
5194                    "Preempt layer {} has non-null disallow_open_after_us, ignored",
5195                    &spec.name
5196                );
5197            }
5198            if common.disallow_preempt_after_us.is_some() {
5199                warn!(
5200                    "Preempt layer {} has non-null disallow_preempt_after_us, ignored",
5201                    &spec.name
5202                );
5203            }
5204            common.disallow_open_after_us = Some(u64::MAX);
5205            common.disallow_preempt_after_us = Some(u64::MAX);
5206        } else {
5207            if common.disallow_open_after_us.is_none() {
5208                common.disallow_open_after_us = Some(*DFL_DISALLOW_OPEN_AFTER_US);
5209            }
5210
5211            if common.disallow_preempt_after_us.is_none() {
5212                common.disallow_preempt_after_us = Some(*DFL_DISALLOW_PREEMPT_AFTER_US);
5213            }
5214        }
5215
5216        if common.idle_smt.is_some() {
5217            warn!("Layer {} has deprecated flag \"idle_smt\"", &spec.name);
5218        }
5219
5220        if common.allow_node_aligned.is_some() {
5221            warn!(
5222                "Layer {} has deprecated flag \"allow_node_aligned\", node-aligned tasks are now always dispatched on layer DSQs",
5223                &spec.name
5224            );
5225        }
5226    }
5227
5228    let membw_required = layer_config.specs.iter().any(|spec| match spec.kind {
5229        LayerKind::Confined { membw_gb, .. } | LayerKind::Grouped { membw_gb, .. } => {
5230            membw_gb.is_some()
5231        }
5232        LayerKind::Open { .. } => false,
5233    });
5234
5235    if opts.print_and_exit {
5236        println!("specs={}", serde_json::to_string_pretty(&layer_config)?);
5237        return Ok(());
5238    }
5239
5240    debug!("specs={}", serde_json::to_string_pretty(&layer_config)?);
5241    let hint_to_layer_map = verify_layer_specs(&layer_config.specs)?;
5242
5243    let mut open_object = MaybeUninit::uninit();
5244    loop {
5245        let mut sched = Scheduler::init(
5246            &opts,
5247            &layer_config.specs,
5248            &mut open_object,
5249            &hint_to_layer_map,
5250            membw_required,
5251        )?;
5252        if !sched.run(shutdown.clone())?.should_restart() {
5253            break;
5254        }
5255    }
5256
5257    Ok(())
5258}
5259
5260#[cfg(test)]
5261mod peak_util_tests {
5262    use super::*;
5263
5264    #[test]
5265    fn test_peak_pins_recent_burst() {
5266        let elapsed = Duration::from_millis(100);
5267        let half_life = Duration::from_secs(1);
5268        let mut peak = 0.0;
5269
5270        for _ in 0..5 {
5271            peak = update_peak_util(peak, 0.1, half_life, elapsed);
5272        }
5273
5274        peak = update_peak_util(peak, 2.0, half_life, elapsed);
5275
5276        for _ in 0..5 {
5277            peak = update_peak_util(peak, 0.1, half_life, elapsed);
5278        }
5279
5280        assert!(
5281            peak > 1.3,
5282            "peak should still hold the recent burst, got {peak}"
5283        );
5284    }
5285
5286    #[test]
5287    fn test_peak_decays_after_quiet_period() {
5288        let elapsed = Duration::from_millis(100);
5289        let half_life = Duration::from_secs(1);
5290        let mut peak = 2.0;
5291
5292        for _ in 0..50 {
5293            peak = update_peak_util(peak, 0.0, half_life, elapsed);
5294        }
5295
5296        assert!(
5297            peak < 0.1,
5298            "peak should decay after a long quiet period, got {peak}"
5299        );
5300    }
5301
5302    #[test]
5303    fn test_peak_matches_steady_input() {
5304        let elapsed = Duration::from_millis(100);
5305        let half_life = Duration::from_secs(1);
5306        let mut peak = 0.0;
5307
5308        for _ in 0..30 {
5309            peak = update_peak_util(peak, 1.0, half_life, elapsed);
5310        }
5311
5312        assert!(
5313            (peak - 1.0).abs() < 1e-9,
5314            "peak should match steady-state input, got {peak}"
5315        );
5316    }
5317
5318    #[test]
5319    fn test_peak_only_changes_shrink_target() {
5320        let util_range = (0.7, 0.9);
5321        let (low_no_peak, high_no_peak) = calc_peak_aware_target_range(1.8, 1.8, util_range);
5322        let (low_with_peak, high_with_peak) = calc_peak_aware_target_range(1.8, 3.1, util_range);
5323
5324        assert_eq!(low_no_peak, 2);
5325        assert_eq!(
5326            low_with_peak, low_no_peak,
5327            "peak should not affect growth target"
5328        );
5329        assert_eq!(high_no_peak, 2);
5330        assert_eq!(
5331            high_with_peak, 4,
5332            "peak should only widen the shrink boundary"
5333        );
5334    }
5335}
5336
5337#[cfg(test)]
5338mod xnuma_tests {
5339    use super::*;
5340
5341    // Default thresholds for tests
5342    const THRESH: (f64, f64) = (0.6, 0.7);
5343    const DELTA: (f64, f64) = (0.2, 0.3);
5344
5345    // =====================================================================
5346    // xnuma_check_active tests — per-(layer, node) two-threshold
5347    // =====================================================================
5348
5349    #[test]
5350    fn test_activation_below_threshold() {
5351        // Both nodes below threshold — both closed
5352        let duty = vec![40.0, 40.0];
5353        let allocs = vec![96, 96];
5354        let gd = vec![true, true];
5355        let cur = vec![false, false];
5356        let result = xnuma_check_active(&duty, &allocs, THRESH, DELTA, &gd, &cur);
5357        // ratio = 0.42 < 0.6 (low) → deactivate
5358        assert!(!result[0]);
5359        assert!(!result[1]);
5360    }
5361
5362    #[test]
5363    fn test_activation_above_all_thresholds() {
5364        // N0 overloaded + imbalanced + growth denied → open
5365        let duty = vec![90.0, 20.0];
5366        let allocs = vec![96, 96];
5367        let gd = vec![true, false];
5368        let cur = vec![false, false];
5369        let result = xnuma_check_active(&duty, &allocs, THRESH, DELTA, &gd, &cur);
5370        // N0: load=0.94>0.7, eq=110/192=0.573, surplus=90-55=35, ratio=0.365>0.3, gd=true → open
5371        assert!(result[0]);
5372        // N1: load=0.21<0.6 → closed
5373        assert!(!result[1]);
5374    }
5375
5376    #[test]
5377    fn test_activation_requires_growth_denied() {
5378        // N0 overloaded + imbalanced but growth NOT denied → closed
5379        let duty = vec![90.0, 20.0];
5380        let allocs = vec![96, 96];
5381        let gd = vec![false, false]; // growth succeeded
5382        let cur = vec![false, false];
5383        let result = xnuma_check_active(&duty, &allocs, THRESH, DELTA, &gd, &cur);
5384        assert!(!result[0]); // !growth_denied → deactivate
5385    }
5386
5387    #[test]
5388    fn test_symmetric_high_load_stays_closed() {
5389        // Both nodes above threshold but balanced (delta=0) → closed
5390        let duty = vec![80.0, 80.0];
5391        let allocs = vec![96, 96];
5392        let gd = vec![true, true];
5393        let cur = vec![false, false];
5394        let result = xnuma_check_active(&duty, &allocs, THRESH, DELTA, &gd, &cur);
5395        // load=0.83>0.7 but surplus=0, surplus_ratio=0 < 0.3 → no activation
5396        assert!(!result[0]);
5397        assert!(!result[1]);
5398    }
5399
5400    #[test]
5401    fn test_hysteresis_stays_active() {
5402        // N0 was active, now between thresholds → stays active
5403        let duty = vec![75.0, 20.0];
5404        let allocs = vec![96, 96];
5405        let gd = vec![true, false];
5406        let cur = vec![true, false]; // N0 was active
5407        let result = xnuma_check_active(&duty, &allocs, THRESH, DELTA, &gd, &cur);
5408        // N0: load=0.78 > 0.6(lo) and < 0.7(hi), surplus=(75-20)/2=27.5, ratio=0.286 > 0.2(lo)
5409        // gd=true. activate: 0.78<0.7 NO. deactivate: 0.78>0.6 NO, 0.286>0.2 NO, gd=true NO
5410        // → hysteresis preserves true
5411        assert!(result[0]);
5412    }
5413
5414    #[test]
5415    fn test_hysteresis_stays_inactive() {
5416        // N0 was inactive, in hysteresis band → stays inactive
5417        let duty = vec![75.0, 20.0];
5418        let allocs = vec![96, 96];
5419        let gd = vec![true, false];
5420        let cur = vec![false, false]; // N0 was inactive
5421        let result = xnuma_check_active(&duty, &allocs, THRESH, DELTA, &gd, &cur);
5422        // Same conditions but currently inactive → stays inactive
5423        assert!(!result[0]);
5424    }
5425
5426    #[test]
5427    fn test_deactivation_load_drops() {
5428        // N0 was active, load drops below low → closes
5429        let duty = vec![50.0, 50.0];
5430        let allocs = vec![96, 96];
5431        let gd = vec![true, true];
5432        let cur = vec![true, false];
5433        let result = xnuma_check_active(&duty, &allocs, THRESH, DELTA, &gd, &cur);
5434        // N0: load=0.52 < 0.6(lo) → deactivate
5435        assert!(!result[0]);
5436    }
5437
5438    #[test]
5439    fn test_deactivation_growth_succeeds() {
5440        // N0 was active, growth now succeeds → closes
5441        let duty = vec![90.0, 20.0];
5442        let allocs = vec![96, 96];
5443        let gd = vec![false, false]; // growth succeeded
5444        let cur = vec![true, false];
5445        let result = xnuma_check_active(&duty, &allocs, THRESH, DELTA, &gd, &cur);
5446        // !growth_denied → deactivate regardless of load/delta
5447        assert!(!result[0]);
5448    }
5449
5450    #[test]
5451    fn test_zero_alloc_with_duty_and_growth_denied() {
5452        // Zero alloc but duty > 0 and growth denied → open
5453        let duty = vec![50.0, 0.0];
5454        let allocs = vec![0, 96];
5455        let gd = vec![true, false];
5456        let cur = vec![false, false];
5457        let result = xnuma_check_active(&duty, &allocs, THRESH, DELTA, &gd, &cur);
5458        assert!(result[0]); // zero alloc + duty + gd → source
5459    }
5460
5461    #[test]
5462    fn test_all_zero_alloc() {
5463        let duty = vec![0.0, 0.0];
5464        let allocs = vec![0, 0];
5465        let gd = vec![true, true];
5466        let cur = vec![true, true];
5467        let result = xnuma_check_active(&duty, &allocs, THRESH, DELTA, &gd, &cur);
5468        assert!(!result[0]);
5469        assert!(!result[1]);
5470    }
5471
5472    #[test]
5473    fn test_three_nodes_mixed() {
5474        // 3 nodes: N0 overloaded+imbalanced+gd, N1 moderate, N2 low
5475        let duty = vec![90.0, 40.0, 10.0];
5476        let allocs = vec![96, 96, 96];
5477        let gd = vec![true, true, false];
5478        let cur = vec![false, false, false];
5479        let result = xnuma_check_active(&duty, &allocs, THRESH, DELTA, &gd, &cur);
5480        // eq_ratio = 140/288 ≈ 0.486
5481        // N0: load=0.94>0.7, surplus=90-46.7=43.3, ratio=0.451>0.3, gd=true → open
5482        assert!(result[0]);
5483        // N1: load=0.42<0.6 → deactivate
5484        assert!(!result[1]);
5485        // N2: load=0.10<0.6 → deactivate
5486        assert!(!result[2]);
5487    }
5488
5489    #[test]
5490    fn test_per_node_independence() {
5491        // N0 active, N1 deactivates independently
5492        let duty = vec![90.0, 50.0];
5493        let allocs = vec![96, 96];
5494        let gd = vec![true, true];
5495        let cur = vec![true, true];
5496        let result = xnuma_check_active(&duty, &allocs, THRESH, DELTA, &gd, &cur);
5497        // N0: load=0.94>0.7, eq=140/192=0.729, surplus=90-70=20, ratio=0.208>0.2(lo)
5498        //   activate: 0.94>0.7 YES, 0.208>0.3 NO → no activate
5499        //   deactivate: 0.94>0.6 NO, 0.208>0.2 NO, gd=true NO → no deactivate → preserve true
5500        assert!(result[0]);
5501        // N1: load=0.52<0.6 → deactivate
5502        assert!(!result[1]);
5503    }
5504
5505    // =====================================================================
5506    // xnuma_compute_rates tests — basic
5507    // =====================================================================
5508
5509    #[test]
5510    fn test_rates_balanced_load() {
5511        // Equal load per CPU on both nodes — no migration needed
5512        let duty = vec![48.0, 48.0];
5513        let allocs = vec![96, 96];
5514        let result = xnuma_compute_rates(&duty, &allocs);
5515
5516        // No surplus/deficit → all rates zero
5517        for src in 0..2 {
5518            for dst in 0..2 {
5519                assert_eq!(result.rates[src][dst], 0);
5520            }
5521        }
5522    }
5523
5524    #[test]
5525    fn test_rates_one_overloaded() {
5526        // N0 has 80 CPUs of duty, N1 has 40. Both have 96 CPUs.
5527        // eq_ratio = 120/192 = 0.625
5528        // N0 expected = 0.625 * 96 = 60, surplus = 80 - 60 = 20
5529        // N1 expected = 0.625 * 96 = 60, deficit = 60 - 40 = 20
5530        let duty = vec![80.0, 40.0];
5531        let allocs = vec![96, 96];
5532        let result = xnuma_compute_rates(&duty, &allocs);
5533
5534        // rate[0][1] should be positive (migrate from N0 to N1)
5535        assert!(result.rates[0][1] > 0);
5536        // rate[1][0] should be 0 (N1 has no surplus)
5537        assert_eq!(result.rates[1][0], 0);
5538        // Self-rates always 0
5539        assert_eq!(result.rates[0][0], 0);
5540        assert_eq!(result.rates[1][1], 0);
5541
5542        // Verify the rate magnitude: migration = 20.0 * DAMPEN, scaled
5543        let expected_rate = (20.0 * XNUMA_RATE_DAMPEN * DUTY_CYCLE_SCALE) as u64;
5544        assert_eq!(result.rates[0][1], expected_rate);
5545    }
5546
5547    #[test]
5548    fn test_rates_asymmetric_allocation() {
5549        // N0 has 48 CPUs, N1 has 144 CPUs. Duty 48 each.
5550        // eq_ratio = 96/192 = 0.5
5551        // N0 expected = 0.5 * 48 = 24, surplus = 48 - 24 = 24
5552        // N1 expected = 0.5 * 144 = 72, deficit = 72 - 48 = 24
5553        let duty = vec![48.0, 48.0];
5554        let allocs = vec![48, 144];
5555        let result = xnuma_compute_rates(&duty, &allocs);
5556
5557        let expected_rate = (24.0 * XNUMA_RATE_DAMPEN * DUTY_CYCLE_SCALE) as u64;
5558        assert_eq!(result.rates[0][1], expected_rate);
5559        assert_eq!(result.rates[1][0], 0);
5560    }
5561
5562    // =====================================================================
5563    // xnuma_compute_rates tests — multi-node
5564    // =====================================================================
5565
5566    #[test]
5567    fn test_rates_three_nodes_one_source() {
5568        // N0 overloaded, N1 and N2 are deficit
5569        // allocs: 96 each, duty: N0=120, N1=30, N2=30
5570        // eq_ratio = 180/288 = 0.625
5571        // N0 expected = 60, surplus = 60
5572        // N1 expected = 60, deficit = 30
5573        // N2 expected = 60, deficit = 30
5574        let duty = vec![120.0, 30.0, 30.0];
5575        let allocs = vec![96, 96, 96];
5576        let result = xnuma_compute_rates(&duty, &allocs);
5577
5578        // Total deficit = 60. N1 gets 30/60 = 50%, N2 gets 30/60 = 50%
5579        // rate[0][1] = 60 * 30/60 * DAMPEN = 15
5580        // rate[0][2] = 60 * 30/60 * DAMPEN = 15
5581        let rate_01 = (30.0 * XNUMA_RATE_DAMPEN * DUTY_CYCLE_SCALE) as u64;
5582        let rate_02 = (30.0 * XNUMA_RATE_DAMPEN * DUTY_CYCLE_SCALE) as u64;
5583        assert_eq!(result.rates[0][1], rate_01);
5584        assert_eq!(result.rates[0][2], rate_02);
5585
5586        // No reverse flow
5587        assert_eq!(result.rates[1][0], 0);
5588        assert_eq!(result.rates[2][0], 0);
5589        assert_eq!(result.rates[1][2], 0);
5590        assert_eq!(result.rates[2][1], 0);
5591    }
5592
5593    #[test]
5594    fn test_rates_three_nodes_unequal_deficit() {
5595        // N0 overloaded. N1 slight deficit, N2 large deficit.
5596        // allocs: 96 each, duty: N0=120, N1=50, N2=10
5597        // eq_ratio = 180/288 = 0.625
5598        // N0: expected=60, surplus=60
5599        // N1: expected=60, deficit=10
5600        // N2: expected=60, deficit=50
5601        let duty = vec![120.0, 50.0, 10.0];
5602        let allocs = vec![96, 96, 96];
5603        let result = xnuma_compute_rates(&duty, &allocs);
5604
5605        // Total deficit = 60. N1 share = 10/60, N2 share = 50/60
5606        // rate[0][1] = 60 * 10/60 * DAMPEN = 5
5607        // rate[0][2] = 60 * 50/60 * DAMPEN = 25
5608        let rate_01 = (10.0 * XNUMA_RATE_DAMPEN * DUTY_CYCLE_SCALE) as u64;
5609        let rate_02 = (50.0 * XNUMA_RATE_DAMPEN * DUTY_CYCLE_SCALE) as u64;
5610        assert_eq!(result.rates[0][1], rate_01);
5611        assert_eq!(result.rates[0][2], rate_02);
5612    }
5613
5614    #[test]
5615    fn test_rates_two_sources_one_sink() {
5616        // N0 and N1 overloaded, N2 deficit
5617        // allocs: 96 each, duty: N0=80, N1=70, N2=30
5618        // total = 180, eq_ratio = 180/288 = 0.625
5619        // N0: expected=60, surplus=20
5620        // N1: expected=60, surplus=10
5621        // N2: expected=60, deficit=30
5622        let duty = vec![80.0, 70.0, 30.0];
5623        let allocs = vec![96, 96, 96];
5624        let result = xnuma_compute_rates(&duty, &allocs);
5625
5626        // Total deficit = 30. Only N2 is deficit, so deficit share = 1.0
5627        // rate[0][2] = 20 * 1.0 * DAMPEN = 10
5628        // rate[1][2] = 10 * 1.0 * DAMPEN = 5
5629        let rate_02 = (20.0 * XNUMA_RATE_DAMPEN * DUTY_CYCLE_SCALE) as u64;
5630        let rate_12 = (10.0 * XNUMA_RATE_DAMPEN * DUTY_CYCLE_SCALE) as u64;
5631        assert_eq!(result.rates[0][2], rate_02);
5632        assert_eq!(result.rates[1][2], rate_12);
5633
5634        // No flow between sources
5635        assert_eq!(result.rates[0][1], 0);
5636        assert_eq!(result.rates[1][0], 0);
5637    }
5638
5639    // =====================================================================
5640    // Conservation invariants
5641    // =====================================================================
5642
5643    #[test]
5644    fn test_conservation_per_source_outbound() {
5645        // Each source's total outbound should equal its surplus * DAMPEN (scaled).
5646        // Total outbound from src = surplus[src] * DAMPEN * DUTY_CYCLE_SCALE
5647        let duty = vec![100.0, 30.0, 50.0, 20.0];
5648        let allocs = vec![96, 96, 96, 96];
5649        let nr = 4;
5650        let result = xnuma_compute_rates(&duty, &allocs);
5651
5652        let total_duty: f64 = duty.iter().sum();
5653        let total_alloc: f64 = allocs.iter().map(|&a| a as f64).sum();
5654        let eq_ratio = total_duty / total_alloc;
5655
5656        for src in 0..nr {
5657            let expected = eq_ratio * allocs[src] as f64;
5658            let surplus = (duty[src] - expected).max(0.0);
5659            let total_outbound: u64 = (0..nr).map(|dst| result.rates[src][dst]).sum();
5660            let expected_rate = (surplus * XNUMA_RATE_DAMPEN * DUTY_CYCLE_SCALE) as u64;
5661            assert_eq!(
5662                total_outbound, expected_rate,
5663                "node {} outbound mismatch",
5664                src
5665            );
5666        }
5667    }
5668
5669    #[test]
5670    fn test_conservation_surplus_equals_deficit() {
5671        // Mathematical invariant: total surplus == total deficit in water-fill
5672        let duty = [100.0, 30.0, 50.0];
5673        let allocs = [96, 96, 96];
5674
5675        let total_duty: f64 = duty.iter().sum();
5676        let total_alloc: f64 = allocs.iter().map(|&a| a as f64).sum();
5677        let eq_ratio = total_duty / total_alloc;
5678
5679        let mut total_surplus = 0.0f64;
5680        let mut total_deficit = 0.0f64;
5681        for i in 0..3 {
5682            let expected = eq_ratio * allocs[i] as f64;
5683            let delta = duty[i] - expected;
5684            if delta > 0.0 {
5685                total_surplus += delta;
5686            } else {
5687                total_deficit += -delta;
5688            }
5689        }
5690        assert!((total_surplus - total_deficit).abs() < 1e-10);
5691    }
5692
5693    #[test]
5694    fn test_self_rates_always_zero() {
5695        let duty = vec![100.0, 30.0, 50.0];
5696        let allocs = vec![96, 96, 96];
5697        let result = xnuma_compute_rates(&duty, &allocs);
5698
5699        for nid in 0..3 {
5700            assert_eq!(result.rates[nid][nid], 0);
5701        }
5702    }
5703
5704    #[test]
5705    fn test_deficit_nodes_have_zero_outbound() {
5706        // Deficit nodes should have zero outbound rates
5707        let duty = vec![100.0, 20.0, 30.0, 50.0];
5708        let allocs = vec![96, 96, 96, 96];
5709        let result = xnuma_compute_rates(&duty, &allocs);
5710
5711        // eq_ratio = 200/384 ≈ 0.521. N0: surplus=50, N3: surplus=0
5712        // N1: deficit=30, N2: deficit=20
5713        // Deficit nodes (outbound sum == 0) should have all zero rates
5714        for nid in 0..4 {
5715            let outbound: u64 = (0..4).map(|dst| result.rates[nid][dst]).sum();
5716            if outbound == 0 {
5717                for dst in 0..4 {
5718                    assert_eq!(
5719                        result.rates[nid][dst], 0,
5720                        "deficit node {} has non-zero rate to {}",
5721                        nid, dst
5722                    );
5723                }
5724            }
5725        }
5726    }
5727
5728    // =====================================================================
5729    // Edge cases
5730    // =====================================================================
5731
5732    #[test]
5733    fn test_rates_zero_duty_everywhere() {
5734        let duty = vec![0.0, 0.0];
5735        let allocs = vec![96, 96];
5736        let result = xnuma_compute_rates(&duty, &allocs);
5737
5738        for src in 0..2 {
5739            for dst in 0..2 {
5740                assert_eq!(result.rates[src][dst], 0);
5741            }
5742        }
5743    }
5744
5745    #[test]
5746    fn test_rates_zero_alloc() {
5747        let duty = vec![50.0, 50.0];
5748        let allocs = vec![0, 0];
5749        let result = xnuma_compute_rates(&duty, &allocs);
5750
5751        // total_alloc = 0 → early return with all zeros
5752        for src in 0..2 {
5753            for dst in 0..2 {
5754                assert_eq!(result.rates[src][dst], 0);
5755            }
5756        }
5757    }
5758
5759    #[test]
5760    fn test_rates_all_load_one_node() {
5761        // All duty on N0, nothing on N1
5762        // allocs: 96 each, duty: N0=96, N1=0
5763        // eq_ratio = 96/192 = 0.5
5764        // N0: expected=48, surplus=48
5765        // N1: expected=48, deficit=48
5766        let duty = vec![96.0, 0.0];
5767        let allocs = vec![96, 96];
5768        let result = xnuma_compute_rates(&duty, &allocs);
5769
5770        let expected_rate = (48.0 * XNUMA_RATE_DAMPEN * DUTY_CYCLE_SCALE) as u64;
5771        assert_eq!(result.rates[0][1], expected_rate);
5772    }
5773
5774    #[test]
5775    fn test_rates_single_node() {
5776        // Single node — balanced by definition
5777        let duty = vec![96.0];
5778        let allocs = vec![96];
5779        let result = xnuma_compute_rates(&duty, &allocs);
5780
5781        assert_eq!(result.rates[0][0], 0);
5782    }
5783
5784    #[test]
5785    fn test_rates_one_node_zero_alloc() {
5786        // N0 has allocation, N1 has zero
5787        // eq_ratio = 80/96
5788        // N0: expected=80, surplus=0 → balanced
5789        // N1: expected=0, deficit=0 → zero alloc skipped effectively
5790        let duty = vec![80.0, 0.0];
5791        let allocs = vec![96, 0];
5792        let result = xnuma_compute_rates(&duty, &allocs);
5793
5794        // N0: surplus = 80 - (80/96 * 96) = 0
5795        // N1: deficit = (80/96 * 0) - 0 = 0
5796        // All zeros — nothing to migrate
5797        assert_eq!(result.rates[0][1], 0);
5798        assert_eq!(result.rates[1][0], 0);
5799    }
5800
5801    // =====================================================================
5802    // Rate magnitude and scaling
5803    // =====================================================================
5804
5805    #[test]
5806    fn test_rate_scaling() {
5807        // Verify rates are in DUTY_CYCLE_SCALE units
5808        let duty = vec![80.0, 40.0];
5809        let allocs = vec![96, 96];
5810        let result = xnuma_compute_rates(&duty, &allocs);
5811
5812        // surplus = 20, deficit = 20 → migration = 20 * DAMPEN = 10
5813        // rate = 10 * (1 << 20)
5814        let expected = (20.0 * XNUMA_RATE_DAMPEN * DUTY_CYCLE_SCALE) as u64;
5815        assert_eq!(result.rates[0][1], expected);
5816        assert_eq!(expected, 10 * (1 << 20));
5817    }
5818
5819    #[test]
5820    fn test_rates_tiny_imbalance() {
5821        // Very small imbalance — should still produce non-zero rate
5822        let duty = vec![48.001, 47.999];
5823        let allocs = vec![96, 96];
5824        let result = xnuma_compute_rates(&duty, &allocs);
5825
5826        // surplus ≈ 0.001, rate ≈ 0.001 * 2^20 ≈ 1048
5827        assert!(result.rates[0][1] > 0);
5828        assert!(result.rates[0][1] < (1 << 20)); // Less than 1.0 CPU worth
5829    }
5830
5831    #[test]
5832    fn test_rates_large_values() {
5833        // Large system: 8 nodes, 96 CPUs each
5834        let duty = vec![300.0, 100.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0];
5835        let allocs = vec![96, 96, 96, 96, 96, 96, 96, 96];
5836        let result = xnuma_compute_rates(&duty, &allocs);
5837
5838        // eq_ratio = 700/768 ≈ 0.911. N0 surplus=212.5, N1 surplus=12.5
5839        // N2-N7 deficit=37.5 each. N0 and N1 have surplus.
5840        assert!(result.rates[0][2] > 0); // N0 → N2 (deficit node)
5841        assert!(result.rates[1][2] > 0); // N1 → N2 (N1 also has surplus)
5842
5843        // Verify conservation: per-source outbound ≈ surplus * DAMPEN (within
5844        // truncation tolerance — each `as u64` can lose up to 1 per cell)
5845        let total_duty: f64 = duty.iter().sum();
5846        let total_alloc: f64 = allocs.iter().map(|&a| a as f64).sum();
5847        let eq_ratio = total_duty / total_alloc;
5848        let nr = 8;
5849        for src in 0..nr {
5850            let surplus = (duty[src] - eq_ratio * allocs[src] as f64).max(0.0);
5851            let outbound: u64 = (0..nr).map(|dst| result.rates[src][dst]).sum();
5852            let expected = (surplus * XNUMA_RATE_DAMPEN * DUTY_CYCLE_SCALE) as u64;
5853            let tolerance = nr as u64; // up to 1 per destination cell
5854            assert!(
5855                outbound.abs_diff(expected) <= tolerance,
5856                "node {} outbound {} vs expected {}, diff {}",
5857                src,
5858                outbound,
5859                expected,
5860                outbound.abs_diff(expected)
5861            );
5862        }
5863    }
5864
5865    // =====================================================================
5866    // Hysteresis integration
5867    // =====================================================================
5868
5869    #[test]
5870    fn test_hysteresis_cycle() {
5871        let allocs = vec![96, 96];
5872        let gd = vec![true, false]; // N0 growth denied
5873
5874        // Start: all closed, low load
5875        let active =
5876            xnuma_check_active(&[40.0, 40.0], &allocs, THRESH, DELTA, &gd, &[false, false]);
5877        assert!(!active[0]); // load 0.42 < 0.6(lo) → deactivate
5878
5879        // N0 overloaded + imbalanced + gd → opens
5880        let active =
5881            xnuma_check_active(&[90.0, 20.0], &allocs, THRESH, DELTA, &gd, &[false, false]);
5882        assert!(active[0]); // load 0.94>0.7, surplus=35, ratio=0.365>0.3, gd=true
5883
5884        // Load decreases but in hysteresis band → stays open
5885        let active = xnuma_check_active(&[75.0, 20.0], &allocs, THRESH, DELTA, &gd, &[true, false]);
5886        assert!(active[0]); // load 0.78 between 0.6 and 0.7, surplus ok, gd → preserve
5887
5888        // Load drops below low threshold → closes
5889        let active = xnuma_check_active(&[50.0, 50.0], &allocs, THRESH, DELTA, &gd, &[true, false]);
5890        assert!(!active[0]); // load 0.52 < 0.6(lo) → deactivate
5891    }
5892
5893    #[test]
5894    fn test_hysteresis_growth_toggle() {
5895        // N0 was open, then growth succeeds → closes
5896        let allocs = vec![96, 96];
5897        let gd_denied = vec![true, false];
5898        let gd_ok = vec![false, false];
5899
5900        // Active with growth denied
5901        let active = xnuma_check_active(
5902            &[90.0, 20.0],
5903            &allocs,
5904            THRESH,
5905            DELTA,
5906            &gd_denied,
5907            &[false, false],
5908        );
5909        assert!(active[0]);
5910
5911        // Growth succeeds → !gd → deactivate
5912        let active = xnuma_check_active(
5913            &[90.0, 20.0],
5914            &allocs,
5915            THRESH,
5916            DELTA,
5917            &gd_ok,
5918            &[true, false],
5919        );
5920        assert!(!active[0]); // !growth_denied → deactivate
5921    }
5922
5923    // =====================================================================
5924    // Proportional distribution to multiple sinks
5925    // =====================================================================
5926
5927    #[test]
5928    fn test_proportional_sink_distribution() {
5929        // 4 nodes: N0 source, N1-N3 sinks with different deficits
5930        // allocs: 96 each, duty: N0=180, N1=20, N2=40, N3=0
5931        // total = 240, eq_ratio = 240/384 = 0.625
5932        // N0: expected=60, surplus=120
5933        // N1: expected=60, deficit=40
5934        // N2: expected=60, deficit=20
5935        // N3: expected=60, deficit=60
5936        // total_deficit = 120
5937        let duty = vec![180.0, 20.0, 40.0, 0.0];
5938        let allocs = vec![96, 96, 96, 96];
5939        let result = xnuma_compute_rates(&duty, &allocs);
5940
5941        // rate[0][1] = 120 * 40/120 * DAMPEN = 20
5942        // rate[0][2] = 120 * 20/120 * DAMPEN = 10
5943        // rate[0][3] = 120 * 60/120 * DAMPEN = 30
5944        assert_eq!(
5945            result.rates[0][1],
5946            (40.0 * XNUMA_RATE_DAMPEN * DUTY_CYCLE_SCALE) as u64
5947        );
5948        assert_eq!(
5949            result.rates[0][2],
5950            (20.0 * XNUMA_RATE_DAMPEN * DUTY_CYCLE_SCALE) as u64
5951        );
5952        assert_eq!(
5953            result.rates[0][3],
5954            (60.0 * XNUMA_RATE_DAMPEN * DUTY_CYCLE_SCALE) as u64
5955        );
5956
5957        // Total outbound from N0 = 20 + 10 + 30 = 60 (half of surplus, dampened)
5958        let total_from_n0: u64 = (0..4).map(|dst| result.rates[0][dst]).sum();
5959        assert_eq!(
5960            total_from_n0,
5961            (120.0 * XNUMA_RATE_DAMPEN * DUTY_CYCLE_SCALE) as u64
5962        );
5963    }
5964}
5965
5966#[cfg(test)]
5967mod util_compensation_tests {
5968    /// Compute the scale factor: s = Δt / (Δt - irq - softirq - stolen)
5969    fn compute_scale(delta_total: u64, overhead: u64) -> f64 {
5970        let available = delta_total.saturating_sub(overhead);
5971        if available > 0 {
5972            (delta_total as f64 / available as f64).clamp(1.0, 20.0)
5973        } else {
5974            1.0
5975        }
5976    }
5977
5978    /// Simulate the per-CPU-scaled aggregation that refresh() does.
5979    fn scaled_aggregate(
5980        deltas: &[Vec<u64>],
5981        scales: &[f64],
5982        nr_layers: usize,
5983        elapsed: f64,
5984    ) -> Vec<f64> {
5985        (0..nr_layers)
5986            .map(|layer| {
5987                let mut sum = 0.0f64;
5988                for (cpu, cpu_deltas) in deltas.iter().enumerate() {
5989                    sum += cpu_deltas[layer] as f64 * scales[cpu];
5990                }
5991                sum / 1_000_000_000.0 / elapsed
5992            })
5993            .collect()
5994    }
5995
5996    #[test]
5997    fn test_scale_no_overhead() {
5998        // No irq/softirq/stolen → scale = 1.0
5999        assert!((compute_scale(1000, 0) - 1.0).abs() < 0.01);
6000    }
6001
6002    #[test]
6003    fn test_scale_half_overhead() {
6004        // 50% overhead → s = 1000/500 = 2.0
6005        assert!((compute_scale(1000, 500) - 2.0).abs() < 0.01);
6006    }
6007
6008    #[test]
6009    fn test_scale_high_overhead() {
6010        // 90% overhead → s = 1000/100 = 10.0
6011        assert!((compute_scale(1000, 900) - 10.0).abs() < 0.01);
6012    }
6013
6014    #[test]
6015    fn test_scale_very_high_overhead() {
6016        // 95% overhead → s = 1000/50 = 20.0 (at clamp boundary)
6017        assert!((compute_scale(1000, 950) - 20.0).abs() < 0.01);
6018    }
6019
6020    #[test]
6021    fn test_scale_clamped_at_max() {
6022        // 98% overhead → ratio = 50.0, clamped to 20.0
6023        assert!((compute_scale(1000, 980) - 20.0).abs() < 0.01);
6024    }
6025
6026    #[test]
6027    fn test_scale_all_overhead() {
6028        // 100% overhead → available = 0, returns 1.0
6029        assert!((compute_scale(1000, 1000) - 1.0).abs() < 0.01);
6030    }
6031
6032    #[test]
6033    fn test_scale_idle_cpu() {
6034        // Idle CPU: delta_total = 0 → returns 1.0
6035        assert!((compute_scale(0, 0) - 1.0).abs() < 0.01);
6036    }
6037
6038    #[test]
6039    fn test_scale_small_overhead_mostly_idle() {
6040        // 1% softirq + 1% user + 98% idle = total 10000, overhead 100
6041        // s = 10000 / 9900 ≈ 1.01 — nearly 1.0, not pathological
6042        let s = compute_scale(10000, 100);
6043        assert!((s - 1.01).abs() < 0.01, "expected ~1.01, got {}", s);
6044    }
6045
6046    #[test]
6047    fn test_uniform_scale_matches_unscaled() {
6048        let deltas = vec![vec![1_000_000_000u64; 2]; 4];
6049        let scales = vec![1.0; 4];
6050        let result = scaled_aggregate(&deltas, &scales, 2, 1.0);
6051        assert!((result[0] - 4.0).abs() < 0.01);
6052        assert!((result[1] - 4.0).abs() < 0.01);
6053    }
6054
6055    #[test]
6056    fn test_hot_cpu_weighted_more() {
6057        // CPU 0: scale=2.0, CPU 1: scale=1.0
6058        // Layer 0: 900ms on CPU 0, 100ms on CPU 1
6059        // Compensated: 900*2.0 + 100*1.0 = 1900ms = 1.9
6060        let deltas = vec![vec![900_000_000, 0], vec![100_000_000, 0]];
6061        let scales = vec![2.0, 1.0];
6062        let result = scaled_aggregate(&deltas, &scales, 2, 1.0);
6063        assert!(
6064            (result[0] - 1.9).abs() < 0.01,
6065            "expected 1.9, got {}",
6066            result[0]
6067        );
6068    }
6069
6070    #[test]
6071    fn test_cold_cpu_weighted_less() {
6072        let deltas = vec![vec![100_000_000, 0], vec![900_000_000, 0]];
6073        let scales = vec![2.0, 1.0];
6074        let result = scaled_aggregate(&deltas, &scales, 2, 1.0);
6075        assert!(
6076            (result[0] - 1.1).abs() < 0.01,
6077            "expected 1.1, got {}",
6078            result[0]
6079        );
6080    }
6081
6082    #[test]
6083    fn test_no_usage_no_compensation() {
6084        let deltas = vec![vec![0u64; 2]; 4];
6085        let scales = vec![5.0; 4];
6086        let result = scaled_aggregate(&deltas, &scales, 2, 1.0);
6087        assert_eq!(result[0], 0.0);
6088        assert_eq!(result[1], 0.0);
6089    }
6090
6091    #[test]
6092    fn test_multilayer_independent_scaling() {
6093        let deltas = vec![
6094            vec![800_000_000, 200_000_000],
6095            vec![200_000_000, 800_000_000],
6096        ];
6097        let scales = vec![3.0, 1.0];
6098        let result = scaled_aggregate(&deltas, &scales, 2, 1.0);
6099        assert!((result[0] - 2.6).abs() < 0.01);
6100        assert!((result[1] - 1.4).abs() < 0.01);
6101    }
6102
6103    #[test]
6104    fn test_elapsed_time_normalization() {
6105        let deltas = vec![vec![500_000_000u64; 1]; 1];
6106        let scales = vec![1.0];
6107        let result = scaled_aggregate(&deltas, &scales, 1, 2.0);
6108        assert!((result[0] - 0.25).abs() < 0.01);
6109    }
6110
6111    #[test]
6112    fn test_many_cpus_mixed_scales() {
6113        let deltas = vec![vec![1_000_000_000u64; 1]; 8];
6114        let scales = vec![2.5, 1.0, 2.5, 1.0, 2.5, 1.0, 2.5, 1.0];
6115        let result = scaled_aggregate(&deltas, &scales, 1, 1.0);
6116        assert!((result[0] - 14.0).abs() < 0.01);
6117    }
6118
6119    #[test]
6120    fn test_compensated_ge_raw() {
6121        // Since scale >= 1.0 always, compensated >= raw for any input.
6122        let deltas = vec![
6123            vec![500_000_000u64; 3],
6124            vec![300_000_000; 3],
6125            vec![200_000_000; 3],
6126        ];
6127        let scales_raw = vec![1.0; 3];
6128        let scales_comp = vec![1.5, 2.0, 1.0];
6129        let raw = scaled_aggregate(&deltas, &scales_raw, 3, 1.0);
6130        let comp = scaled_aggregate(&deltas, &scales_comp, 3, 1.0);
6131        for i in 0..3 {
6132            assert!(
6133                comp[i] >= raw[i] - 0.001,
6134                "layer {}: comp {} < raw {}",
6135                i,
6136                comp[i],
6137                raw[i]
6138            );
6139        }
6140    }
6141}