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