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::Arc;
22use std::sync::atomic::AtomicBool;
23use std::sync::atomic::Ordering;
24use std::time::{Duration, Instant};
25
26use anyhow::Context;
27use anyhow::Result;
28use anyhow::bail;
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::Nvml;
37use nvml_wrapper::bitmasks::InitFlags;
38use scx_stats::prelude::*;
39use scx_utils::GpuIndex;
40use scx_utils::NR_CPU_IDS;
41use scx_utils::Powermode;
42use scx_utils::Topology;
43use scx_utils::UserExitInfo;
44use scx_utils::build_id;
45use scx_utils::compat;
46use scx_utils::get_primary_cpus;
47use scx_utils::libbpf_clap_opts::LibbpfOpts;
48use scx_utils::perf::PerfEventSpec;
49use scx_utils::perf::parse_perf_event;
50use scx_utils::perf::setup_perf_events;
51use scx_utils::scx_ops_attach;
52use scx_utils::scx_ops_load;
53use scx_utils::scx_ops_open;
54use scx_utils::try_set_rlimit_infinity;
55use scx_utils::uei_exited;
56use scx_utils::uei_report;
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 (DYNAMIC_THRESHOLD_RATE_LOW..=DYNAMIC_THRESHOLD_RATE_HIGH).contains(&rate) {
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                && let Err(e) =
764                    setup_perf_events(&skel.maps.scx_pmu_map, cpu as i32, &opts.perf_config, 0)
765                && cpu == 0
766            {
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!(
771                        "PMU event '{}' not supported - scheduler will run without perf monitoring",
772                        opts.perf_config.display_name
773                    );
774                } else {
775                    warn!("Failed to setup perf events: {}", e);
776                }
777                perf_available = false;
778                break;
779            }
780            if opts.perf_sticky.event_id > 0
781                && let Err(e) = setup_perf_events(
782                    &skel.maps.scx_pmu_map,
783                    cpu as i32,
784                    &opts.perf_sticky,
785                    sticky_counter_idx,
786                )
787                && cpu == 0
788            {
789                let err_str = e.to_string();
790                if err_str.contains("errno 2") || err_str.contains("os error 2") {
791                    warn!("Performance counters not available on this CPU architecture");
792                    warn!(
793                        "PMU event '{}' not supported - scheduler will run without perf monitoring",
794                        opts.perf_sticky.display_name
795                    );
796                } else {
797                    warn!("Failed to setup perf events: {}", e);
798                }
799                perf_available = false;
800                break;
801            }
802        }
803        if perf_available {
804            info!("Performance counters configured successfully for all CPUs");
805        }
806
807        // Configure GPU->node mapping.
808        if opts.gpu && numa_enabled {
809            for (id, gpu) in topo.gpus() {
810                let GpuIndex::Nvidia { nvml_id } = id;
811                if opts.verbose {
812                    info!("GPU{} -> node{}", nvml_id, gpu.node_id);
813                }
814                skel.maps.gpu_node_map.update(
815                    &nvml_id.to_ne_bytes(),
816                    &(gpu.node_id as u32).to_ne_bytes(),
817                    MapFlags::ANY,
818                )?;
819            }
820        }
821
822        // Enable primary scheduling domain, if defined.
823        if primary_cpus.len() < *NR_CPU_IDS {
824            for cpu in primary_cpus {
825                if let Err(err) = Self::enable_primary_cpu(&mut skel, cpu as i32) {
826                    bail!("failed to add CPU {} to primary domain: error {}", cpu, err);
827                }
828            }
829        }
830
831        // Initialize SMT domains.
832        if smt_enabled {
833            Self::init_smt_domains(&mut skel, &topo)?;
834        }
835
836        // Attach the scheduler.
837        let struct_ops = Some(scx_ops_attach!(skel, cosmos_ops)?);
838        let stats_server = StatsServer::new(stats::server_data()).launch()?;
839
840        // Initialize dynamic threshold states for perf events (only when using dynamic mode).
841        let perf_threshold_state = if opts.perf_config.event_id > 0 && opts.perf_threshold == 0 {
842            Some(DynamicThresholdState::new(DYNAMIC_THRESHOLD_INIT_VALUE))
843        } else {
844            None
845        };
846        let perf_sticky_threshold_state =
847            if opts.perf_sticky.event_id > 0 && opts.perf_sticky_threshold == 0 {
848                Some(DynamicThresholdState::new(DYNAMIC_THRESHOLD_INIT_VALUE))
849            } else {
850                None
851            };
852
853        Ok(Self {
854            skel,
855            opts,
856            struct_ops,
857            stats_server,
858            gpu_index_to_node,
859            previous_gpu_pids,
860            nvml,
861            gpu_cgroup_reader,
862            perf_threshold_state,
863            perf_sticky_threshold_state,
864        })
865    }
866
867    /// Sync process TGID -> GPU (node) hints from NVML. Peer processes in the same exact,
868    /// non-root cgroup inherit the hint when all observed GPUs resolve to one NUMA node.
869    /// gpu_util_threshold > 0 retains process-only behavior because its NVML snapshot is
870    /// intentionally filtered.
871    fn sync_gpu_pids(&mut self) -> Result<()> {
872        let gpu_index_to_node = match &self.gpu_index_to_node {
873            Some(m) => m,
874            None => return Ok(()),
875        };
876        let nvml = match &self.nvml {
877            Some(n) => n,
878            None => return Ok(()),
879        };
880        let threshold = self.opts.gpu_util_threshold;
881        // First collect TGID -> set of nodes (GPUs) per process.
882        let mut pid_to_nodes: HashMap<u32, HashSet<u32>> = HashMap::new();
883        let mut snapshot_complete = true;
884
885        // A failed NVML query is not an empty snapshot. Keep the last applied
886        // hints instead of deleting processes we simply failed to observe.
887        let count = nvml.device_count().context("NVML device count")?;
888        for i in 0..count {
889            let node = match gpu_index_to_node.get(&i) {
890                Some(&n) => n,
891                None => {
892                    snapshot_complete = false;
893                    continue;
894                }
895            };
896            let device = match nvml.device_by_index(i) {
897                Ok(device) => device,
898                Err(error) => {
899                    debug!("NVML device {i} lookup failed: {error:#}");
900                    return Err(error).context(format!("NVML device {i} lookup failed"));
901                }
902            };
903
904            if threshold > 0 {
905                // Use process utilization; only add PIDs above threshold.
906                match device.process_utilization_stats(None::<u64>) {
907                    Ok(samples) => {
908                        for sample in samples {
909                            let util = sample.sm_util.max(sample.mem_util);
910                            if util >= threshold {
911                                pid_to_nodes.entry(sample.pid).or_default().insert(node);
912                            }
913                        }
914                    }
915                    Err(_) => {
916                        // NotSupported or other: fall back to all running processes.
917                        Self::add_running_gpu_processes_to_set(&device, node, &mut pid_to_nodes)
918                            .with_context(|| {
919                                format!("NVML device {i} process snapshot is incomplete")
920                            })?;
921                    }
922                }
923            } else {
924                Self::add_running_gpu_processes_to_set(&device, node, &mut pid_to_nodes)
925                    .with_context(|| format!("NVML device {i} process snapshot is incomplete"))?;
926            }
927        }
928
929        let mut direct = gpu::direct_gpu_processes(&pid_to_nodes);
930        direct.remove(&std::process::id());
931        let mut current = if snapshot_complete {
932            self.gpu_cgroup_reader
933                .as_ref()
934                .map(|reader| gpu::expand_gpu_processes(&pid_to_nodes, reader))
935                .unwrap_or_else(|| direct.clone())
936        } else {
937            direct.clone()
938        };
939        current.remove(&std::process::id());
940        let max_entries = self.skel.maps.gpu_pid_map.max_entries() as usize;
941
942        // Workload discovery can include many peer processes. Fall back to
943        // direct NVML processes rather than partially populating the map.
944        if current.len() > max_entries {
945            warn!(
946                "GPU workload has {} processes, exceeding gpu_pid_map capacity {}; using {} direct NVML processes",
947                current.len(),
948                max_entries,
949                direct.len()
950            );
951        }
952        if direct.len() > max_entries {
953            warn!(
954                "{} direct NVML processes exceed gpu_pid_map capacity {}; clearing GPU process hints",
955                direct.len(),
956                max_entries
957            );
958        }
959        current = gpu::fit_gpu_processes(current, &direct, max_entries);
960
961        self.reconcile_gpu_pid_hints(&current)
962    }
963
964    /// Reconcile the desired GPU workload hints with the BPF map.
965    fn reconcile_gpu_pid_hints(&mut self, desired: &HashMap<u32, u32>) -> Result<()> {
966        let previous = self.previous_gpu_pids.as_ref().unwrap().clone();
967        let map = &self.skel.maps.gpu_pid_map;
968
969        // Track the state actually applied to BPF so a partial map operation
970        // is retried and cleaned up on the next synchronization.
971        let mut applied = previous.clone();
972        let mut first_error = None;
973
974        // Delete stale entries first so replacements cannot temporarily run
975        // out of map capacity.
976        for pid in previous.keys() {
977            if desired.contains_key(pid) {
978                continue;
979            }
980            match map.delete(&pid.to_ne_bytes()).context("gpu_pid_map delete") {
981                Ok(()) => {
982                    applied.remove(pid);
983                }
984                Err(error) => {
985                    if first_error.is_none() {
986                        first_error = Some(error);
987                    }
988                }
989            }
990        }
991        for (pid, node) in desired {
992            match map
993                .update(&pid.to_ne_bytes(), &node.to_ne_bytes(), MapFlags::ANY)
994                .context("gpu_pid_map update")
995            {
996                Ok(()) => {
997                    applied.insert(*pid, *node);
998                }
999                Err(error) => {
1000                    if first_error.is_none() {
1001                        first_error = Some(error);
1002                    }
1003                }
1004            }
1005        }
1006        *self.previous_gpu_pids.as_mut().unwrap() = applied;
1007        if let Some(error) = first_error {
1008            return Err(error);
1009        }
1010        Ok(())
1011    }
1012
1013    /// Record running compute/graphics process TGIDs and the GPU node in pid_to_nodes.
1014    fn add_running_gpu_processes_to_set(
1015        device: &nvml_wrapper::Device<'_>,
1016        node: u32,
1017        pid_to_nodes: &mut HashMap<u32, HashSet<u32>>,
1018    ) -> Result<()> {
1019        let mut errors = Vec::new();
1020
1021        match device.running_compute_processes() {
1022            Ok(processes) => {
1023                for process in processes {
1024                    pid_to_nodes.entry(process.pid).or_default().insert(node);
1025                }
1026            }
1027            Err(error) => errors.push(format!("compute process query failed: {error}")),
1028        }
1029        match device.running_graphics_processes() {
1030            Ok(processes) => {
1031                for process in processes {
1032                    pid_to_nodes.entry(process.pid).or_default().insert(node);
1033                }
1034            }
1035            Err(error) => errors.push(format!("graphics process query failed: {error}")),
1036        }
1037
1038        if errors.is_empty() {
1039            Ok(())
1040        } else {
1041            bail!(errors.join("; "))
1042        }
1043    }
1044
1045    fn enable_primary_cpu(skel: &mut BpfSkel<'_>, cpu: i32) -> Result<(), u32> {
1046        let prog = &mut skel.progs.enable_primary_cpu;
1047        let mut args = cpu_arg {
1048            cpu_id: cpu as c_int,
1049        };
1050        let input = ProgramInput {
1051            context_in: Some(unsafe {
1052                std::slice::from_raw_parts_mut(
1053                    &mut args as *mut _ as *mut u8,
1054                    std::mem::size_of_val(&args),
1055                )
1056            }),
1057            ..Default::default()
1058        };
1059        let out = prog.test_run(input).unwrap();
1060        if out.return_value != 0 {
1061            return Err(out.return_value);
1062        }
1063
1064        Ok(())
1065    }
1066
1067    fn enable_sibling_cpu(
1068        skel: &mut BpfSkel<'_>,
1069        cpu: usize,
1070        sibling_cpu: usize,
1071    ) -> Result<(), u32> {
1072        let prog = &mut skel.progs.enable_sibling_cpu;
1073        let mut args = domain_arg {
1074            cpu_id: cpu as c_int,
1075            sibling_cpu_id: sibling_cpu as c_int,
1076        };
1077        let input = ProgramInput {
1078            context_in: Some(unsafe {
1079                std::slice::from_raw_parts_mut(
1080                    &mut args as *mut _ as *mut u8,
1081                    std::mem::size_of_val(&args),
1082                )
1083            }),
1084            ..Default::default()
1085        };
1086        let out = prog.test_run(input).unwrap();
1087        if out.return_value != 0 {
1088            return Err(out.return_value);
1089        }
1090
1091        Ok(())
1092    }
1093
1094    fn init_smt_domains(skel: &mut BpfSkel<'_>, topo: &Topology) -> Result<(), std::io::Error> {
1095        let smt_siblings = topo.sibling_cpus();
1096
1097        info!("SMT sibling CPUs: {:?}", smt_siblings);
1098        for (cpu, sibling_cpu) in smt_siblings.iter().enumerate() {
1099            Self::enable_sibling_cpu(skel, cpu, *sibling_cpu as usize).unwrap();
1100        }
1101
1102        Ok(())
1103    }
1104
1105    fn get_metrics(&self) -> Metrics {
1106        let bss_data = self.skel.maps.bss_data.as_ref().unwrap();
1107        Metrics {
1108            nr_event_dispatches: bss_data.nr_event_dispatches,
1109            nr_ev_sticky_dispatches: bss_data.nr_ev_sticky_dispatches,
1110            nr_gpu_dispatches: bss_data.nr_gpu_dispatches,
1111            nr_overload_events: bss_data.nr_overload_events,
1112        }
1113    }
1114
1115    pub fn exited(&mut self) -> bool {
1116        uei_exited!(&self.skel, uei)
1117    }
1118
1119    fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
1120        let (res_ch, req_ch) = self.stats_server.channels();
1121
1122        // Periodic (option -p) updates of the dynamic perf thresholds.
1123        let polling_time = Duration::from_millis(self.opts.polling_ms).min(Duration::from_secs(1));
1124        let mut last_update = Instant::now();
1125        let mut last_gpu_sync = Instant::now();
1126
1127        while !shutdown.load(Ordering::Relaxed) && !self.exited() {
1128            if !polling_time.is_zero() && last_update.elapsed() >= polling_time {
1129                // Update dynamic perf thresholds using EMA + hysteresis.
1130                let elapsed_secs = last_update.elapsed().as_secs_f64();
1131
1132                // Update migration threshold state if dynamic mode is enabled.
1133                if let Some(ref mut state) = self.perf_threshold_state {
1134                    let nr_event = self
1135                        .skel
1136                        .maps
1137                        .bss_data
1138                        .as_ref()
1139                        .unwrap()
1140                        .nr_event_dispatches;
1141                    if let Some(new_thresh) =
1142                        state.update(nr_event, elapsed_secs, self.opts.verbose, "perf_threshold")
1143                    {
1144                        self.skel.maps.bss_data.as_mut().unwrap().perf_threshold = new_thresh;
1145                    }
1146                }
1147
1148                // Update sticky threshold state if dynamic mode is enabled.
1149                if let Some(ref mut state) = self.perf_sticky_threshold_state {
1150                    let nr_sticky = self
1151                        .skel
1152                        .maps
1153                        .bss_data
1154                        .as_ref()
1155                        .unwrap()
1156                        .nr_ev_sticky_dispatches;
1157                    if let Some(new_thresh) = state.update(
1158                        nr_sticky,
1159                        elapsed_secs,
1160                        self.opts.verbose,
1161                        "perf_sticky_threshold",
1162                    ) {
1163                        self.skel
1164                            .maps
1165                            .bss_data
1166                            .as_mut()
1167                            .unwrap()
1168                            .perf_sticky_threshold = new_thresh;
1169                    }
1170                }
1171
1172                last_update = Instant::now();
1173            }
1174
1175            // GPU PID sync is throttled to GPU_SYNC_INTERVAL.
1176            if self.gpu_index_to_node.is_some() && last_gpu_sync.elapsed() >= GPU_SYNC_INTERVAL {
1177                if let Err(e) = self.sync_gpu_pids() {
1178                    debug!("GPU PID sync: {}", e);
1179                }
1180                last_gpu_sync = Instant::now();
1181            }
1182
1183            // Update statistics and check for exit condition.
1184            let timeout = if polling_time.is_zero() {
1185                Duration::from_secs(1)
1186            } else {
1187                polling_time
1188            };
1189            match req_ch.recv_timeout(timeout) {
1190                Ok(()) => res_ch.send(self.get_metrics())?,
1191                Err(RecvTimeoutError::Timeout) => {}
1192                Err(e) => Err(e)?,
1193            }
1194        }
1195
1196        let _ = self.struct_ops.take();
1197        uei_report!(&self.skel, uei)
1198    }
1199}
1200
1201impl Drop for Scheduler<'_> {
1202    fn drop(&mut self) {
1203        info!("Unregister {SCHEDULER_NAME} scheduler");
1204    }
1205}
1206
1207fn main() -> Result<()> {
1208    let opts = Opts::parse();
1209
1210    if opts.version {
1211        println!(
1212            "{} {}",
1213            SCHEDULER_NAME,
1214            build_id::full_version(env!("CARGO_PKG_VERSION"))
1215        );
1216        return Ok(());
1217    }
1218
1219    if opts.help_stats {
1220        stats::server_data().describe_meta(&mut std::io::stdout(), None)?;
1221        return Ok(());
1222    }
1223
1224    let loglevel = simplelog::LevelFilter::Info;
1225
1226    let mut lcfg = simplelog::ConfigBuilder::new();
1227    lcfg.set_time_offset_to_local()
1228        .expect("Failed to set local time offset")
1229        .set_time_level(simplelog::LevelFilter::Error)
1230        .set_location_level(simplelog::LevelFilter::Off)
1231        .set_target_level(simplelog::LevelFilter::Off)
1232        .set_thread_level(simplelog::LevelFilter::Off);
1233    simplelog::TermLogger::init(
1234        loglevel,
1235        lcfg.build(),
1236        simplelog::TerminalMode::Stderr,
1237        simplelog::ColorChoice::Auto,
1238    )?;
1239
1240    let shutdown = Arc::new(AtomicBool::new(false));
1241    let shutdown_clone = shutdown.clone();
1242    ctrlc::set_handler(move || {
1243        shutdown_clone.store(true, Ordering::Relaxed);
1244    })
1245    .context("Error setting Ctrl-C handler")?;
1246
1247    if let Some(intv) = opts.monitor.or(opts.stats) {
1248        let shutdown_copy = shutdown.clone();
1249        let jh = std::thread::spawn(move || {
1250            match stats::monitor(Duration::from_secs_f64(intv), shutdown_copy) {
1251                Ok(_) => {
1252                    debug!("stats monitor thread finished successfully")
1253                }
1254                Err(error_object) => {
1255                    warn!(
1256                        "stats monitor thread finished because of an error {}",
1257                        error_object
1258                    )
1259                }
1260            }
1261        });
1262        if opts.monitor.is_some() {
1263            let _ = jh.join();
1264            return Ok(());
1265        }
1266    }
1267
1268    let mut open_object = MaybeUninit::uninit();
1269    loop {
1270        let mut sched = Scheduler::init(&opts, &mut open_object)?;
1271        if !sched.run(shutdown.clone())?.should_restart() {
1272            break;
1273        }
1274    }
1275
1276    Ok(())
1277}