Skip to main content

scx_forge/
main.rs

1// SPDX-License-Identifier: GPL-2.0
2//
3// Copyright (c) 2026 Andrea Righi <arighi@nvidia.com>
4
5// This software may be used and distributed according to the terms of the
6// GNU General Public License version 2.
7
8mod bpf_skel;
9pub use bpf_skel::*;
10pub mod bpf_intf;
11pub use bpf_intf::*;
12
13mod stats;
14use std::collections::HashSet;
15use std::ffi::c_int;
16use std::mem::MaybeUninit;
17use std::sync::atomic::AtomicBool;
18use std::sync::atomic::Ordering;
19use std::sync::Arc;
20use std::time::Duration;
21
22use anyhow::bail;
23use anyhow::Context;
24use anyhow::Result;
25use clap::Parser;
26use crossbeam::channel::RecvTimeoutError;
27use libbpf_rs::MapCore;
28use libbpf_rs::MapFlags;
29use libbpf_rs::OpenObject;
30use libbpf_rs::ProgramInput;
31use log::warn;
32use log::{debug, info};
33use scx_stats::prelude::*;
34use scx_utils::build_id;
35use scx_utils::compat;
36use scx_utils::get_primary_cpus;
37use scx_utils::libbpf_clap_opts::LibbpfOpts;
38use scx_utils::perf::parse_perf_event;
39use scx_utils::perf::setup_perf_events;
40use scx_utils::perf::PerfEventSpec;
41use scx_utils::scx_ops_attach;
42use scx_utils::scx_ops_load;
43use scx_utils::scx_ops_open;
44use scx_utils::try_set_rlimit_infinity;
45use scx_utils::uei_exited;
46use scx_utils::uei_report;
47use scx_utils::CoreType;
48use scx_utils::Cpumask;
49use scx_utils::Powermode;
50use scx_utils::Topology;
51use scx_utils::UserExitInfo;
52use scx_utils::NR_CPUS_POSSIBLE;
53use scx_utils::NR_CPU_IDS;
54use stats::Metrics;
55
56const SCHEDULER_NAME: &str = "scx_forge";
57const FORGE_MAX_TOPO_DOMAINS_USIZE: usize = 4096;
58const FORGE_MAX_TOPO_DISTANCES_USIZE: usize = 65536;
59const FORGE_TOPO_CPUMASK_WORDS_USIZE: usize = FORGE_MAX_TOPO_DOMAINS_USIZE / 64;
60
61/// Topology level of the user-created DSQs. Each variant maps to a value of
62/// `enum topology_dsq_type` from src/bpf/intf.h (the single source of truth),
63/// so the two cannot drift.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
65enum DsqTopology {
66    /// Per-CPU DSQs.
67    Cpu,
68    /// Per-LLC DSQs.
69    Llc,
70    /// Per-node DSQs.
71    Node,
72    /// Single shared DSQ.
73    Global,
74}
75
76impl DsqTopology {
77    /// Return the matching `enum topology_dsq_type` value from the BPF intf.
78    fn to_bpf(self) -> u32 {
79        match self {
80            DsqTopology::Cpu => bpf_intf::topology_dsq_type_TOPO_DSQ_CPU,
81            DsqTopology::Llc => bpf_intf::topology_dsq_type_TOPO_DSQ_LLC,
82            DsqTopology::Node => bpf_intf::topology_dsq_type_TOPO_DSQ_NODE,
83            DsqTopology::Global => bpf_intf::topology_dsq_type_TOPO_DSQ_GLOBAL,
84        }
85    }
86}
87
88/// Ordering algorithm for the queue key of vtime-ordered DSQs. Each variant
89/// maps to a value of `enum ordering_type` from src/bpf/intf.h.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
91enum QueueOrdering {
92    /// Virtual-runtime (CFS-like) fair ordering.
93    Vruntime,
94    /// Earliest-deadline-first (weighted).
95    Deadline,
96    /// First-in-first-out (by enqueue time).
97    Fifo,
98}
99
100impl QueueOrdering {
101    /// Return the matching `enum ordering_type` value from the BPF intf.
102    fn to_bpf(self) -> u32 {
103        match self {
104            QueueOrdering::Vruntime => bpf_intf::ordering_type_ORDER_VRUNTIME,
105            QueueOrdering::Deadline => bpf_intf::ordering_type_ORDER_DEADLINE,
106            QueueOrdering::Fifo => bpf_intf::ordering_type_ORDER_FIFO,
107        }
108    }
109}
110
111/// Wakeup idle-CPU selection policy. Each variant maps to a value of
112/// `enum idle_policy_type` from src/bpf/intf.h.
113#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
114enum IdlePolicy {
115    /// Capacity-aware waker preference (performance oriented).
116    Capacity,
117    /// Keep the wakee on its previous CPU (cache affinity).
118    Wakee,
119    /// Move the wakee toward the waker CPU (wakeup locality).
120    Waker,
121    /// Move the wakee toward the waker CPU only when both are threads of the
122    /// same task (multi-thread locality).
123    Thread,
124    /// Keep the wakee on its previous CPU, even if system is busy (tail-latency).
125    Sticky,
126}
127
128impl IdlePolicy {
129    /// Return the matching `enum idle_policy_type` value from the BPF intf.
130    fn to_bpf(self) -> u32 {
131        match self {
132            IdlePolicy::Capacity => bpf_intf::idle_policy_type_IDLE_CAPACITY,
133            IdlePolicy::Wakee => bpf_intf::idle_policy_type_IDLE_WAKEE,
134            IdlePolicy::Waker => bpf_intf::idle_policy_type_IDLE_WAKER,
135            IdlePolicy::Thread => bpf_intf::idle_policy_type_IDLE_THREAD,
136            IdlePolicy::Sticky => bpf_intf::idle_policy_type_IDLE_STICKY,
137        }
138    }
139}
140
141#[derive(Debug, Parser)]
142struct Opts {
143    /// Maximum scheduling slice duration in microseconds.
144    #[clap(short = 's', long, default_value = "1000")]
145    slice_us: u64,
146
147    /// Topology level of the user-created DSQs.
148    #[clap(long, value_enum, default_value_t = DsqTopology::Global)]
149    dsq_topology: DsqTopology,
150
151    /// Ordering algorithm for the queue key of vtime-ordered DSQs.
152    #[clap(long, value_enum, default_value_t = QueueOrdering::Vruntime)]
153    ordering: QueueOrdering,
154
155    /// Wakeup idle-CPU selection policy.
156    #[clap(long, value_enum, default_value_t = IdlePolicy::Wakee)]
157    idle_policy: IdlePolicy,
158
159    /// Disable synchronous-wakeup bias during idle CPU selection.
160    #[clap(short = 'w', long, action = clap::ArgAction::SetTrue)]
161    no_wake_sync: bool,
162
163    /// Specifies a list of CPUs to prioritize.
164    ///
165    /// Accepts a comma-separated list of CPUs or ranges (i.e., 0-3,12-15) or the following special
166    /// keywords:
167    ///
168    /// "performance" = automatically detect and prioritize the fastest CPUs,
169    /// "powersave" = automatically detect and prioritize the slowest CPUs,
170    /// "all" = all CPUs assigned to the primary domain.
171    ///
172    /// By default "all" CPUs are used.
173    #[clap(short = 'm', long)]
174    primary_domain: Option<String>,
175
176    /// Enable preempting the running task when an eligible sleeper wakes up on
177    /// its CPU.
178    #[clap(short = 'p', long, action = clap::ArgAction::SetTrue)]
179    enable_preemption: bool,
180
181    /// Hardware/software perf event to monitor for the event-heavy migration
182    /// hint (0x0 = disabled). Accepts hex (0xN) or a symbolic name (e.g.
183    /// cache-misses, LLC-load-misses, page-faults, branch-misses).
184    #[clap(short = 'e', long, default_value = "0x0", value_parser = parse_perf_event)]
185    perf_config: PerfEventSpec,
186
187    /// Threshold (events accumulated over a slice) above which a task is
188    /// classified as event-heavy and biased toward migrating to an idle CPU.
189    /// The behavior is gated on --perf-config.
190    #[clap(short = 'E', long, default_value = "1000")]
191    perf_threshold: u64,
192
193    /// Sticky perf event (0x0 = disabled). When a task exceeds
194    /// --perf-sticky-threshold for this event, keep it on its previous CPU.
195    /// Accepts hex (0xN) or a symbolic name.
196    #[clap(short = 'y', long, default_value = "0x0", value_parser = parse_perf_event)]
197    perf_sticky: PerfEventSpec,
198
199    /// Sticky perf threshold; a task is kept on its previous CPU when its count
200    /// for the --perf-sticky event exceeds this. The behavior is gated on
201    /// --perf-sticky.
202    #[clap(short = 'Y', long, default_value = "1000")]
203    perf_sticky_threshold: u64,
204
205    /// Exit debug dump buffer length. 0 indicates default.
206    #[clap(long, default_value = "0")]
207    exit_dump_len: u32,
208
209    /// Enable stats monitoring with the specified interval.
210    #[clap(long)]
211    stats: Option<f64>,
212
213    /// Run in stats monitoring mode with the specified interval. Scheduler
214    /// is not launched.
215    #[clap(long)]
216    monitor: Option<f64>,
217
218    /// Enable BPF debugging via /sys/kernel/tracing/trace_pipe.
219    #[clap(short = 'd', long, action = clap::ArgAction::SetTrue)]
220    debug: bool,
221
222    /// Enable verbose output, including libbpf details.
223    #[clap(short = 'v', long, action = clap::ArgAction::SetTrue)]
224    verbose: bool,
225
226    /// Print scheduler version and exit.
227    #[clap(short = 'V', long, action = clap::ArgAction::SetTrue)]
228    version: bool,
229
230    /// Show descriptions for statistics.
231    #[clap(long)]
232    help_stats: bool,
233
234    #[clap(flatten, next_help_heading = "Libbpf Options")]
235    pub libbpf: LibbpfOpts,
236}
237
238pub fn parse_cpu_list(optarg: &str) -> Result<Vec<usize>, String> {
239    let mut cpus = Vec::new();
240    let mut seen = HashSet::new();
241
242    // Handle special keywords.
243    if let Some(mode) = match optarg {
244        "powersave" => Some(Powermode::Powersave),
245        "performance" => Some(Powermode::Performance),
246        "all" => Some(Powermode::Any),
247        _ => None,
248    } {
249        return get_primary_cpus(mode).map_err(|e| e.to_string());
250    }
251
252    // Validate input characters.
253    if optarg
254        .chars()
255        .any(|c| !c.is_ascii_digit() && c != '-' && c != ',' && !c.is_whitespace())
256    {
257        return Err("Invalid character in CPU list".to_string());
258    }
259
260    let cleaned = optarg.replace(' ', "\t");
261
262    for token in cleaned.split(',') {
263        let token = token.trim_matches(|c: char| c.is_whitespace());
264
265        if token.is_empty() {
266            continue;
267        }
268
269        if let Some((start_str, end_str)) = token.split_once('-') {
270            let start = start_str
271                .trim()
272                .parse::<usize>()
273                .map_err(|_| "Invalid range start")?;
274            let end = end_str
275                .trim()
276                .parse::<usize>()
277                .map_err(|_| "Invalid range end")?;
278
279            if start > end {
280                return Err(format!("Invalid CPU range: {}-{}", start, end));
281            }
282
283            for i in start..=end {
284                if cpus.len() >= *NR_CPU_IDS {
285                    return Err(format!("Too many CPUs specified (max {})", *NR_CPU_IDS));
286                }
287                if seen.insert(i) {
288                    cpus.push(i);
289                }
290            }
291        } else {
292            let cpu = token
293                .parse::<usize>()
294                .map_err(|_| format!("Invalid CPU: {}", token))?;
295            if cpus.len() >= *NR_CPU_IDS {
296                return Err(format!("Too many CPUs specified (max {})", *NR_CPU_IDS));
297            }
298            if seen.insert(cpu) {
299                cpus.push(cpu);
300            }
301        }
302    }
303
304    Ok(cpus)
305}
306
307struct Scheduler<'a> {
308    skel: BpfSkel<'a>,
309    struct_ops: Option<libbpf_rs::Link>,
310    stats_server: StatsServer<(), Metrics>,
311    user_restart: bool,
312}
313
314impl<'a> Scheduler<'a> {
315    fn init(opts: &'a Opts, open_object: &'a mut MaybeUninit<OpenObject>) -> Result<Self> {
316        try_set_rlimit_infinity();
317
318        info!(
319            "{} {}",
320            SCHEDULER_NAME,
321            build_id::full_version(env!("CARGO_PKG_VERSION")),
322        );
323
324        // Initialize CPU topology.
325        let topo = Topology::new().unwrap();
326        if *NR_CPU_IDS > FORGE_TOPO_CPUMASK_WORDS_USIZE * 64 {
327            bail!(
328                "CPU ID space {} exceeds scx_forge topology map limit {}",
329                *NR_CPU_IDS,
330                FORGE_TOPO_CPUMASK_WORDS_USIZE * 64
331            );
332        }
333
334        // Check host topology to determine if we need to enable SMT capabilities.
335        let smt_enabled = topo.smt_enabled;
336        if !smt_enabled {
337            info!("Disabling SMT optimizations");
338        }
339
340        // Determine the amount of non-empty NUMA nodes in the system.
341        let nr_nodes = topo
342            .nodes
343            .values()
344            .filter(|node| !node.all_cpus.is_empty())
345            .count();
346        info!("NUMA nodes: {}", nr_nodes);
347
348        // Print command line.
349        info!(
350            "scheduler options: {}",
351            std::env::args().collect::<Vec<_>>().join(" ")
352        );
353
354        // Initialize BPF connector.
355        let mut skel_builder = BpfSkelBuilder::default();
356        skel_builder.obj_builder.debug(opts.verbose);
357        let open_opts = opts.libbpf.clone().into_bpf_open_opts();
358        let mut skel = scx_ops_open!(skel_builder, open_object, forge_ops, open_opts)?;
359
360        if !compat::struct_has_field("sched_ext_ops", "cgroup_init").unwrap_or(false) {
361            warn!("kernel doesn't support ops.cgroup_init(), disabling");
362            skel.struct_ops.forge_ops_mut().cgroup_init = std::ptr::null_mut();
363        }
364        if !compat::struct_has_field("sched_ext_ops", "cgroup_exit").unwrap_or(false) {
365            warn!("kernel doesn't support ops.cgroup_exit(), disabling");
366            skel.struct_ops.forge_ops_mut().cgroup_exit = std::ptr::null_mut();
367        }
368        if !compat::struct_has_field("sched_ext_ops", "cgroup_set_weight").unwrap_or(false) {
369            warn!("kernel doesn't support ops.cgroup_set_weight(), disabling");
370            skel.struct_ops.forge_ops_mut().cgroup_set_weight = std::ptr::null_mut();
371        }
372        if !compat::struct_has_field("sched_ext_ops", "cgroup_move").unwrap_or(false) {
373            warn!("kernel doesn't support ops.cgroup_move(), disabling");
374            skel.struct_ops.forge_ops_mut().cgroup_move = std::ptr::null_mut();
375        }
376
377        skel.struct_ops.forge_ops_mut().exit_dump_len = opts.exit_dump_len;
378
379        // Override default BPF scheduling parameters.
380        let rodata = skel.maps.rodata_data.as_mut().unwrap();
381        rodata.slice_ns = opts.slice_us * 1000;
382        rodata.debug = opts.debug;
383        rodata.smt_enabled = smt_enabled;
384        rodata.topo_dsq = opts.dsq_topology.to_bpf();
385        rodata.ordering = opts.ordering.to_bpf();
386        rodata.idle_policy = opts.idle_policy.to_bpf();
387        rodata.no_wake_sync = opts.no_wake_sync;
388        rodata.preemption = opts.enable_preemption;
389
390        // PMU event monitoring. The event ids select which counters the BPF
391        // side reads; the thresholds (0 = behavior disabled, tracking only)
392        // gate the event-heavy migration and sticky placement hints.
393        rodata.perf_config = opts.perf_config.event_id;
394        rodata.perf_threshold = opts.perf_threshold;
395        rodata.perf_sticky = opts.perf_sticky.event_id;
396        rodata.perf_sticky_threshold = opts.perf_sticky_threshold;
397
398        // Define the primary scheduling domain.
399        let primary_cpus = if let Some(ref domain) = opts.primary_domain {
400            match parse_cpu_list(domain) {
401                Ok(cpus) => cpus,
402                Err(e) => bail!("Error parsing primary domain: {}", e),
403            }
404        } else {
405            (0..*NR_CPU_IDS).collect()
406        };
407        if primary_cpus.len() < *NR_CPU_IDS {
408            info!("Primary CPUs: {:?}", primary_cpus);
409            rodata.primary_all = false;
410        } else {
411            rodata.primary_all = true;
412        }
413
414        // Normalize CPU capacities to 1..1024 so the highest capacity is always 1024.
415        let cpus: Vec<_> = topo.all_cpus.values().collect();
416        let max_cap = cpus
417            .iter()
418            .map(|cpu| cpu.cpu_capacity)
419            .max()
420            .unwrap_or(1)
421            .max(1);
422        let cpu_capacities: std::collections::HashMap<_, _> = cpus
423            .iter()
424            .map(|cpu| {
425                let normalized = (cpu.cpu_capacity * 1024 / max_cap).clamp(1, 1024);
426                (cpu.id, normalized as u64)
427            })
428            .collect();
429        rodata.all_cpus_same_capacity = cpus.iter().all(|cpu| cpu.cpu_capacity == max_cap);
430
431        // Build dense LLC index for topology lookup maps.
432        let mut llc_id_to_dense = std::collections::HashMap::new();
433        for (dense_id, (_, llc)) in topo.all_llcs.iter().enumerate() {
434            llc_id_to_dense.insert(llc.id, dense_id);
435        }
436
437        // Set scheduler flags.
438        skel.struct_ops.forge_ops_mut().flags = *compat::SCX_OPS_ENQ_EXITING
439            | *compat::SCX_OPS_ENQ_LAST
440            | *compat::SCX_OPS_ENQ_MIGRATION_DISABLED
441            | *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP
442            | *compat::SCX_OPS_BUILTIN_IDLE_PER_NODE;
443        info!(
444            "scheduler flags: {:#x}",
445            skel.struct_ops.forge_ops_mut().flags
446        );
447
448        // Load the BPF program for validation.
449        let mut skel = scx_ops_load!(skel, forge_ops, uei)?;
450
451        Self::init_topology_maps(
452            &mut skel,
453            &topo,
454            &llc_id_to_dense,
455            &cpu_capacities,
456            smt_enabled,
457        )?;
458
459        // Initialize SMT sibling cpumasks (populate per-CPU smt via enable_sibling_cpu prog).
460        if smt_enabled {
461            Self::init_smt_domains(&mut skel, &topo)?;
462        }
463
464        // Enable primary scheduling domain, if defined.
465        if primary_cpus.len() < *NR_CPU_IDS {
466            for cpu in primary_cpus {
467                if let Err(err) = Self::enable_primary_cpu(&mut skel, cpu as i32) {
468                    bail!("failed to add CPU {} to primary domain: error {}", cpu, err);
469                }
470            }
471        }
472
473        // Configure CPU topology mappings (must be done after skeleton is loaded).
474        for cpu in topo.all_cpus.values() {
475            let dense_llc = llc_id_to_dense.get(&cpu.llc_id).copied().unwrap_or(0);
476            if opts.verbose {
477                let cap = cpu_capacities.get(&cpu.id).copied().unwrap_or(0);
478                info!(
479                    "CPU{} -> node{} LLC{} (cpu capacity: {})",
480                    cpu.id, cpu.node_id, dense_llc, cap
481                );
482            }
483            skel.maps.cpu_node_map.update(
484                &(cpu.id as u32).to_ne_bytes(),
485                &(cpu.node_id as u32).to_ne_bytes(),
486                MapFlags::ANY,
487            )?;
488            skel.maps.cpu_llc_map.update(
489                &(cpu.id as u32).to_ne_bytes(),
490                &(dense_llc as u32).to_ne_bytes(),
491                MapFlags::ANY,
492            )?;
493        }
494
495        // Set up PMU counters on every CPU (must be done after load so the
496        // scx_pmu_map fd is available). Counter indices match the BPF install
497        // order: migration event first (0), then sticky (1); when only the
498        // sticky event is used it takes index 0.
499        if opts.perf_config.event_id > 0 || opts.perf_sticky.event_id > 0 {
500            let nr_cpus = *NR_CPU_IDS;
501            info!("Setting up performance counters for {} CPUs...", nr_cpus);
502            let sticky_counter_idx = if opts.perf_config.event_id > 0 { 1 } else { 0 };
503            for cpu in 0..nr_cpus {
504                if opts.perf_config.event_id > 0 {
505                    setup_perf_events(&skel.maps.scx_pmu_map, cpu as i32, &opts.perf_config, 0)
506                        .with_context(|| {
507                            format!(
508                                "setting up perf event '{}' on CPU {}",
509                                opts.perf_config.display_name, cpu
510                            )
511                        })?;
512                }
513                if opts.perf_sticky.event_id > 0 {
514                    setup_perf_events(
515                        &skel.maps.scx_pmu_map,
516                        cpu as i32,
517                        &opts.perf_sticky,
518                        sticky_counter_idx,
519                    )
520                    .with_context(|| {
521                        format!(
522                            "setting up sticky perf event '{}' on CPU {}",
523                            opts.perf_sticky.display_name, cpu
524                        )
525                    })?;
526                }
527            }
528            info!("Performance counters configured for all CPUs");
529        }
530
531        info!("{SCHEDULER_NAME} can be used with scx-forge-agent for policy optimization");
532
533        // Attach the scheduler.
534        let struct_ops = Some(scx_ops_attach!(skel, forge_ops, false)?);
535        let stats_server = StatsServer::new(stats::server_data()).launch()?;
536
537        Ok(Self {
538            skel,
539            struct_ops,
540            stats_server,
541            user_restart: false,
542        })
543    }
544
545    fn bytes_of<T>(value: &T) -> &[u8] {
546        unsafe {
547            std::slice::from_raw_parts(value as *const T as *const u8, std::mem::size_of::<T>())
548        }
549    }
550
551    fn topo_core_type(core_type: &CoreType) -> u32 {
552        match core_type {
553            CoreType::Big { turbo: true } => 1,
554            CoreType::Big { turbo: false } => 0,
555            CoreType::Little => 2,
556        }
557    }
558
559    fn topo_cpumask(mask: &Cpumask) -> Result<forge_topo_cpumask> {
560        let raw = mask.as_raw_slice();
561        if raw.len() > FORGE_TOPO_CPUMASK_WORDS_USIZE {
562            bail!(
563                "topology cpumask uses {} words, but scx_forge supports {}",
564                raw.len(),
565                FORGE_TOPO_CPUMASK_WORDS_USIZE
566            );
567        }
568
569        let mut topo_mask: forge_topo_cpumask = unsafe { std::mem::zeroed() };
570        topo_mask.bits[..raw.len()].copy_from_slice(raw);
571        Ok(topo_mask)
572    }
573
574    fn init_topology_maps(
575        skel: &mut BpfSkel<'_>,
576        topo: &Topology,
577        llc_id_to_dense: &std::collections::HashMap<usize, usize>,
578        cpu_capacities: &std::collections::HashMap<usize, u64>,
579        smt_enabled: bool,
580    ) -> Result<()> {
581        if topo.all_cpus.len() > FORGE_MAX_TOPO_DOMAINS_USIZE
582            || topo.all_cores.len() > FORGE_MAX_TOPO_DOMAINS_USIZE
583            || topo.all_llcs.len() > FORGE_MAX_TOPO_DOMAINS_USIZE
584            || topo.nodes.len() > FORGE_MAX_TOPO_DOMAINS_USIZE
585        {
586            bail!(
587                "topology exceeds scx_forge map limits: cpus={} cores={} llcs={} nodes={} max={}",
588                topo.all_cpus.len(),
589                topo.all_cores.len(),
590                topo.all_llcs.len(),
591                topo.nodes.len(),
592                FORGE_MAX_TOPO_DOMAINS_USIZE
593            );
594        }
595
596        let nr_distances = topo
597            .nodes
598            .values()
599            .map(|node| node.distance.len())
600            .sum::<usize>();
601        if nr_distances > FORGE_MAX_TOPO_DISTANCES_USIZE {
602            bail!(
603                "NUMA distance entries {} exceed scx_forge map limit {}",
604                nr_distances,
605                FORGE_MAX_TOPO_DISTANCES_USIZE
606            );
607        }
608
609        let mut topo_info: forge_topology = unsafe { std::mem::zeroed() };
610        topo_info.nr_cpu_ids = *NR_CPU_IDS as u32;
611        topo_info.nr_possible_cpus = *NR_CPUS_POSSIBLE as u32;
612        topo_info.nr_online_cpus = topo.all_cpus.len() as u32;
613        topo_info.nr_nodes = topo.nodes.len() as u32;
614        topo_info.nr_llcs = topo.all_llcs.len() as u32;
615        topo_info.nr_cores = topo.all_cores.len() as u32;
616        topo_info.nr_cpus = topo.all_cpus.len() as u32;
617        topo_info.smt_enabled = smt_enabled as u8;
618        topo_info.span = Self::topo_cpumask(&topo.span)?;
619        skel.maps.topo_info_map.update(
620            &0u32.to_ne_bytes(),
621            Self::bytes_of(&topo_info),
622            MapFlags::ANY,
623        )?;
624
625        for (idx, cpu_id) in topo.all_cpus.keys().enumerate() {
626            skel.maps.topo_cpu_ids.update(
627                &(idx as u32).to_ne_bytes(),
628                &(*cpu_id as u32).to_ne_bytes(),
629                MapFlags::ANY,
630            )?;
631        }
632        for (idx, core_id) in topo.all_cores.keys().enumerate() {
633            skel.maps.topo_core_ids.update(
634                &(idx as u32).to_ne_bytes(),
635                &(*core_id as u32).to_ne_bytes(),
636                MapFlags::ANY,
637            )?;
638        }
639        for (idx, llc_id) in topo.all_llcs.keys().enumerate() {
640            skel.maps.topo_llc_ids.update(
641                &(idx as u32).to_ne_bytes(),
642                &(*llc_id as u32).to_ne_bytes(),
643                MapFlags::ANY,
644            )?;
645        }
646        for (idx, node_id) in topo.nodes.keys().enumerate() {
647            skel.maps.topo_node_ids.update(
648                &(idx as u32).to_ne_bytes(),
649                &(*node_id as u32).to_ne_bytes(),
650                MapFlags::ANY,
651            )?;
652        }
653
654        for cpu in topo.all_cpus.values() {
655            let dense_llc = llc_id_to_dense.get(&cpu.llc_id).copied().unwrap_or(0);
656            let mut topo_cpu: forge_topo_cpu = unsafe { std::mem::zeroed() };
657            topo_cpu.id = cpu.id as u32;
658            topo_cpu.core_id = cpu.core_id as u32;
659            topo_cpu.llc_id = cpu.llc_id as u32;
660            topo_cpu.llc_dense_id = dense_llc as u32;
661            topo_cpu.node_id = cpu.node_id as u32;
662            topo_cpu.package_id = cpu.package_id as u32;
663            topo_cpu.cluster_id = cpu.cluster_id as i32;
664            topo_cpu.l2_id = cpu.l2_id as u32;
665            topo_cpu.l3_id = cpu.l3_id as u32;
666            topo_cpu.smt_level = cpu.smt_level as u32;
667            topo_cpu.core_type = Self::topo_core_type(&cpu.core_type);
668            topo_cpu.min_freq = cpu.min_freq as u64;
669            topo_cpu.max_freq = cpu.max_freq as u64;
670            topo_cpu.base_freq = cpu.base_freq as u64;
671            topo_cpu.cpu_capacity = cpu_capacities.get(&cpu.id).copied().unwrap_or(1);
672            topo_cpu.pm_qos_resume_latency_us = cpu.pm_qos_resume_latency_us as u64;
673            topo_cpu.trans_lat_ns = cpu.trans_lat_ns as u64;
674            topo_cpu.cache_size = cpu.cache_size as u64;
675            skel.maps.topo_cpu_map.update(
676                &(cpu.id as u32).to_ne_bytes(),
677                Self::bytes_of(&topo_cpu),
678                MapFlags::ANY,
679            )?;
680        }
681
682        for core in topo.all_cores.values() {
683            let dense_llc = llc_id_to_dense.get(&core.llc_id).copied().unwrap_or(0);
684            let mut topo_core: forge_topo_core = unsafe { std::mem::zeroed() };
685            topo_core.id = core.id as u32;
686            topo_core.kernel_id = core.kernel_id as u32;
687            topo_core.cluster_id = core.cluster_id as i32;
688            topo_core.llc_id = core.llc_id as u32;
689            topo_core.llc_dense_id = dense_llc as u32;
690            topo_core.node_id = core.node_id as u32;
691            topo_core.nr_cpus = core.cpus.len() as u32;
692            topo_core.core_type = Self::topo_core_type(&core.core_type);
693            topo_core.span = Self::topo_cpumask(&core.span)?;
694            skel.maps.topo_core_map.update(
695                &(core.id as u32).to_ne_bytes(),
696                Self::bytes_of(&topo_core),
697                MapFlags::ANY,
698            )?;
699        }
700
701        for llc in topo.all_llcs.values() {
702            let dense_llc = llc_id_to_dense.get(&llc.id).copied().unwrap_or(0);
703            let mut topo_llc: forge_topo_llc = unsafe { std::mem::zeroed() };
704            topo_llc.id = llc.id as u32;
705            topo_llc.kernel_id = llc.kernel_id as u32;
706            topo_llc.dense_id = dense_llc as u32;
707            topo_llc.node_id = llc.node_id as u32;
708            topo_llc.nr_cores = llc.cores.len() as u32;
709            topo_llc.nr_cpus = llc.all_cpus.len() as u32;
710            topo_llc.span = Self::topo_cpumask(&llc.span)?;
711            skel.maps.topo_llc_map.update(
712                &(llc.id as u32).to_ne_bytes(),
713                Self::bytes_of(&topo_llc),
714                MapFlags::ANY,
715            )?;
716        }
717
718        for node in topo.nodes.values() {
719            let mut topo_node: forge_topo_node = unsafe { std::mem::zeroed() };
720            topo_node.id = node.id as u32;
721            topo_node.nr_llcs = node.llcs.len() as u32;
722            topo_node.nr_cores = node.all_cores.len() as u32;
723            topo_node.nr_cpus = node.all_cpus.len() as u32;
724            topo_node.nr_distances = node.distance.len() as u32;
725            topo_node.span = Self::topo_cpumask(&node.span)?;
726            skel.maps.topo_node_map.update(
727                &(node.id as u32).to_ne_bytes(),
728                Self::bytes_of(&topo_node),
729                MapFlags::ANY,
730            )?;
731
732            for (distance_idx, distance) in node.distance.iter().enumerate() {
733                let key = forge_topo_distance_key {
734                    node_id: node.id as u32,
735                    distance_idx: distance_idx as u32,
736                };
737                skel.maps.topo_distance_map.update(
738                    Self::bytes_of(&key),
739                    &(*distance as u32).to_ne_bytes(),
740                    MapFlags::ANY,
741                )?;
742            }
743        }
744
745        Ok(())
746    }
747
748    fn enable_primary_cpu(skel: &mut BpfSkel<'_>, cpu: i32) -> Result<(), u32> {
749        let prog = &mut skel.progs.enable_primary_cpu;
750        let mut args = cpu_arg {
751            cpu_id: cpu as c_int,
752        };
753        let input = ProgramInput {
754            context_in: Some(unsafe {
755                std::slice::from_raw_parts_mut(
756                    &mut args as *mut _ as *mut u8,
757                    std::mem::size_of_val(&args),
758                )
759            }),
760            ..Default::default()
761        };
762        let out = prog.test_run(input).unwrap();
763        if out.return_value != 0 {
764            return Err(out.return_value);
765        }
766
767        Ok(())
768    }
769
770    fn enable_sibling_cpu(
771        skel: &mut BpfSkel<'_>,
772        cpu: usize,
773        sibling_cpu: usize,
774    ) -> Result<(), u32> {
775        let prog = &mut skel.progs.enable_sibling_cpu;
776        let mut args = domain_arg {
777            cpu_id: cpu as c_int,
778            sibling_cpu_id: sibling_cpu as c_int,
779        };
780        let input = ProgramInput {
781            context_in: Some(unsafe {
782                std::slice::from_raw_parts_mut(
783                    &mut args as *mut _ as *mut u8,
784                    std::mem::size_of_val(&args),
785                )
786            }),
787            ..Default::default()
788        };
789        let out = prog.test_run(input).unwrap();
790        if out.return_value != 0 {
791            return Err(out.return_value as u32);
792        }
793        Ok(())
794    }
795
796    fn init_smt_domains(skel: &mut BpfSkel<'_>, topo: &Topology) -> Result<(), std::io::Error> {
797        let smt_siblings = topo.sibling_cpus();
798        info!("SMT sibling CPUs: {:?}", smt_siblings);
799        for (cpu, sibling_cpu) in smt_siblings.iter().enumerate() {
800            Self::enable_sibling_cpu(skel, cpu, *sibling_cpu as usize).map_err(|e| {
801                std::io::Error::new(
802                    std::io::ErrorKind::Other,
803                    format!("enable_sibling_cpu: {}", e),
804                )
805            })?;
806        }
807        Ok(())
808    }
809
810    fn get_metrics(&self) -> Metrics {
811        let bss_data = self.skel.maps.bss_data.as_ref().unwrap();
812        Metrics {
813            nr_direct_dispatches: bss_data.nr_direct_dispatches,
814            nr_enqueues: bss_data.nr_enqueues,
815            nr_preempt_dispatches: bss_data.nr_preempt_dispatches,
816            nr_local_dispatches: bss_data.nr_local_dispatches,
817            nr_remote_dispatches: bss_data.nr_remote_dispatches,
818            nr_llc_dispatches: bss_data.nr_llc_dispatches,
819            nr_node_dispatches: bss_data.nr_node_dispatches,
820            nr_global_dispatches: bss_data.nr_global_dispatches,
821            nr_dequeues: bss_data.nr_dequeues,
822            nr_dispatch_dequeues: bss_data.nr_dispatch_dequeues,
823            nr_sched_change_dequeues: bss_data.nr_sched_change_dequeues,
824            nr_task_state_errors: bss_data.nr_task_state_errors,
825            nr_event_dispatches: bss_data.nr_event_dispatches,
826            nr_ev_sticky_dispatches: bss_data.nr_ev_sticky_dispatches,
827        }
828    }
829
830    pub fn exited(&mut self) -> bool {
831        uei_exited!(&self.skel, uei)
832    }
833
834    fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
835        let (res_ch, req_ch) = self.stats_server.channels();
836        while !shutdown.load(Ordering::Relaxed) && !self.exited() {
837            match req_ch.recv_timeout(Duration::from_secs(1)) {
838                Ok(()) => res_ch.send(self.get_metrics())?,
839                Err(RecvTimeoutError::Timeout) => {}
840                Err(e) => Err(e)?,
841            }
842        }
843
844        let _ = self.struct_ops.take();
845        uei_report!(&self.skel, uei)
846    }
847}
848
849impl Drop for Scheduler<'_> {
850    fn drop(&mut self) {
851        info!("Unregister {SCHEDULER_NAME} scheduler");
852    }
853}
854
855fn main() -> Result<()> {
856    let opts = Opts::parse();
857
858    if opts.version {
859        println!(
860            "{} {}",
861            SCHEDULER_NAME,
862            build_id::full_version(env!("CARGO_PKG_VERSION"))
863        );
864        return Ok(());
865    }
866
867    if opts.help_stats {
868        stats::server_data().describe_meta(&mut std::io::stdout(), None)?;
869        return Ok(());
870    }
871
872    let loglevel = simplelog::LevelFilter::Info;
873
874    let mut lcfg = simplelog::ConfigBuilder::new();
875    lcfg.set_time_offset_to_local()
876        .expect("Failed to set local time offset")
877        .set_time_level(simplelog::LevelFilter::Error)
878        .set_location_level(simplelog::LevelFilter::Off)
879        .set_target_level(simplelog::LevelFilter::Off)
880        .set_thread_level(simplelog::LevelFilter::Off);
881    simplelog::TermLogger::init(
882        loglevel,
883        lcfg.build(),
884        simplelog::TerminalMode::Stderr,
885        simplelog::ColorChoice::Auto,
886    )?;
887
888    let shutdown = Arc::new(AtomicBool::new(false));
889    let shutdown_clone = shutdown.clone();
890    ctrlc::set_handler(move || {
891        shutdown_clone.store(true, Ordering::Relaxed);
892    })
893    .context("Error setting Ctrl-C handler")?;
894
895    if let Some(intv) = opts.monitor.or(opts.stats) {
896        let shutdown_copy = shutdown.clone();
897        let jh = std::thread::spawn(move || {
898            match stats::monitor(Duration::from_secs_f64(intv), shutdown_copy) {
899                Ok(_) => {
900                    debug!("stats monitor thread finished successfully")
901                }
902                Err(error_object) => {
903                    warn!(
904                        "stats monitor thread finished because of an error {}",
905                        error_object
906                    )
907                }
908            }
909        });
910        if opts.monitor.is_some() {
911            let _ = jh.join();
912            return Ok(());
913        }
914    }
915
916    let mut open_object = MaybeUninit::uninit();
917    loop {
918        let mut sched = Scheduler::init(&opts, &mut open_object)?;
919        if !sched.run(shutdown.clone())?.should_restart() {
920            if sched.user_restart {
921                continue;
922            }
923            break;
924        }
925    }
926
927    Ok(())
928}