Skip to main content

scx_bpfland/
main.rs

1// SPDX-License-Identifier: GPL-2.0
2//
3// Copyright (c) 2024 Andrea Righi <andrea.righi@linux.dev>
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::ffi::{c_int, c_ulong};
15use std::fmt::Write;
16use std::mem::MaybeUninit;
17use std::sync::Arc;
18use std::sync::atomic::AtomicBool;
19use std::sync::atomic::Ordering;
20use std::time::Duration;
21
22use anyhow::Context;
23use anyhow::Result;
24use anyhow::anyhow;
25use anyhow::bail;
26use clap::Parser;
27use crossbeam::channel::RecvTimeoutError;
28use libbpf_rs::OpenObject;
29use libbpf_rs::ProgramInput;
30use log::warn;
31use log::{debug, info};
32use scx_stats::prelude::*;
33use scx_utils::Cpumask;
34use scx_utils::NR_CPU_IDS;
35use scx_utils::Powermode;
36use scx_utils::Topology;
37use scx_utils::UserExitInfo;
38use scx_utils::autopower::{PowerProfile, fetch_power_profile};
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::pm::{cpu_idle_resume_latency_supported, update_cpu_idle_resume_latency};
44use scx_utils::scx_ops_attach;
45use scx_utils::scx_ops_load;
46use scx_utils::scx_ops_open;
47use scx_utils::try_set_rlimit_infinity;
48use scx_utils::uei_exited;
49use scx_utils::uei_report;
50use stats::Metrics;
51
52const SCHEDULER_NAME: &str = "scx_bpfland";
53
54// Convert an array of CPUs to the corresponding cpumask of any arbitrary size.
55fn cpus_to_cpumask(cpus: &Vec<usize>) -> String {
56    if cpus.is_empty() {
57        return String::from("none");
58    }
59
60    // Determine the maximum CPU ID to create a sufficiently large byte vector.
61    let max_cpu_id = *cpus.iter().max().unwrap();
62
63    // Create a byte vector with enough bytes to cover all CPU IDs.
64    let mut bitmask = vec![0u8; (max_cpu_id + 1).div_ceil(8)];
65
66    // Set the appropriate bits for each CPU ID.
67    for cpu_id in cpus {
68        let byte_index = cpu_id / 8;
69        let bit_index = cpu_id % 8;
70        bitmask[byte_index] |= 1 << bit_index;
71    }
72
73    // Convert the byte vector to a hexadecimal string.
74    let hex_str: String = bitmask.iter().rev().fold(String::new(), |mut f, byte| {
75        let _ = write!(&mut f, "{:02x}", byte);
76        f
77    });
78
79    format!("0x{}", hex_str)
80}
81
82/// scx_bpfland: a vruntime-based sched_ext scheduler that prioritizes interactive workloads.
83///
84/// This scheduler is derived from scx_rustland, but it is fully implemented in BPF. It has a minimal
85/// user-space part written in Rust to process command line options, collect metrics and log out
86/// scheduling statistics.
87///
88/// The BPF part makes all the scheduling decisions (see src/bpf/main.bpf.c).
89#[derive(Debug, Parser)]
90struct Opts {
91    /// Exit debug dump buffer length. 0 indicates default.
92    #[clap(long, default_value = "0")]
93    exit_dump_len: u32,
94
95    /// Maximum scheduling slice duration in microseconds.
96    #[clap(short = 's', long, default_value = "1000")]
97    slice_us: u64,
98
99    /// Minimum scheduling slice duration in microseconds (0 = no minimum time slice).
100    #[clap(short = 'L', long, default_value = "0")]
101    slice_min_us: u64,
102
103    /// Maximum time slice lag in microseconds.
104    ///
105    /// A positive value can help to enhance the responsiveness of interactive tasks, but it can
106    /// also make performance more "spikey".
107    #[clap(short = 'l', long, default_value = "40000")]
108    slice_us_lag: u64,
109
110    /// Throttle the running CPUs by periodically injecting idle cycles.
111    ///
112    /// This option can help extend battery life on portable devices, reduce heating, fan noise
113    /// and overall energy consumption (0 = disable).
114    #[clap(short = 't', long, default_value = "0")]
115    throttle_us: u64,
116
117    /// Set CPU idle QoS resume latency in microseconds (-1 = disabled).
118    ///
119    /// Setting a lower latency value makes CPUs less likely to enter deeper idle states, enhancing
120    /// performance at the cost of higher power consumption. Alternatively, increasing the latency
121    /// value may reduce performance, but also improve power efficiency.
122    #[clap(short = 'I', long, allow_hyphen_values = true, default_value = "-1")]
123    idle_resume_us: i64,
124
125    /// Enable per-CPU tasks prioritization.
126    ///
127    /// This allows to prioritize per-CPU tasks that usually tend to be de-prioritized (since they
128    /// can't be migrated when their only usable CPU is busy). Enabling this option can introduce
129    /// unfairness and potentially trigger stalls, but it can improve performance of server-type
130    /// workloads (such as large parallel builds).
131    #[clap(short = 'p', long, action = clap::ArgAction::SetTrue)]
132    local_pcpu: bool,
133
134    /// Enable kthreads prioritization (EXPERIMENTAL).
135    ///
136    /// Enabling this can improve system performance, but it may also introduce noticeable
137    /// interactivity issues or unfairness in scenarios with high kthread activity, such as heavy
138    /// I/O or network traffic.
139    ///
140    /// Use it only when conducting specific experiments or if you have a clear understanding of
141    /// its implications.
142    #[clap(short = 'k', long, action = clap::ArgAction::SetTrue)]
143    local_kthreads: bool,
144
145    /// Disable direct dispatch during synchronous wakeups.
146    ///
147    /// Enabling this option can lead to a more uniform load distribution across available cores,
148    /// potentially improving performance in certain scenarios. However, it may come at the cost of
149    /// reduced efficiency for pipe-intensive workloads that benefit from tighter producer-consumer
150    /// coupling.
151    #[clap(short = 'w', long, action = clap::ArgAction::SetTrue)]
152    no_wake_sync: bool,
153
154    /// Enable sticky tasks.
155    ///
156    /// If enabled force tasks with a high rate of enqueues/sec to stay on the same CPU, to reduce
157    /// locking contention on the shared runqueues.
158    ///
159    /// This can help making the scheduler more robust with intensive scheduling workloads and
160    /// benchmarks, but it can negatively impact on latency.
161    #[clap(short = 'S', long, action = clap::ArgAction::SetTrue)]
162    sticky_tasks: bool,
163
164    /// Specifies the initial set of CPUs, represented as a bitmask in hex (e.g., 0xff), that the
165    /// scheduler will use to dispatch tasks, until the system becomes saturated, at which point
166    /// tasks may overflow to other available CPUs.
167    ///
168    /// Special values:
169    ///  - "auto" = automatically detect the CPUs based on the active power profile
170    ///  - "performance" = automatically detect and prioritize the fastest CPUs
171    ///  - "powersave" = automatically detect and prioritize the slowest CPUs
172    ///  - "all" = all CPUs assigned to the primary domain
173    ///  - "none" = no prioritization, tasks are dispatched on the first CPU available
174    #[clap(short = 'm', long, default_value = "auto")]
175    primary_domain: String,
176
177    /// Enable preferred idle CPU scanning.
178    ///
179    /// With this option enabled, the scheduler will prioritize assigning tasks to higher-ranked
180    /// cores before considering lower-ranked ones.
181    #[clap(short = 'P', long, action = clap::ArgAction::SetTrue)]
182    preferred_idle_scan: bool,
183
184    /// Disable SMT awareness.
185    #[clap(long, action = clap::ArgAction::SetTrue)]
186    disable_smt: bool,
187
188    /// Disable NUMA awareness.
189    #[clap(long, action = clap::ArgAction::SetTrue)]
190    disable_numa: bool,
191
192    /// Enable CPU frequency control (only with schedutil governor).
193    ///
194    /// With this option enabled the CPU frequency will be automatically scaled based on the load.
195    #[clap(short = 'f', long, action = clap::ArgAction::SetTrue)]
196    cpufreq: bool,
197
198    /// Enable stats monitoring with the specified interval.
199    #[clap(long)]
200    stats: Option<f64>,
201
202    /// Run in stats monitoring mode with the specified interval. Scheduler
203    /// is not launched.
204    #[clap(long)]
205    monitor: Option<f64>,
206
207    /// Enable BPF debugging via /sys/kernel/tracing/trace_pipe.
208    #[clap(short = 'd', long, action = clap::ArgAction::SetTrue)]
209    debug: bool,
210
211    /// Enable verbose output, including libbpf details.
212    #[clap(short = 'v', long, action = clap::ArgAction::SetTrue)]
213    verbose: bool,
214
215    /// Print scheduler version and exit.
216    #[clap(short = 'V', long, action = clap::ArgAction::SetTrue)]
217    version: bool,
218
219    /// Show descriptions for statistics.
220    #[clap(long)]
221    help_stats: bool,
222
223    #[clap(flatten, next_help_heading = "Libbpf Options")]
224    pub libbpf: LibbpfOpts,
225}
226
227struct Scheduler<'a> {
228    skel: BpfSkel<'a>,
229    struct_ops: Option<libbpf_rs::Link>,
230    opts: &'a Opts,
231    topo: Topology,
232    power_profile: PowerProfile,
233    stats_server: StatsServer<(), Metrics>,
234    user_restart: bool,
235}
236
237impl<'a> Scheduler<'a> {
238    fn init(opts: &'a Opts, open_object: &'a mut MaybeUninit<OpenObject>) -> Result<Self> {
239        try_set_rlimit_infinity();
240
241        // Initialize CPU topology.
242        let topo = Topology::new().unwrap();
243
244        // Check host topology to determine if we need to enable SMT capabilities.
245        let smt_enabled = !opts.disable_smt && topo.smt_enabled;
246
247        // Determine the amount of non-empty NUMA nodes in the system.
248        let nr_nodes = topo
249            .nodes
250            .values()
251            .filter(|node| !node.all_cpus.is_empty())
252            .count();
253        info!("NUMA nodes: {}", nr_nodes);
254
255        // Automatically disable NUMA optimizations when running on non-NUMA systems.
256        let numa_enabled = !opts.disable_numa && nr_nodes > 1;
257        if !numa_enabled {
258            info!("Disabling NUMA optimizations");
259        }
260
261        // Determine the primary scheduling domain.
262        let power_profile = Self::power_profile();
263        let domain =
264            Self::resolve_energy_domain(&opts.primary_domain, power_profile).map_err(|err| {
265                anyhow!(
266                    "failed to resolve primary domain '{}': {}",
267                    opts.primary_domain,
268                    err
269                )
270            })?;
271
272        info!(
273            "{} {} {}",
274            SCHEDULER_NAME,
275            build_id::full_version(env!("CARGO_PKG_VERSION")),
276            if smt_enabled { "SMT on" } else { "SMT off" }
277        );
278
279        // Print command line.
280        info!(
281            "scheduler options: {}",
282            std::env::args().collect::<Vec<_>>().join(" ")
283        );
284
285        if opts.idle_resume_us >= 0 {
286            if !cpu_idle_resume_latency_supported() {
287                warn!("idle resume latency not supported");
288            } else {
289                info!("Setting idle QoS to {} us", opts.idle_resume_us);
290                for cpu in topo.all_cpus.values() {
291                    update_cpu_idle_resume_latency(
292                        cpu.id,
293                        opts.idle_resume_us.try_into().unwrap(),
294                    )?;
295                }
296            }
297        }
298
299        // Initialize BPF connector.
300        let mut skel_builder = BpfSkelBuilder::default();
301        skel_builder.obj_builder.debug(opts.verbose);
302        let open_opts = opts.libbpf.clone().into_bpf_open_opts();
303        let mut skel = scx_ops_open!(skel_builder, open_object, bpfland_ops, open_opts)?;
304
305        skel.struct_ops.bpfland_ops_mut().exit_dump_len = opts.exit_dump_len;
306
307        // Override default BPF scheduling parameters.
308        let rodata = skel.maps.rodata_data.as_mut().unwrap();
309        rodata.debug = opts.debug;
310        rodata.smt_enabled = smt_enabled;
311        rodata.numa_enabled = numa_enabled;
312        rodata.local_pcpu = opts.local_pcpu;
313        rodata.no_wake_sync = opts.no_wake_sync;
314        rodata.sticky_tasks = opts.sticky_tasks;
315        rodata.slice_max = opts.slice_us * 1000;
316        rodata.slice_min = opts.slice_min_us * 1000;
317        rodata.slice_lag = opts.slice_us_lag * 1000;
318        rodata.throttle_ns = opts.throttle_us * 1000;
319        rodata.primary_all = domain.weight() == *NR_CPU_IDS;
320
321        // Generate the list of available CPUs sorted by capacity in descending order.
322        let mut cpus: Vec<_> = topo.all_cpus.values().collect();
323        cpus.sort_by_key(|cpu| std::cmp::Reverse(cpu.cpu_capacity));
324        for (i, cpu) in cpus.iter().enumerate() {
325            rodata.cpu_capacity[cpu.id] = cpu.cpu_capacity as c_ulong;
326            rodata.preferred_cpus[i] = cpu.id as u64;
327        }
328        if opts.preferred_idle_scan {
329            info!(
330                "Preferred CPUs: {:?}",
331                &rodata.preferred_cpus[0..cpus.len()]
332            );
333        }
334        rodata.preferred_idle_scan = opts.preferred_idle_scan;
335
336        // Implicitly enable direct dispatch of per-CPU kthreads if CPU throttling is enabled
337        // (it's never a good idea to throttle per-CPU kthreads).
338        rodata.local_kthreads = opts.local_kthreads || opts.throttle_us > 0;
339
340        // Set scheduler flags.
341        skel.struct_ops.bpfland_ops_mut().flags = *compat::SCX_OPS_ENQ_EXITING
342            | *compat::SCX_OPS_ENQ_LAST
343            | *compat::SCX_OPS_ENQ_MIGRATION_DISABLED
344            | *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP
345            | if numa_enabled {
346                *compat::SCX_OPS_BUILTIN_IDLE_PER_NODE
347            } else {
348                0
349            };
350        info!(
351            "scheduler flags: {:#x}",
352            skel.struct_ops.bpfland_ops_mut().flags
353        );
354
355        // Load the BPF program for validation.
356        let mut skel = scx_ops_load!(skel, bpfland_ops, uei)?;
357
358        // Initialize the primary scheduling domain.
359        Self::init_energy_domain(&mut skel, &domain).map_err(|err| {
360            anyhow!(
361                "failed to initialize primary domain 0x{:x}: {}",
362                domain,
363                err
364            )
365        })?;
366
367        // Initialize CPU frequency scaling.
368        if let Err(err) = Self::init_cpufreq_perf(&mut skel, &opts.primary_domain, opts.cpufreq) {
369            bail!(
370                "failed to initialize cpufreq performance level: error {}",
371                err
372            );
373        }
374
375        // Initialize SMT domains.
376        if smt_enabled {
377            Self::init_smt_domains(&mut skel, &topo)?;
378        }
379
380        // Attach the scheduler.
381        let struct_ops = Some(scx_ops_attach!(skel, bpfland_ops)?);
382        let stats_server = StatsServer::new(stats::server_data()).launch()?;
383
384        Ok(Self {
385            skel,
386            struct_ops,
387            opts,
388            topo,
389            power_profile,
390            stats_server,
391            user_restart: false,
392        })
393    }
394
395    fn enable_primary_cpu(skel: &mut BpfSkel<'_>, cpu: i32) -> Result<(), u32> {
396        let prog = &mut skel.progs.enable_primary_cpu;
397        let mut args = cpu_arg {
398            cpu_id: cpu as c_int,
399        };
400        let input = ProgramInput {
401            context_in: Some(unsafe {
402                std::slice::from_raw_parts_mut(
403                    &mut args as *mut _ as *mut u8,
404                    std::mem::size_of_val(&args),
405                )
406            }),
407            ..Default::default()
408        };
409        let out = prog.test_run(input).unwrap();
410        if out.return_value != 0 {
411            return Err(out.return_value);
412        }
413
414        Ok(())
415    }
416
417    fn epp_to_cpumask(profile: Powermode) -> Result<Cpumask> {
418        let mut cpus = get_primary_cpus(profile).unwrap_or_default();
419        if cpus.is_empty() {
420            cpus = get_primary_cpus(Powermode::Any).unwrap_or_default();
421        }
422        Cpumask::from_str(&cpus_to_cpumask(&cpus))
423    }
424
425    fn resolve_energy_domain(primary_domain: &str, power_profile: PowerProfile) -> Result<Cpumask> {
426        let domain = match primary_domain {
427            "powersave" => Self::epp_to_cpumask(Powermode::Powersave)?,
428            "performance" => Self::epp_to_cpumask(Powermode::Performance)?,
429            "turbo" => Self::epp_to_cpumask(Powermode::Turbo)?,
430            "auto" => match power_profile {
431                PowerProfile::Powersave => Self::epp_to_cpumask(Powermode::Powersave)?,
432                PowerProfile::Balanced { .. }
433                | PowerProfile::Performance
434                | PowerProfile::Unknown => Self::epp_to_cpumask(Powermode::Any)?,
435            },
436            "all" => Self::epp_to_cpumask(Powermode::Any)?,
437            &_ => Cpumask::from_str(primary_domain)?,
438        };
439
440        Ok(domain)
441    }
442
443    fn init_energy_domain(skel: &mut BpfSkel<'_>, domain: &Cpumask) -> Result<()> {
444        info!("primary CPU domain = 0x{:x}", domain);
445
446        // Clear the primary domain by passing a negative CPU id.
447        if let Err(err) = Self::enable_primary_cpu(skel, -1) {
448            bail!("failed to reset primary domain: error {}", err);
449        }
450
451        // Update primary scheduling domain.
452        for cpu in 0..*NR_CPU_IDS {
453            if domain.test_cpu(cpu)
454                && let Err(err) = Self::enable_primary_cpu(skel, cpu as i32)
455            {
456                bail!("failed to add CPU {} to primary domain: error {}", cpu, err);
457            }
458        }
459
460        Ok(())
461    }
462
463    // Update hint for the cpufreq governor.
464    fn init_cpufreq_perf(
465        skel: &mut BpfSkel<'_>,
466        primary_domain: &String,
467        auto: bool,
468    ) -> Result<()> {
469        // If we are using the powersave profile always scale the CPU frequency to the minimum,
470        // otherwise use the maximum, unless automatic frequency scaling is enabled.
471        let perf_lvl: i64 = match primary_domain.as_str() {
472            "powersave" => 0,
473            _ if auto => -1,
474            _ => 1024,
475        };
476        info!(
477            "cpufreq performance level: {}",
478            match perf_lvl {
479                1024 => "max".into(),
480                0 => "min".into(),
481                n if n < 0 => "auto".into(),
482                _ => perf_lvl.to_string(),
483            }
484        );
485        skel.maps.bss_data.as_mut().unwrap().cpufreq_perf_lvl = perf_lvl;
486
487        Ok(())
488    }
489
490    fn power_profile() -> PowerProfile {
491        let profile = fetch_power_profile(true);
492        if profile == PowerProfile::Unknown {
493            fetch_power_profile(false)
494        } else {
495            profile
496        }
497    }
498
499    fn refresh_sched_domain(&mut self) -> bool {
500        if self.power_profile != PowerProfile::Unknown {
501            let power_profile = Self::power_profile();
502            if power_profile != self.power_profile {
503                self.power_profile = power_profile;
504
505                if self.opts.primary_domain == "auto" {
506                    return true;
507                }
508                if let Err(err) = Self::init_cpufreq_perf(
509                    &mut self.skel,
510                    &self.opts.primary_domain,
511                    self.opts.cpufreq,
512                ) {
513                    warn!("failed to refresh cpufreq performance level: error {}", err);
514                }
515            }
516        }
517
518        false
519    }
520
521    fn enable_sibling_cpu(
522        skel: &mut BpfSkel<'_>,
523        cpu: usize,
524        sibling_cpu: usize,
525    ) -> Result<(), u32> {
526        let prog = &mut skel.progs.enable_sibling_cpu;
527        let mut args = domain_arg {
528            cpu_id: cpu as c_int,
529            sibling_cpu_id: sibling_cpu as c_int,
530        };
531        let input = ProgramInput {
532            context_in: Some(unsafe {
533                std::slice::from_raw_parts_mut(
534                    &mut args as *mut _ as *mut u8,
535                    std::mem::size_of_val(&args),
536                )
537            }),
538            ..Default::default()
539        };
540        let out = prog.test_run(input).unwrap();
541        if out.return_value != 0 {
542            return Err(out.return_value);
543        }
544
545        Ok(())
546    }
547
548    fn init_smt_domains(skel: &mut BpfSkel<'_>, topo: &Topology) -> Result<(), std::io::Error> {
549        let smt_siblings = topo.sibling_cpus();
550
551        info!("SMT sibling CPUs: {:?}", smt_siblings);
552        for (cpu, sibling_cpu) in smt_siblings.iter().enumerate() {
553            Self::enable_sibling_cpu(skel, cpu, *sibling_cpu as usize).unwrap();
554        }
555
556        Ok(())
557    }
558
559    fn get_metrics(&self) -> Metrics {
560        let bss_data = self.skel.maps.bss_data.as_ref().unwrap();
561        Metrics {
562            nr_running: bss_data.nr_running,
563            nr_cpus: bss_data.nr_online_cpus,
564            nr_kthread_dispatches: bss_data.nr_kthread_dispatches,
565            nr_direct_dispatches: bss_data.nr_direct_dispatches,
566            nr_shared_dispatches: bss_data.nr_shared_dispatches,
567            nr_idle_select_path_picks: bss_data.nr_idle_select_path_picks,
568            nr_idle_enqueue_path_picks: bss_data.nr_idle_enqueue_path_picks,
569            nr_idle_prev_cpu_picks: bss_data.nr_idle_prev_cpu_picks,
570            nr_idle_primary_picks: bss_data.nr_idle_primary_picks,
571            nr_idle_spill_picks: bss_data.nr_idle_spill_picks,
572            nr_idle_pick_failures: bss_data.nr_idle_pick_failures,
573            nr_idle_primary_domain_misses: bss_data.nr_idle_primary_domain_misses,
574            nr_idle_global_misses: bss_data.nr_idle_global_misses,
575            nr_waker_cpu_biases: bss_data.nr_waker_cpu_biases,
576            nr_keep_running_reuses: bss_data.nr_keep_running_reuses,
577            nr_keep_running_queue_empty: bss_data.nr_keep_running_queue_empty,
578            nr_keep_running_smt_blocked: bss_data.nr_keep_running_smt_blocked,
579            nr_keep_running_queued_work: bss_data.nr_keep_running_queued_work,
580            nr_dispatch_cpu_dsq_consumes: bss_data.nr_dispatch_cpu_dsq_consumes,
581            nr_dispatch_node_dsq_consumes: bss_data.nr_dispatch_node_dsq_consumes,
582        }
583    }
584
585    pub fn exited(&mut self) -> bool {
586        uei_exited!(&self.skel, uei)
587    }
588
589    fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
590        let (res_ch, req_ch) = self.stats_server.channels();
591        while !shutdown.load(Ordering::Relaxed) && !self.exited() {
592            if self.refresh_sched_domain() {
593                self.user_restart = true;
594                break;
595            }
596            match req_ch.recv_timeout(Duration::from_secs(1)) {
597                Ok(()) => res_ch.send(self.get_metrics())?,
598                Err(RecvTimeoutError::Timeout) => {}
599                Err(e) => Err(e)?,
600            }
601        }
602
603        let _ = self.struct_ops.take();
604        uei_report!(&self.skel, uei)
605    }
606}
607
608impl Drop for Scheduler<'_> {
609    fn drop(&mut self) {
610        info!("Unregister {SCHEDULER_NAME} scheduler");
611
612        // Restore default CPU idle QoS resume latency.
613        if self.opts.idle_resume_us >= 0 && cpu_idle_resume_latency_supported() {
614            for cpu in self.topo.all_cpus.values() {
615                update_cpu_idle_resume_latency(cpu.id, cpu.pm_qos_resume_latency_us as i32)
616                    .unwrap();
617            }
618        }
619    }
620}
621
622fn main() -> Result<()> {
623    let opts = Opts::parse();
624
625    if opts.version {
626        println!(
627            "{} {}",
628            SCHEDULER_NAME,
629            build_id::full_version(env!("CARGO_PKG_VERSION"))
630        );
631        return Ok(());
632    }
633
634    if opts.help_stats {
635        stats::server_data().describe_meta(&mut std::io::stdout(), None)?;
636        return Ok(());
637    }
638
639    let loglevel = simplelog::LevelFilter::Info;
640
641    let mut lcfg = simplelog::ConfigBuilder::new();
642    lcfg.set_time_offset_to_local()
643        .expect("Failed to set local time offset")
644        .set_time_level(simplelog::LevelFilter::Error)
645        .set_location_level(simplelog::LevelFilter::Off)
646        .set_target_level(simplelog::LevelFilter::Off)
647        .set_thread_level(simplelog::LevelFilter::Off);
648    simplelog::TermLogger::init(
649        loglevel,
650        lcfg.build(),
651        simplelog::TerminalMode::Stderr,
652        simplelog::ColorChoice::Auto,
653    )?;
654
655    let shutdown = Arc::new(AtomicBool::new(false));
656    let shutdown_clone = shutdown.clone();
657    ctrlc::set_handler(move || {
658        shutdown_clone.store(true, Ordering::Relaxed);
659    })
660    .context("Error setting Ctrl-C handler")?;
661
662    if let Some(intv) = opts.monitor.or(opts.stats) {
663        let shutdown_copy = shutdown.clone();
664        let jh = std::thread::spawn(move || {
665            match stats::monitor(Duration::from_secs_f64(intv), shutdown_copy) {
666                Ok(_) => {
667                    debug!("stats monitor thread finished successfully")
668                }
669                Err(error_object) => {
670                    warn!(
671                        "stats monitor thread finished because of an error {}",
672                        error_object
673                    )
674                }
675            }
676        });
677        if opts.monitor.is_some() {
678            let _ = jh.join();
679            return Ok(());
680        }
681    }
682
683    let mut open_object = MaybeUninit::uninit();
684    loop {
685        let mut sched = Scheduler::init(&opts, &mut open_object)?;
686        if !sched.run(shutdown.clone())?.should_restart() {
687            if sched.user_restart {
688                continue;
689            }
690            break;
691        }
692    }
693
694    Ok(())
695}