Skip to main content

scx_cosmos/
main.rs

1// SPDX-License-Identifier: GPL-2.0
2//
3// Copyright (c) 2025 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 cgroup;
14mod gpu;
15mod stats;
16use cgroup::CgroupReader;
17
18use std::collections::{HashMap, HashSet};
19use std::ffi::{c_int, c_ulong};
20use std::mem::MaybeUninit;
21use std::sync::atomic::AtomicBool;
22use std::sync::atomic::Ordering;
23use std::sync::Arc;
24use std::time::{Duration, Instant};
25
26use anyhow::bail;
27use anyhow::Context;
28use anyhow::Result;
29use clap::Parser;
30use crossbeam::channel::RecvTimeoutError;
31use libbpf_rs::MapCore;
32use libbpf_rs::MapFlags;
33use libbpf_rs::OpenObject;
34use libbpf_rs::ProgramInput;
35use log::{debug, info, warn};
36use nvml_wrapper::bitmasks::InitFlags;
37use nvml_wrapper::Nvml;
38use scx_stats::prelude::*;
39use scx_utils::build_id;
40use scx_utils::compat;
41use scx_utils::get_primary_cpus;
42use scx_utils::libbpf_clap_opts::LibbpfOpts;
43use scx_utils::perf::parse_perf_event;
44use scx_utils::perf::setup_perf_events;
45use scx_utils::perf::PerfEventSpec;
46use scx_utils::scx_ops_attach;
47use scx_utils::scx_ops_load;
48use scx_utils::scx_ops_open;
49use scx_utils::try_set_rlimit_infinity;
50use scx_utils::uei_exited;
51use scx_utils::uei_report;
52use scx_utils::GpuIndex;
53use scx_utils::Powermode;
54use scx_utils::Topology;
55use scx_utils::UserExitInfo;
56use scx_utils::NR_CPU_IDS;
57use stats::Metrics;
58
59const SCHEDULER_NAME: &str = "scx_cosmos";
60
61#[derive(Debug, clap::Parser)]
62#[command(
63    name = "scx_cosmos",
64    version,
65    disable_version_flag = true,
66    about = "Lightweight scheduler optimized for preserving task-to-CPU locality."
67)]
68struct Opts {
69    /// Exit debug dump buffer length. 0 indicates default.
70    #[clap(long, default_value = "0")]
71    exit_dump_len: u32,
72
73    /// Maximum scheduling slice duration in microseconds.
74    #[clap(short = 's', long, default_value = "1000")]
75    slice_us: u64,
76
77    /// Maximum runtime (since last sleep) that can be charged to a task in microseconds.
78    #[clap(short = 'l', long, default_value = "20000")]
79    slice_lag_us: u64,
80
81    /// CPU busy threshold.
82    ///
83    /// Specifies the user CPU utilization percentage (0-100%) at which the scheduler considers a
84    /// CPU to be busy. Only user time counts: sleep-intensive workloads burn most of their CPU
85    /// time in the kernel and their CPUs are deliberately not considered busy, so that tasks stay
86    /// on their CPU / local dispatch queue, reducing the scheduling overhead and the balancing
87    /// pressure in dispatch.
88    ///
89    /// When a CPU crosses this threshold, the scheduler switches it from using its per-CPU
90    /// round-robin dispatch queue (which favors locality and reduced locking contention) to the
91    /// global deadline-based dispatch queue (which improves load balancing).
92    ///
93    /// The global dispatch queue can increase task migrations and improve responsiveness for
94    /// interactive tasks under heavy load. Lower values make the scheduler switch to deadline
95    /// mode sooner, improving overall responsiveness at the cost of reducing single-task
96    /// performance due to the additional migrations. Higher values makes task more "sticky" to
97    /// their CPU, improving workloads that benefit from cache locality.
98    ///
99    /// A higher value is recommended for server-type workloads, while a lower value is recommended
100    /// for interactive-type workloads.
101    ///
102    /// 0 = utilization tracking disabled: every CPU is always considered busy (pure deadline
103    /// mode) with no tracking overhead.
104    #[clap(short = 'c', long, default_value = "0")]
105    cpu_busy_thresh: u64,
106
107    /// Polling time (ms) to refresh the dynamic perf event thresholds (options -E / -Y in
108    /// dynamic mode).
109    ///
110    /// Value is clamped to the range [10 .. 1000].
111    ///
112    /// 0 = disabled.
113    #[clap(short = 'p', long, default_value = "0")]
114    polling_ms: u64,
115
116    /// Specifies a list of CPUs to prioritize.
117    ///
118    /// Accepts a comma-separated list of CPUs or ranges (i.e., 0-3,12-15) or the following special
119    /// keywords:
120    ///
121    /// "turbo" = automatically detect and prioritize the CPUs with the highest max frequency,
122    /// "performance" = automatically detect and prioritize the fastest CPUs,
123    /// "powersave" = automatically detect and prioritize the slowest CPUs,
124    /// "all" = all CPUs assigned to the primary domain.
125    ///
126    /// By default "all" CPUs are used.
127    #[clap(short = 'm', long)]
128    primary_domain: Option<String>,
129
130    /// Maximum time (us) that tasks can wait in a shared DSQ before the primary domain is
131    /// considered overloaded, allowing tasks to spill to the non-primary CPUs.
132    ///
133    /// Tasks are normally contained in the primary domain (option -m). When the domain's shared
134    /// DSQ has been continuously backlogged for longer than this threshold, tasks are allowed to
135    /// run on the non-primary CPUs until the backlog drains.
136    ///
137    /// 0 = tasks are always contained in the primary domain, even when it is overloaded.
138    ///
139    /// This option is ignored when no primary domain is defined.
140    #[clap(long, default_value = "1000")]
141    primary_overload_us: u64,
142
143    /// Hardware perf event to monitor (0x0 = disabled). Accepts hex (0xN) or symbolic names
144    /// (e.g. cache-misses, LLC-load-misses, page-faults, branch-misses).
145    #[clap(short = 'e', long, default_value = "0x0", value_parser = parse_perf_event)]
146    perf_config: PerfEventSpec,
147
148    /// Threshold (perf events/msec) to classify a task as event heavy; exceeding it triggers migration.
149    #[clap(short = 'E', default_value = "0", long)]
150    perf_threshold: u64,
151
152    /// Sticky perf event (0x0 = disabled). When a task exceeds -Y for this event, keep it on the same CPU.
153    /// Accepts hex (0xN) or symbolic names (e.g. cache-misses, LLC-load-misses).
154    #[clap(short = 'y', long, default_value = "0x0", value_parser = parse_perf_event)]
155    perf_sticky: PerfEventSpec,
156
157    /// Sticky perf threshold; task is kept on same CPU when its count for -y event exceeds this.
158    #[clap(short = 'Y', default_value = "0", long)]
159    perf_sticky_threshold: u64,
160
161    /// Enable GPU-aware scheduling.
162    #[clap(short = 'g', long, action = clap::ArgAction::SetTrue)]
163    gpu: bool,
164
165    /// Only treat a process as GPU-bound if its GPU utilization is at least this percentage (0–100).
166    ///
167    /// Uses NVML process utilization (SM + memory). 0 = no filter (all processes on the GPU are
168    /// considered GPU-bound). Requires driver support (Maxwell or newer).
169    #[clap(long, default_value = "0", value_parser = clap::value_parser!(u32).range(0..=100))]
170    gpu_util_threshold: u32,
171
172    /// Disable NUMA optimizations.
173    #[clap(short = 'n', long, action = clap::ArgAction::SetTrue)]
174    disable_numa: bool,
175
176    /// Disable CPU frequency control.
177    #[clap(short = 'f', long, action = clap::ArgAction::SetTrue)]
178    disable_cpufreq: bool,
179
180    /// Enable flat idle CPU scanning.
181    ///
182    /// This option can help reducing some overhead when trying to allocate idle CPUs and it can be
183    /// quite effective with simple CPU topologies.
184    #[arg(short = 'i', long, action = clap::ArgAction::SetTrue)]
185    flat_idle_scan: bool,
186
187    /// Enable preferred idle CPU scanning.
188    ///
189    /// With this option enabled, the scheduler will prioritize assigning tasks to higher-ranked
190    /// cores before considering lower-ranked ones.
191    #[clap(short = 'P', long, action = clap::ArgAction::SetTrue)]
192    preferred_idle_scan: bool,
193
194    /// Disable SMT.
195    ///
196    /// This option can only be used together with --flat-idle-scan or --preferred-idle-scan,
197    /// otherwise it is ignored.
198    #[clap(long, action = clap::ArgAction::SetTrue)]
199    disable_smt: bool,
200
201    /// ***DEPRECATED*** SMT contention avoidance.
202    #[clap(short = 'S', long, action = clap::ArgAction::SetTrue)]
203    avoid_smt: bool,
204
205    /// Disable early clearing of idle CPU state.
206    ///
207    /// When enabled, multiple concurrent wakeups can select the same idle CPU
208    /// before it fully wakes up. This can improve performance in highly communicative
209    /// workloads by aggressively stacking tasks on the same cache.
210    #[clap(short = 'N', long, action = clap::ArgAction::SetTrue)]
211    no_early_clear: bool,
212
213    /// Disable direct dispatch during synchronous wakeups.
214    ///
215    /// Enabling this option can lead to a more uniform load distribution across available cores,
216    /// potentially improving performance in certain scenarios. However, it may come at the cost of
217    /// reduced efficiency for pipe-intensive workloads that benefit from tighter producer-consumer
218    /// coupling.
219    #[clap(short = 'w', long, action = clap::ArgAction::SetTrue)]
220    no_wake_sync: bool,
221
222    /// ***DEPRECATED*** Disable deferred wakeups.
223    #[clap(short = 'd', long, action = clap::ArgAction::SetTrue)]
224    no_deferred_wakeup: bool,
225
226    /// Enable high-resolution timer preemption.
227    ///
228    /// By default, the scheduler preempts tasks that exceed their time slice, measuring the time
229    /// slice via the tick handler. Add an option to enforce preemption based on the high-precision
230    /// timer and CPU occupancy. Enable this option to improve latency-sensitive workloads.
231    #[clap(long, action = clap::ArgAction::SetTrue)]
232    time_preemption: bool,
233
234    /// Enable address space affinity.
235    ///
236    /// This option allows to keep tasks that share the same address space (e.g., threads of the
237    /// same process) on the same CPU across wakeups.
238    ///
239    /// This can improve locality and performance in certain cache-sensitive workloads.
240    #[clap(short = 'a', long, action = clap::ArgAction::SetTrue)]
241    mm_affinity: bool,
242
243    /// Enable stats monitoring with the specified interval.
244    #[clap(long)]
245    stats: Option<f64>,
246
247    /// Run in stats monitoring mode with the specified interval. Scheduler
248    /// is not launched.
249    #[clap(long)]
250    monitor: Option<f64>,
251
252    /// Enable verbose output, including libbpf details.
253    #[clap(short = 'v', long, action = clap::ArgAction::SetTrue)]
254    verbose: bool,
255
256    /// Print scheduler version and exit.
257    #[clap(short = 'V', long, action = clap::ArgAction::SetTrue)]
258    version: bool,
259
260    /// Show descriptions for statistics.
261    #[clap(long)]
262    help_stats: bool,
263
264    #[clap(flatten, next_help_heading = "Libbpf Options")]
265    pub libbpf: LibbpfOpts,
266}
267
268pub fn parse_cpu_list(optarg: &str) -> Result<Vec<usize>, String> {
269    let mut cpus = Vec::new();
270    let mut seen = HashSet::new();
271
272    // Handle special keywords
273    if let Some(mode) = match optarg {
274        "powersave" => Some(Powermode::Powersave),
275        "performance" => Some(Powermode::Performance),
276        "turbo" => Some(Powermode::Turbo),
277        "all" => Some(Powermode::Any),
278        _ => None,
279    } {
280        return get_primary_cpus(mode).map_err(|e| e.to_string());
281    }
282
283    // Validate input characters
284    if optarg
285        .chars()
286        .any(|c| !c.is_ascii_digit() && c != '-' && c != ',' && !c.is_whitespace())
287    {
288        return Err("Invalid character in CPU list".to_string());
289    }
290
291    // Replace all whitespace with tab (or just trim later)
292    let cleaned = optarg.replace(' ', "\t");
293
294    for token in cleaned.split(',') {
295        let token = token.trim_matches(|c: char| c.is_whitespace());
296
297        if token.is_empty() {
298            continue;
299        }
300
301        if let Some((start_str, end_str)) = token.split_once('-') {
302            let start = start_str
303                .trim()
304                .parse::<usize>()
305                .map_err(|_| "Invalid range start")?;
306            let end = end_str
307                .trim()
308                .parse::<usize>()
309                .map_err(|_| "Invalid range end")?;
310
311            if start > end {
312                return Err(format!("Invalid CPU range: {}-{}", start, end));
313            }
314
315            for i in start..=end {
316                if cpus.len() >= *NR_CPU_IDS {
317                    return Err(format!("Too many CPUs specified (max {})", *NR_CPU_IDS));
318                }
319                if seen.insert(i) {
320                    cpus.push(i);
321                }
322            }
323        } else {
324            let cpu = token
325                .parse::<usize>()
326                .map_err(|_| format!("Invalid CPU: {}", token))?;
327            if cpus.len() >= *NR_CPU_IDS {
328                return Err(format!("Too many CPUs specified (max {})", *NR_CPU_IDS));
329            }
330            if seen.insert(cpu) {
331                cpus.push(cpu);
332            }
333        }
334    }
335
336    Ok(cpus)
337}
338
339/// Initial value for the dynamic threshold (in BPF units).
340const DYNAMIC_THRESHOLD_INIT_VALUE: u64 = 1000;
341
342/// Minimum value for the dynamic threshold (in BPF units).
343const DYNAMIC_THRESHOLD_MIN_VALUE: u64 = 10;
344
345/// Target event rate (per second) above which we consider migrations/sticky dispatches too high.
346const DYNAMIC_THRESHOLD_RATE_HIGH: f64 = 4000.0;
347
348/// Target event rate (per second) below which we consider migrations/sticky dispatches too low.
349const DYNAMIC_THRESHOLD_RATE_LOW: f64 = 2000.0;
350
351/// Hysteresis band: rate must move by this fraction beyond the target bounds before we act.
352/// This prevents oscillation when the rate hovers near the threshold boundaries.
353const DYNAMIC_THRESHOLD_HYSTERESIS: f64 = 0.1;
354
355/// EMA smoothing factor (alpha). Higher values give more weight to recent samples.
356/// 0.3 provides good balance between responsiveness and stability.
357const DYNAMIC_THRESHOLD_EMA_ALPHA: f64 = 0.3;
358
359/// Minimum scale factor when just outside the target band (slow convergence near optimal).
360const DYNAMIC_THRESHOLD_SCALE_MIN: f64 = 0.0001;
361
362/// Maximum scale factor when far from target (fast convergence when initial threshold is way off).
363const DYNAMIC_THRESHOLD_SCALE_MAX: f64 = 1000.0;
364
365/// Slope for "too high" case: scale grows with (rate/HIGH - 1) so we step much harder when rate is
366/// many times over target.
367const DYNAMIC_THRESHOLD_SLOPE_HIGH: f64 = 0.35;
368
369/// Slope for "too low" case: scale grows with deficit so we step harder when rate is near zero.
370const DYNAMIC_THRESHOLD_SLOPE_LOW: f64 = 0.58;
371
372/// Minimum interval between NVML GPU PID syncs. Kept separate from CPU polling so that fast
373/// polling (e.g. 100 ms) does not trigger expensive NVML calls every tick.
374const GPU_SYNC_INTERVAL: Duration = Duration::from_secs(1);
375
376/// State for EMA-based dynamic threshold adjustment with hysteresis.
377///
378/// This struct maintains the smoothed rate estimate and tracks whether we're
379/// currently in an adjustment state (raising or lowering threshold) to implement
380/// hysteresis and prevent oscillation.
381#[derive(Debug, Clone)]
382struct DynamicThresholdState {
383    /// Current threshold value.
384    threshold: u64,
385    /// EMA-smoothed rate estimate.
386    smoothed_rate: f64,
387    /// Previous raw counter value for delta calculation.
388    prev_counter: u64,
389    /// Whether the EMA has been initialized with a valid sample.
390    initialized: bool,
391    /// Current adjustment direction: None (stable), Some(true) = raising, Some(false) = lowering.
392    /// Used for hysteresis: once we start adjusting in a direction, we continue until
393    /// the rate crosses back into the stable band with hysteresis margin.
394    adjustment_direction: Option<bool>,
395}
396
397impl DynamicThresholdState {
398    /// Create a new dynamic threshold state with the given initial threshold.
399    fn new(initial_threshold: u64) -> Self {
400        Self {
401            threshold: initial_threshold,
402            smoothed_rate: 0.0,
403            prev_counter: 0,
404            initialized: false,
405            adjustment_direction: None,
406        }
407    }
408
409    /// Update the state with a new counter sample and elapsed time.
410    /// Returns the new threshold if it changed, or None if unchanged.
411    fn update(
412        &mut self,
413        counter: u64,
414        elapsed_secs: f64,
415        verbose: bool,
416        name: &str,
417    ) -> Option<u64> {
418        if elapsed_secs <= 0.0 {
419            return None;
420        }
421
422        // Calculate instantaneous rate.
423        let delta = counter.saturating_sub(self.prev_counter);
424        self.prev_counter = counter;
425        let raw_rate = delta as f64 / elapsed_secs;
426
427        // Update EMA.
428        if self.initialized {
429            self.smoothed_rate = DYNAMIC_THRESHOLD_EMA_ALPHA * raw_rate
430                + (1.0 - DYNAMIC_THRESHOLD_EMA_ALPHA) * self.smoothed_rate;
431        } else {
432            // First sample: initialize EMA directly.
433            self.smoothed_rate = raw_rate;
434            self.initialized = true;
435        }
436
437        // Determine if we should adjust the threshold using hysteresis.
438        let rate = self.smoothed_rate;
439        let old_threshold = self.threshold;
440
441        // Calculate hysteresis-adjusted bounds based on current state.
442        let (effective_high, effective_low) = match self.adjustment_direction {
443            Some(true) => {
444                // Currently raising threshold: need rate to drop below LOW - hysteresis to stop.
445                (
446                    DYNAMIC_THRESHOLD_RATE_HIGH,
447                    DYNAMIC_THRESHOLD_RATE_LOW * (1.0 - DYNAMIC_THRESHOLD_HYSTERESIS),
448                )
449            }
450            Some(false) => {
451                // Currently lowering threshold: need rate to rise above HIGH + hysteresis to stop.
452                (
453                    DYNAMIC_THRESHOLD_RATE_HIGH * (1.0 + DYNAMIC_THRESHOLD_HYSTERESIS),
454                    DYNAMIC_THRESHOLD_RATE_LOW,
455                )
456            }
457            None => {
458                // Stable state: need rate to exceed bounds + hysteresis to start adjusting.
459                (
460                    DYNAMIC_THRESHOLD_RATE_HIGH * (1.0 + DYNAMIC_THRESHOLD_HYSTERESIS),
461                    DYNAMIC_THRESHOLD_RATE_LOW * (1.0 - DYNAMIC_THRESHOLD_HYSTERESIS),
462                )
463            }
464        };
465
466        // Determine new adjustment direction.
467        let new_direction = if rate > effective_high {
468            Some(true) // Rate too high, raise threshold.
469        } else if rate < effective_low && rate >= 0.0 {
470            Some(false) // Rate too low, lower threshold.
471        } else {
472            // Rate in stable band (considering hysteresis).
473            if self.adjustment_direction.is_some() {
474                // We were adjusting; check if we should stop.
475                if rate >= DYNAMIC_THRESHOLD_RATE_LOW && rate <= DYNAMIC_THRESHOLD_RATE_HIGH {
476                    None // Back in target band, stop adjusting.
477                } else {
478                    self.adjustment_direction // Continue current direction.
479                }
480            } else {
481                None // Already stable.
482            }
483        };
484
485        // Apply adjustment if we have a direction.
486        if let Some(raising) = new_direction {
487            let scale = Self::compute_scale(rate, raising);
488            let factor = if raising { 1.0 + scale } else { 1.0 - scale };
489            let new_threshold = ((self.threshold as f64) * factor).round() as u64;
490            self.threshold = new_threshold.clamp(DYNAMIC_THRESHOLD_MIN_VALUE, u64::MAX);
491        }
492
493        self.adjustment_direction = new_direction;
494
495        // Return new threshold only if it changed.
496        if self.threshold != old_threshold {
497            if verbose {
498                info!(
499                    "{}: {} -> {} (smoothed rate {:.1}/s, raw {:.1}/s, dir {:?})",
500                    name,
501                    old_threshold,
502                    self.threshold,
503                    self.smoothed_rate,
504                    raw_rate,
505                    self.adjustment_direction
506                );
507            }
508            Some(self.threshold)
509        } else {
510            None
511        }
512    }
513
514    /// Compute the scale factor for threshold adjustment based on how far the rate
515    /// is from the target band.
516    fn compute_scale(rate: f64, too_high: bool) -> f64 {
517        if too_high {
518            let excess = ((rate / DYNAMIC_THRESHOLD_RATE_HIGH) - 1.0).max(0.0);
519            let scale =
520                DYNAMIC_THRESHOLD_SCALE_MIN + DYNAMIC_THRESHOLD_SLOPE_HIGH * excess.min(4.0);
521            scale.min(DYNAMIC_THRESHOLD_SCALE_MAX)
522        } else {
523            if rate <= 0.0 {
524                return DYNAMIC_THRESHOLD_SCALE_MAX;
525            }
526            let deficit = (DYNAMIC_THRESHOLD_RATE_LOW - rate) / DYNAMIC_THRESHOLD_RATE_LOW;
527            let t = deficit.clamp(0.0, 1.0);
528            DYNAMIC_THRESHOLD_SCALE_MIN + DYNAMIC_THRESHOLD_SLOPE_LOW * t
529        }
530    }
531}
532
533struct Scheduler<'a> {
534    skel: BpfSkel<'a>,
535    opts: &'a Opts,
536    struct_ops: Option<libbpf_rs::Link>,
537    stats_server: StatsServer<(), Metrics>,
538    /// GPU device index -> NUMA node (for NVML PID sync). Only set when --gpu and NUMA enabled.
539    gpu_index_to_node: Option<HashMap<u32, u32>>,
540    /// Previous (TGID, node) set so we can remove processes that stopped using the GPU.
541    previous_gpu_pids: Option<HashMap<u32, u32>>,
542    /// Reused NVML handle to avoid re-initializing on every sync (expensive).
543    nvml: Option<Nvml>,
544    /// Host cgroup v2 reader used to discover peer processes of NVML GPU processes.
545    gpu_cgroup_reader: Option<CgroupReader>,
546    /// Dynamic threshold state for perf event migrations (when --perf-threshold is 0/dynamic).
547    perf_threshold_state: Option<DynamicThresholdState>,
548    /// Dynamic threshold state for sticky perf events (when --perf-sticky-threshold is 0/dynamic).
549    perf_sticky_threshold_state: Option<DynamicThresholdState>,
550}
551
552impl<'a> Scheduler<'a> {
553    fn init(opts: &'a Opts, open_object: &'a mut MaybeUninit<OpenObject>) -> Result<Self> {
554        try_set_rlimit_infinity();
555
556        // Initialize CPU topology.
557        let topo = Topology::new().unwrap();
558
559        // Check host topology to determine if we need to enable SMT capabilities.
560        let smt_enabled = !opts.disable_smt && topo.smt_enabled;
561
562        // Determine the amount of non-empty NUMA nodes in the system.
563        let nr_nodes = topo
564            .nodes
565            .values()
566            .filter(|node| !node.all_cpus.is_empty())
567            .count();
568        info!("NUMA nodes: {}", nr_nodes);
569
570        // Automatically disable NUMA optimizations when running on non-NUMA systems.
571        let numa_enabled = !opts.disable_numa && nr_nodes > 1;
572        if !numa_enabled {
573            info!("Disabling NUMA optimizations");
574        }
575
576        info!(
577            "{} {} {}",
578            SCHEDULER_NAME,
579            build_id::full_version(env!("CARGO_PKG_VERSION")),
580            if smt_enabled { "SMT on" } else { "SMT off" }
581        );
582
583        // Print command line.
584        info!(
585            "scheduler options: {}",
586            std::env::args().collect::<Vec<_>>().join(" ")
587        );
588
589        // Initialize BPF connector.
590        let mut skel_builder = BpfSkelBuilder::default();
591        skel_builder.obj_builder.debug(opts.verbose);
592        let open_opts = opts.libbpf.clone().into_bpf_open_opts();
593        let mut skel = scx_ops_open!(skel_builder, open_object, cosmos_ops, open_opts)?;
594
595        skel.struct_ops.cosmos_ops_mut().exit_dump_len = opts.exit_dump_len;
596
597        // Override default BPF scheduling parameters.
598        let rodata = skel.maps.rodata_data.as_mut().unwrap();
599        rodata.slice_ns = opts.slice_us * 1000;
600        rodata.slice_lag = opts.slice_lag_us * 1000;
601        rodata.cpufreq_enabled = !opts.disable_cpufreq;
602        rodata.flat_idle_scan = opts.flat_idle_scan;
603        rodata.smt_enabled = smt_enabled;
604        rodata.numa_enabled = numa_enabled;
605        rodata.nr_node_ids = topo.nodes.len() as u32;
606        rodata.no_wake_sync = opts.no_wake_sync;
607        rodata.no_early_clear = opts.no_early_clear;
608        rodata.time_preemption = opts.time_preemption;
609        rodata.mm_affinity = opts.mm_affinity;
610
611        // Enable perf event scheduling settings.
612        rodata.perf_config = opts.perf_config.event_id;
613        rodata.perf_sticky = opts.perf_sticky.event_id;
614
615        // Normalize CPU busy threshold in the range [0 .. 1024].
616        rodata.busy_threshold = opts.cpu_busy_thresh * 1024 / 100;
617
618        // Maximum shared DSQ backlog before spilling outside the primary domain.
619        rodata.overload_thresh_ns = opts.primary_overload_us * 1000;
620
621        // Generate the list of available CPUs sorted by capacity in descending order.
622        let mut cpus: Vec<_> = topo.all_cpus.values().collect();
623        cpus.sort_by_key(|cpu| std::cmp::Reverse(cpu.cpu_capacity));
624        // Normalize CPU capacities to 1..1024 so the highest capacity is always 1024.
625        let max_cap = cpus.first().map(|c| c.cpu_capacity).unwrap_or(1).max(1);
626        for (i, cpu) in cpus.iter().enumerate() {
627            let normalized = (cpu.cpu_capacity * 1024 / max_cap).clamp(1, 1024);
628            rodata.cpu_capacity[cpu.id] = normalized as c_ulong;
629            rodata.preferred_cpus[i] = cpu.id as u64;
630        }
631        rodata.all_cpus_same_capacity = cpus.iter().all(|cpu| cpu.cpu_capacity == max_cap);
632        if opts.preferred_idle_scan {
633            info!(
634                "Preferred CPUs: {:?}",
635                &rodata.preferred_cpus[0..cpus.len()]
636            );
637        }
638        rodata.preferred_idle_scan = opts.preferred_idle_scan;
639
640        // Define the primary scheduling domain.
641        let primary_cpus = if let Some(ref domain) = opts.primary_domain {
642            match parse_cpu_list(domain) {
643                Ok(cpus) => cpus,
644                Err(e) => bail!("Error parsing primary domain: {}", e),
645            }
646        } else {
647            (0..*NR_CPU_IDS).collect()
648        };
649        if primary_cpus.len() < *NR_CPU_IDS {
650            info!("Primary CPUs: {:?}", primary_cpus);
651            rodata.primary_all = false;
652        } else {
653            rodata.primary_all = true;
654        }
655
656        // Enable GPU support and build GPU index -> node for NVML PID sync. Init NVML once here
657        // so we reuse the handle in the run loop (re-initing every sync is very expensive).
658        let (gpu_index_to_node, previous_gpu_pids, nvml) = if opts.gpu && numa_enabled {
659            match Nvml::init_with_flags(InitFlags::NO_GPUS) {
660                Ok(nvml) => {
661                    info!("NVIDIA GPU-aware scheduling enabled (NVML PID sync)");
662                    rodata.gpu_enabled = true;
663                    let mut idx_to_node = HashMap::new();
664                    for (id, gpu) in topo.gpus() {
665                        let GpuIndex::Nvidia { nvml_id } = id;
666                        idx_to_node.insert(nvml_id, gpu.node_id as u32);
667                    }
668                    (Some(idx_to_node), Some(HashMap::new()), Some(nvml))
669                }
670                Err(e) => {
671                    warn!("NVML init failed, disabling GPU-aware scheduling: {}", e);
672                    rodata.gpu_enabled = false;
673                    (None, None, None)
674                }
675            }
676        } else {
677            rodata.gpu_enabled = false;
678            (None, None, None)
679        };
680
681        let gpu_cgroup_reader = if nvml.is_some() && opts.gpu_util_threshold == 0 {
682            match CgroupReader::discover() {
683                Ok(reader) => {
684                    info!("NVIDIA GPU workload discovery enabled (cgroup v2)");
685                    Some(reader)
686                }
687                Err(error) => {
688                    warn!(
689                        "GPU workload discovery unavailable, using NVML process scope: {error:#}"
690                    );
691                    None
692                }
693            }
694        } else if nvml.is_some() {
695            info!(
696                "NVIDIA GPU workload discovery requires --gpu-util-threshold=0; using NVML process scope"
697            );
698            None
699        } else {
700            None
701        };
702
703        // Set scheduler flags.
704        skel.struct_ops.cosmos_ops_mut().flags = *compat::SCX_OPS_ENQ_EXITING
705            | *compat::SCX_OPS_ENQ_LAST
706            | *compat::SCX_OPS_ENQ_MIGRATION_DISABLED
707            | *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP
708            | if numa_enabled {
709                *compat::SCX_OPS_BUILTIN_IDLE_PER_NODE
710            } else {
711                0
712            };
713
714        info!(
715            "scheduler flags: {:#x}",
716            skel.struct_ops.cosmos_ops_mut().flags
717        );
718
719        // Load the BPF program for validation.
720        let mut skel = scx_ops_load!(skel, cosmos_ops, uei)?;
721
722        // Initial perf thresholds in bss. When threshold is 0 we use dynamic logic; when user
723        // specifies a value > 0 we use it as a static threshold.
724        let bss = skel.maps.bss_data.as_mut().unwrap();
725        if opts.perf_config.event_id > 0 {
726            bss.perf_threshold = if opts.perf_threshold == 0 {
727                DYNAMIC_THRESHOLD_INIT_VALUE
728            } else {
729                opts.perf_threshold
730            };
731        }
732        if opts.perf_sticky.event_id > 0 {
733            bss.perf_sticky_threshold = if opts.perf_sticky_threshold == 0 {
734                DYNAMIC_THRESHOLD_INIT_VALUE
735            } else {
736                opts.perf_sticky_threshold
737            };
738        }
739
740        // Configure CPU->node mapping (must be done after skeleton is loaded).
741        for node in topo.nodes.values() {
742            for cpu in node.all_cpus.values() {
743                if opts.verbose {
744                    info!("CPU{} -> node{}", cpu.id, node.id);
745                }
746                skel.maps.cpu_node_map.update(
747                    &(cpu.id as u32).to_ne_bytes(),
748                    &(node.id as u32).to_ne_bytes(),
749                    MapFlags::ANY,
750                )?;
751            }
752        }
753
754        // Setup performance events for all CPUs.
755        // Counter indices must match PMU library install order: migration first (0), then sticky (1).
756        // When only sticky is used, it gets index 0; when both are used, sticky gets index 1.
757        let nr_cpus = *NR_CPU_IDS;
758        info!("Setting up performance counters for {} CPUs...", nr_cpus);
759        let mut perf_available = true;
760        let sticky_counter_idx = if opts.perf_config.event_id > 0 { 1 } else { 0 };
761        for cpu in 0..nr_cpus {
762            if opts.perf_config.event_id > 0 {
763                if let Err(e) =
764                    setup_perf_events(&skel.maps.scx_pmu_map, cpu as i32, &opts.perf_config, 0)
765                {
766                    if cpu == 0 {
767                        let err_str = e.to_string();
768                        if err_str.contains("errno 2") || err_str.contains("os error 2") {
769                            warn!("Performance counters not available on this CPU architecture");
770                            warn!("PMU event '{}' not supported - scheduler will run without perf monitoring", opts.perf_config.display_name);
771                        } else {
772                            warn!("Failed to setup perf events: {}", e);
773                        }
774                        perf_available = false;
775                        break;
776                    }
777                }
778            }
779            if opts.perf_sticky.event_id > 0 {
780                if let Err(e) = setup_perf_events(
781                    &skel.maps.scx_pmu_map,
782                    cpu as i32,
783                    &opts.perf_sticky,
784                    sticky_counter_idx,
785                ) {
786                    if cpu == 0 {
787                        let err_str = e.to_string();
788                        if err_str.contains("errno 2") || err_str.contains("os error 2") {
789                            warn!("Performance counters not available on this CPU architecture");
790                            warn!("PMU event '{}' not supported - scheduler will run without perf monitoring", opts.perf_sticky.display_name);
791                        } else {
792                            warn!("Failed to setup perf events: {}", e);
793                        }
794                        perf_available = false;
795                        break;
796                    }
797                }
798            }
799        }
800        if perf_available {
801            info!("Performance counters configured successfully for all CPUs");
802        }
803
804        // Configure GPU->node mapping.
805        if opts.gpu && numa_enabled {
806            for (id, gpu) in topo.gpus() {
807                let GpuIndex::Nvidia { nvml_id } = id;
808                if opts.verbose {
809                    info!("GPU{} -> node{}", nvml_id, gpu.node_id);
810                }
811                skel.maps.gpu_node_map.update(
812                    &(nvml_id as u32).to_ne_bytes(),
813                    &(gpu.node_id as u32).to_ne_bytes(),
814                    MapFlags::ANY,
815                )?;
816            }
817        }
818
819        // Enable primary scheduling domain, if defined.
820        if primary_cpus.len() < *NR_CPU_IDS {
821            for cpu in primary_cpus {
822                if let Err(err) = Self::enable_primary_cpu(&mut skel, cpu as i32) {
823                    bail!("failed to add CPU {} to primary domain: error {}", cpu, err);
824                }
825            }
826        }
827
828        // Initialize SMT domains.
829        if smt_enabled {
830            Self::init_smt_domains(&mut skel, &topo)?;
831        }
832
833        // Attach the scheduler.
834        let struct_ops = Some(scx_ops_attach!(skel, cosmos_ops)?);
835        let stats_server = StatsServer::new(stats::server_data()).launch()?;
836
837        // Initialize dynamic threshold states for perf events (only when using dynamic mode).
838        let perf_threshold_state = if opts.perf_config.event_id > 0 && opts.perf_threshold == 0 {
839            Some(DynamicThresholdState::new(DYNAMIC_THRESHOLD_INIT_VALUE))
840        } else {
841            None
842        };
843        let perf_sticky_threshold_state =
844            if opts.perf_sticky.event_id > 0 && opts.perf_sticky_threshold == 0 {
845                Some(DynamicThresholdState::new(DYNAMIC_THRESHOLD_INIT_VALUE))
846            } else {
847                None
848            };
849
850        Ok(Self {
851            skel,
852            opts,
853            struct_ops,
854            stats_server,
855            gpu_index_to_node,
856            previous_gpu_pids,
857            nvml,
858            gpu_cgroup_reader,
859            perf_threshold_state,
860            perf_sticky_threshold_state,
861        })
862    }
863
864    /// Sync process TGID -> GPU (node) hints from NVML. Peer processes in the same exact,
865    /// non-root cgroup inherit the hint when all observed GPUs resolve to one NUMA node.
866    /// gpu_util_threshold > 0 retains process-only behavior because its NVML snapshot is
867    /// intentionally filtered.
868    fn sync_gpu_pids(&mut self) -> Result<()> {
869        let gpu_index_to_node = match &self.gpu_index_to_node {
870            Some(m) => m,
871            None => return Ok(()),
872        };
873        let nvml = match &self.nvml {
874            Some(n) => n,
875            None => return Ok(()),
876        };
877        let threshold = self.opts.gpu_util_threshold;
878        // First collect TGID -> set of nodes (GPUs) per process.
879        let mut pid_to_nodes: HashMap<u32, HashSet<u32>> = HashMap::new();
880        let mut snapshot_complete = true;
881
882        // A failed NVML query is not an empty snapshot. Keep the last applied
883        // hints instead of deleting processes we simply failed to observe.
884        let count = nvml.device_count().context("NVML device count")?;
885        for i in 0..count {
886            let node = match gpu_index_to_node.get(&i) {
887                Some(&n) => n,
888                None => {
889                    snapshot_complete = false;
890                    continue;
891                }
892            };
893            let device = match nvml.device_by_index(i) {
894                Ok(device) => device,
895                Err(error) => {
896                    debug!("NVML device {i} lookup failed: {error:#}");
897                    return Err(error).context(format!("NVML device {i} lookup failed"));
898                }
899            };
900
901            if threshold > 0 {
902                // Use process utilization; only add PIDs above threshold.
903                match device.process_utilization_stats(None::<u64>) {
904                    Ok(samples) => {
905                        for sample in samples {
906                            let util = sample.sm_util.max(sample.mem_util);
907                            if util >= threshold {
908                                pid_to_nodes.entry(sample.pid).or_default().insert(node);
909                            }
910                        }
911                    }
912                    Err(_) => {
913                        // NotSupported or other: fall back to all running processes.
914                        Self::add_running_gpu_processes_to_set(&device, node, &mut pid_to_nodes)
915                            .with_context(|| {
916                                format!("NVML device {i} process snapshot is incomplete")
917                            })?;
918                    }
919                }
920            } else {
921                Self::add_running_gpu_processes_to_set(&device, node, &mut pid_to_nodes)
922                    .with_context(|| format!("NVML device {i} process snapshot is incomplete"))?;
923            }
924        }
925
926        let mut direct = gpu::direct_gpu_processes(&pid_to_nodes);
927        direct.remove(&std::process::id());
928        let mut current = if snapshot_complete {
929            self.gpu_cgroup_reader
930                .as_ref()
931                .map(|reader| gpu::expand_gpu_processes(&pid_to_nodes, reader))
932                .unwrap_or_else(|| direct.clone())
933        } else {
934            direct.clone()
935        };
936        current.remove(&std::process::id());
937        let max_entries = self.skel.maps.gpu_pid_map.max_entries() as usize;
938
939        // Workload discovery can include many peer processes. Fall back to
940        // direct NVML processes rather than partially populating the map.
941        if current.len() > max_entries {
942            warn!(
943                "GPU workload has {} processes, exceeding gpu_pid_map capacity {}; using {} direct NVML processes",
944                current.len(),
945                max_entries,
946                direct.len()
947            );
948        }
949        if direct.len() > max_entries {
950            warn!(
951                "{} direct NVML processes exceed gpu_pid_map capacity {}; clearing GPU process hints",
952                direct.len(),
953                max_entries
954            );
955        }
956        current = gpu::fit_gpu_processes(current, &direct, max_entries);
957
958        self.reconcile_gpu_pid_hints(&current)
959    }
960
961    /// Reconcile the desired GPU workload hints with the BPF map.
962    fn reconcile_gpu_pid_hints(&mut self, desired: &HashMap<u32, u32>) -> Result<()> {
963        let previous = self.previous_gpu_pids.as_ref().unwrap().clone();
964        let map = &self.skel.maps.gpu_pid_map;
965
966        // Track the state actually applied to BPF so a partial map operation
967        // is retried and cleaned up on the next synchronization.
968        let mut applied = previous.clone();
969        let mut first_error = None;
970
971        // Delete stale entries first so replacements cannot temporarily run
972        // out of map capacity.
973        for pid in previous.keys() {
974            if desired.contains_key(pid) {
975                continue;
976            }
977            match map.delete(&pid.to_ne_bytes()).context("gpu_pid_map delete") {
978                Ok(()) => {
979                    applied.remove(pid);
980                }
981                Err(error) => {
982                    if first_error.is_none() {
983                        first_error = Some(error);
984                    }
985                }
986            }
987        }
988        for (pid, node) in desired {
989            match map
990                .update(&pid.to_ne_bytes(), &node.to_ne_bytes(), MapFlags::ANY)
991                .context("gpu_pid_map update")
992            {
993                Ok(()) => {
994                    applied.insert(*pid, *node);
995                }
996                Err(error) => {
997                    if first_error.is_none() {
998                        first_error = Some(error);
999                    }
1000                }
1001            }
1002        }
1003        *self.previous_gpu_pids.as_mut().unwrap() = applied;
1004        if let Some(error) = first_error {
1005            return Err(error);
1006        }
1007        Ok(())
1008    }
1009
1010    /// Record running compute/graphics process TGIDs and the GPU node in pid_to_nodes.
1011    fn add_running_gpu_processes_to_set(
1012        device: &nvml_wrapper::Device<'_>,
1013        node: u32,
1014        pid_to_nodes: &mut HashMap<u32, HashSet<u32>>,
1015    ) -> Result<()> {
1016        let mut errors = Vec::new();
1017
1018        match device.running_compute_processes() {
1019            Ok(processes) => {
1020                for process in processes {
1021                    pid_to_nodes.entry(process.pid).or_default().insert(node);
1022                }
1023            }
1024            Err(error) => errors.push(format!("compute process query failed: {error}")),
1025        }
1026        match device.running_graphics_processes() {
1027            Ok(processes) => {
1028                for process in processes {
1029                    pid_to_nodes.entry(process.pid).or_default().insert(node);
1030                }
1031            }
1032            Err(error) => errors.push(format!("graphics process query failed: {error}")),
1033        }
1034
1035        if errors.is_empty() {
1036            Ok(())
1037        } else {
1038            bail!(errors.join("; "))
1039        }
1040    }
1041
1042    fn enable_primary_cpu(skel: &mut BpfSkel<'_>, cpu: i32) -> Result<(), u32> {
1043        let prog = &mut skel.progs.enable_primary_cpu;
1044        let mut args = cpu_arg {
1045            cpu_id: cpu as c_int,
1046        };
1047        let input = ProgramInput {
1048            context_in: Some(unsafe {
1049                std::slice::from_raw_parts_mut(
1050                    &mut args as *mut _ as *mut u8,
1051                    std::mem::size_of_val(&args),
1052                )
1053            }),
1054            ..Default::default()
1055        };
1056        let out = prog.test_run(input).unwrap();
1057        if out.return_value != 0 {
1058            return Err(out.return_value);
1059        }
1060
1061        Ok(())
1062    }
1063
1064    fn enable_sibling_cpu(
1065        skel: &mut BpfSkel<'_>,
1066        cpu: usize,
1067        sibling_cpu: usize,
1068    ) -> Result<(), u32> {
1069        let prog = &mut skel.progs.enable_sibling_cpu;
1070        let mut args = domain_arg {
1071            cpu_id: cpu as c_int,
1072            sibling_cpu_id: sibling_cpu as c_int,
1073        };
1074        let input = ProgramInput {
1075            context_in: Some(unsafe {
1076                std::slice::from_raw_parts_mut(
1077                    &mut args as *mut _ as *mut u8,
1078                    std::mem::size_of_val(&args),
1079                )
1080            }),
1081            ..Default::default()
1082        };
1083        let out = prog.test_run(input).unwrap();
1084        if out.return_value != 0 {
1085            return Err(out.return_value);
1086        }
1087
1088        Ok(())
1089    }
1090
1091    fn init_smt_domains(skel: &mut BpfSkel<'_>, topo: &Topology) -> Result<(), std::io::Error> {
1092        let smt_siblings = topo.sibling_cpus();
1093
1094        info!("SMT sibling CPUs: {:?}", smt_siblings);
1095        for (cpu, sibling_cpu) in smt_siblings.iter().enumerate() {
1096            Self::enable_sibling_cpu(skel, cpu, *sibling_cpu as usize).unwrap();
1097        }
1098
1099        Ok(())
1100    }
1101
1102    fn get_metrics(&self) -> Metrics {
1103        let bss_data = self.skel.maps.bss_data.as_ref().unwrap();
1104        Metrics {
1105            nr_event_dispatches: bss_data.nr_event_dispatches,
1106            nr_ev_sticky_dispatches: bss_data.nr_ev_sticky_dispatches,
1107            nr_gpu_dispatches: bss_data.nr_gpu_dispatches,
1108            nr_overload_events: bss_data.nr_overload_events,
1109        }
1110    }
1111
1112    pub fn exited(&mut self) -> bool {
1113        uei_exited!(&self.skel, uei)
1114    }
1115
1116    fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
1117        let (res_ch, req_ch) = self.stats_server.channels();
1118
1119        // Periodic (option -p) updates of the dynamic perf thresholds.
1120        let polling_time = Duration::from_millis(self.opts.polling_ms).min(Duration::from_secs(1));
1121        let mut last_update = Instant::now();
1122        let mut last_gpu_sync = Instant::now();
1123
1124        while !shutdown.load(Ordering::Relaxed) && !self.exited() {
1125            if !polling_time.is_zero() && last_update.elapsed() >= polling_time {
1126                // Update dynamic perf thresholds using EMA + hysteresis.
1127                let elapsed_secs = last_update.elapsed().as_secs_f64();
1128
1129                // Update migration threshold state if dynamic mode is enabled.
1130                if let Some(ref mut state) = self.perf_threshold_state {
1131                    let nr_event = self
1132                        .skel
1133                        .maps
1134                        .bss_data
1135                        .as_ref()
1136                        .unwrap()
1137                        .nr_event_dispatches;
1138                    if let Some(new_thresh) =
1139                        state.update(nr_event, elapsed_secs, self.opts.verbose, "perf_threshold")
1140                    {
1141                        self.skel.maps.bss_data.as_mut().unwrap().perf_threshold = new_thresh;
1142                    }
1143                }
1144
1145                // Update sticky threshold state if dynamic mode is enabled.
1146                if let Some(ref mut state) = self.perf_sticky_threshold_state {
1147                    let nr_sticky = self
1148                        .skel
1149                        .maps
1150                        .bss_data
1151                        .as_ref()
1152                        .unwrap()
1153                        .nr_ev_sticky_dispatches;
1154                    if let Some(new_thresh) = state.update(
1155                        nr_sticky,
1156                        elapsed_secs,
1157                        self.opts.verbose,
1158                        "perf_sticky_threshold",
1159                    ) {
1160                        self.skel
1161                            .maps
1162                            .bss_data
1163                            .as_mut()
1164                            .unwrap()
1165                            .perf_sticky_threshold = new_thresh;
1166                    }
1167                }
1168
1169                last_update = Instant::now();
1170            }
1171
1172            // GPU PID sync is throttled to GPU_SYNC_INTERVAL.
1173            if self.gpu_index_to_node.is_some() && last_gpu_sync.elapsed() >= GPU_SYNC_INTERVAL {
1174                if let Err(e) = self.sync_gpu_pids() {
1175                    debug!("GPU PID sync: {}", e);
1176                }
1177                last_gpu_sync = Instant::now();
1178            }
1179
1180            // Update statistics and check for exit condition.
1181            let timeout = if polling_time.is_zero() {
1182                Duration::from_secs(1)
1183            } else {
1184                polling_time
1185            };
1186            match req_ch.recv_timeout(timeout) {
1187                Ok(()) => res_ch.send(self.get_metrics())?,
1188                Err(RecvTimeoutError::Timeout) => {}
1189                Err(e) => Err(e)?,
1190            }
1191        }
1192
1193        let _ = self.struct_ops.take();
1194        uei_report!(&self.skel, uei)
1195    }
1196}
1197
1198impl Drop for Scheduler<'_> {
1199    fn drop(&mut self) {
1200        info!("Unregister {SCHEDULER_NAME} scheduler");
1201    }
1202}
1203
1204fn main() -> Result<()> {
1205    let opts = Opts::parse();
1206
1207    if opts.version {
1208        println!(
1209            "{} {}",
1210            SCHEDULER_NAME,
1211            build_id::full_version(env!("CARGO_PKG_VERSION"))
1212        );
1213        return Ok(());
1214    }
1215
1216    if opts.help_stats {
1217        stats::server_data().describe_meta(&mut std::io::stdout(), None)?;
1218        return Ok(());
1219    }
1220
1221    let loglevel = simplelog::LevelFilter::Info;
1222
1223    let mut lcfg = simplelog::ConfigBuilder::new();
1224    lcfg.set_time_offset_to_local()
1225        .expect("Failed to set local time offset")
1226        .set_time_level(simplelog::LevelFilter::Error)
1227        .set_location_level(simplelog::LevelFilter::Off)
1228        .set_target_level(simplelog::LevelFilter::Off)
1229        .set_thread_level(simplelog::LevelFilter::Off);
1230    simplelog::TermLogger::init(
1231        loglevel,
1232        lcfg.build(),
1233        simplelog::TerminalMode::Stderr,
1234        simplelog::ColorChoice::Auto,
1235    )?;
1236
1237    let shutdown = Arc::new(AtomicBool::new(false));
1238    let shutdown_clone = shutdown.clone();
1239    ctrlc::set_handler(move || {
1240        shutdown_clone.store(true, Ordering::Relaxed);
1241    })
1242    .context("Error setting Ctrl-C handler")?;
1243
1244    if let Some(intv) = opts.monitor.or(opts.stats) {
1245        let shutdown_copy = shutdown.clone();
1246        let jh = std::thread::spawn(move || {
1247            match stats::monitor(Duration::from_secs_f64(intv), shutdown_copy) {
1248                Ok(_) => {
1249                    debug!("stats monitor thread finished successfully")
1250                }
1251                Err(error_object) => {
1252                    warn!(
1253                        "stats monitor thread finished because of an error {}",
1254                        error_object
1255                    )
1256                }
1257            }
1258        });
1259        if opts.monitor.is_some() {
1260            let _ = jh.join();
1261            return Ok(());
1262        }
1263    }
1264
1265    let mut open_object = MaybeUninit::uninit();
1266    loop {
1267        let mut sched = Scheduler::init(&opts, &mut open_object)?;
1268        if !sched.run(shutdown.clone())?.should_restart() {
1269            break;
1270        }
1271    }
1272
1273    Ok(())
1274}