Skip to main content

scx_lavd/
main.rs

1// SPDX-License-Identifier: GPL-2.0
2//
3// Copyright (c) 2024 Valve Corporation.
4// Author: Changwoo Min <changwoo@igalia.com>
5
6// This software may be used and distributed according to the terms of the
7// GNU General Public License version 2.
8
9mod bpf_skel;
10pub use bpf_skel::*;
11pub mod bpf_intf;
12mod bpf_streams;
13pub use bpf_intf::*;
14
15mod cpu_order;
16use scx_utils::init_libbpf_logging;
17mod stats;
18use std::ffi::c_int;
19use std::ffi::CStr;
20use std::mem;
21use std::mem::MaybeUninit;
22use std::str;
23use std::sync::atomic::AtomicBool;
24use std::sync::atomic::Ordering;
25use std::sync::Arc;
26use std::thread::ThreadId;
27use std::time::Duration;
28
29use anyhow::Context;
30use anyhow::Result;
31use clap::Parser;
32use clap_num::number_range;
33use cpu_order::CpuOrder;
34use cpu_order::PerfCpuOrder;
35use crossbeam::channel;
36use crossbeam::channel::RecvTimeoutError;
37use crossbeam::channel::Sender;
38use crossbeam::channel::TrySendError;
39use libbpf_rs::skel::OpenSkel;
40use libbpf_rs::skel::Skel;
41use libbpf_rs::AsRawLibbpf;
42use libbpf_rs::OpenObject;
43use libbpf_rs::PrintLevel;
44use libbpf_rs::ProgramInput;
45use libc::c_char;
46use plain::Plain;
47use scx_arena::ArenaLib;
48use scx_stats::prelude::*;
49use scx_utils::autopower::{fetch_power_profile, PowerProfile};
50use scx_utils::build_id;
51use scx_utils::compat;
52use scx_utils::ksym_exists;
53use scx_utils::libbpf_clap_opts::LibbpfOpts;
54use scx_utils::scx_ops_attach;
55use scx_utils::scx_ops_load;
56use scx_utils::scx_ops_open;
57use scx_utils::try_set_rlimit_infinity;
58use scx_utils::uei_exited;
59use scx_utils::uei_report;
60use scx_utils::EnergyModel;
61use scx_utils::TopologyArgs;
62use scx_utils::UserExitInfo;
63use scx_utils::NR_CPU_IDS;
64use stats::SchedSample;
65use stats::SchedSamples;
66use stats::StatsReq;
67use stats::StatsRes;
68use stats::SysStats;
69use tracing::{debug, info, warn};
70use tracing_subscriber::filter::EnvFilter;
71
72const SCHEDULER_NAME: &str = "scx_lavd";
73/// scx_lavd: Latency-criticality Aware Virtual Deadline (LAVD) scheduler
74///
75/// The rust part is minimal. It processes command line options and logs out
76/// scheduling statistics. The BPF part makes all the scheduling decisions.
77/// See the more detailed overview of the LAVD design at main.bpf.c.
78#[derive(Debug, Parser)]
79struct Opts {
80    /// Deprecated, noop, use RUST_LOG or --log-level instead.
81    #[clap(short = 'v', long, action = clap::ArgAction::Count)]
82    verbose: u8,
83
84    /// Automatically decide the scheduler's power mode (performance vs.
85    /// powersave vs. balanced), CPU preference order, etc, based on system
86    /// load. The options affecting the power mode and the use of core compaction
87    /// (--autopower, --performance, --powersave, --balanced,
88    /// --no-core-compaction) cannot be used with this option. When no option
89    /// is specified, this is a default mode.
90    #[clap(long = "autopilot", action = clap::ArgAction::SetTrue)]
91    autopilot: bool,
92
93    /// Automatically decide the scheduler's power mode (performance vs.
94    /// powersave vs. balanced) based on the system's active power profile.
95    /// The scheduler's power mode decides the CPU preference order and the use
96    /// of core compaction, so the options affecting these (--autopilot,
97    /// --performance, --powersave, --balanced, --no-core-compaction) cannot
98    /// be used with this option.
99    #[clap(long = "autopower", action = clap::ArgAction::SetTrue)]
100    autopower: bool,
101
102    /// Run the scheduler in performance mode to get maximum performance.
103    /// This option cannot be used with other conflicting options (--autopilot,
104    /// --autopower, --balanced, --powersave, --no-core-compaction)
105    /// affecting the use of core compaction.
106    #[clap(long = "performance", action = clap::ArgAction::SetTrue)]
107    performance: bool,
108
109    /// Run the scheduler in powersave mode to minimize power consumption.
110    /// This option cannot be used with other conflicting options (--autopilot,
111    /// --autopower, --performance, --balanced, --no-core-compaction)
112    /// affecting the use of core compaction.
113    #[clap(long = "powersave", action = clap::ArgAction::SetTrue)]
114    powersave: bool,
115
116    /// Run the scheduler in balanced mode aiming for sweetspot between power
117    /// and performance. This option cannot be used with other conflicting
118    /// options (--autopilot, --autopower, --performance, --powersave,
119    /// --no-core-compaction) affecting the use of core compaction.
120    #[clap(long = "balanced", action = clap::ArgAction::SetTrue)]
121    balanced: bool,
122
123    /// Maximum scheduling slice duration in microseconds.
124    #[clap(long = "slice-max-us", default_value = "5000")]
125    slice_max_us: u64,
126
127    /// Minimum scheduling slice duration in microseconds.
128    #[clap(long = "slice-min-us", default_value = "500")]
129    slice_min_us: u64,
130
131    /// Target load percentage for turbulent CPUs relative to non-turbulent
132    /// CPUs' per-capacity utilization. 100 means turbulent CPUs should carry
133    /// the same per-capacity load as non-turbulent CPUs. Values below 100
134    /// route fewer tasks to turbulent CPUs; values above 100 route more.
135    /// Range: 0-200. Default: 100.
136    #[clap(long = "lat-load-target-pct", default_value = "100", value_parser=Opts::lat_load_target_pct_range)]
137    lat_load_target_pct: u16,
138
139    /// Migration delta threshold percentage (0-100). When set to a non-zero value,
140    /// the migration threshold is mig-delta-pct percent of the average load.
141    /// Additionally, disables force task stealing in the consume path, relying only
142    /// on the is_stealer/is_stealee thresholds for more predictable load balancing.
143    /// Default is 0 (disabled, uses dynamic threshold based on load with both
144    /// probabilistic and force task stealing enabled). This is an experimental feature.
145    #[clap(long = "mig-delta-pct", default_value = "0", value_parser=Opts::mig_delta_pct_range)]
146    mig_delta_pct: u8,
147
148    /// Warm-CPU wait. Maximum time, in microseconds, a waking latency-tolerant
149    /// task waits for its previous CPU to free up before migrating to an idle
150    /// one, queueing on that CPU's per-CPU DSQ meanwhile. The wait is predicted
151    /// from the previous CPU's estimated free time, and warm cache/TLB state on
152    /// that CPU extends the budget up to 2x. 0 disables (default).
153    #[clap(long = "warm-cpu-us", default_value = "0")]
154    warm_cpu_us: u64,
155
156    /// Low utilization threshold percentage (0-100) for periodic load balancing.
157    /// When set to a non-zero value, periodic load balancing is skipped when
158    /// the maximum per-domain utilization is below this percentage.
159    /// Default is 25 (skip periodic LB below 25% utilization).
160    /// Set to 0 to disable. Set to 100 to always skip periodic LB.
161    #[clap(long = "lb-low-util-pct", default_value = "25", value_parser=Opts::lb_low_util_pct_range)]
162    lb_low_util_pct: u8,
163
164    /// Low utilization threshold percentage (0-100) for bypassing deadline
165    /// scheduling. When set to a non-zero value, tasks are dispatched directly
166    /// to the local DSQ (FIFO) instead of using deadline-based ordering when
167    /// the per-CPU utilization is below this percentage.
168    /// Default is 10 (bypass deadline scheduling below 10% utilization).
169    /// Set to 0 to disable. Set to 100 to always bypass deadline scheduling.
170    #[clap(long = "lb-local-dsq-util-pct", default_value = "10", value_parser=Opts::lb_local_dsq_util_pct_range)]
171    lb_local_dsq_util_pct: u8,
172
173    /// Slice duration in microseconds to use for all tasks when pinned tasks
174    /// are running on a CPU. Must be between slice-min-us and slice-max-us.
175    /// When this option is enabled, pinned tasks are always enqueued to per-CPU DSQs
176    /// and the dispatch logic compares vtimes across all DSQs to select the lowest
177    /// vtime task. This helps improve responsiveness for pinned tasks. By default,
178    /// this option is on with a default value of 5000 (5 msec). To turn off the option,
179    /// explicitly set the value to 0.
180    #[clap(long = "pinned-slice-us", default_value = "5000")]
181    pinned_slice_us: Option<u64>,
182
183    /// Limit the ratio of preemption to the roughly top P% of latency-critical
184    /// tasks. When N is given as an argument, P is 0.5^N * 100. The default
185    /// value is 6, which limits the preemption for the top 1.56% of
186    /// latency-critical tasks.
187    #[clap(long = "preempt-shift", default_value = "6", value_parser=Opts::preempt_shift_range)]
188    preempt_shift: u8,
189
190    /// List of CPUs in preferred order (e.g., "0-3,7,6,5,4"). The scheduler
191    /// uses the CPU preference mode only when the core compaction is enabled
192    /// (i.e., balanced or powersave mode is specified as an option or chosen
193    /// in the autopilot or autopower mode). When "--cpu-pref-order" is given,
194    /// it implies "--no-use-em".
195    #[clap(long = "cpu-pref-order", default_value = "")]
196    cpu_pref_order: String,
197
198    /// Do not use the energy model in making CPU preference order decisions.
199    #[clap(long = "no-use-em", action = clap::ArgAction::SetTrue)]
200    no_use_em: bool,
201
202    /// Do not boost futex holders.
203    #[clap(long = "no-futex-boost", action = clap::ArgAction::SetTrue)]
204    no_futex_boost: bool,
205
206    /// Default: --no-fast-lb is deactivated (fast load balancer is on).
207    /// Disable the fast (batch-migration) load balancer and fall back to
208    /// the pre-fast-lb load-balancer behavior.
209    #[clap(long = "no-fast-lb", action = clap::ArgAction::SetTrue)]
210    no_fast_lb: bool,
211
212    /// Disable preemption.
213    #[clap(long = "no-preemption", action = clap::ArgAction::SetTrue)]
214    no_preemption: bool,
215
216    /// Disable an optimization for synchronous wake-up.
217    #[clap(long = "no-wake-sync", action = clap::ArgAction::SetTrue)]
218    no_wake_sync: bool,
219
220    /// Disable dynamic slice boost for long-running tasks.
221    #[clap(long = "no-slice-boost", action = clap::ArgAction::SetTrue)]
222    no_slice_boost: bool,
223
224    /// Enables DSQs per CPU, this enables task queuing and dispatching
225    /// from CPU specific DSQs. This generally increases L1/L2 cache
226    /// locality for tasks and lowers lock contention compared to shared DSQs,
227    /// but at the cost of higher load balancing complexity. This is a
228    /// highly experimental feature.
229    #[clap(long = "per-cpu-dsq", action = clap::ArgAction::SetTrue)]
230    per_cpu_dsq: bool,
231
232    /// Enable CPU bandwidth control using cpu.max in cgroup v2.
233    /// This is a highly experimental feature.
234    #[clap(long = "enable-cpu-bw", action = clap::ArgAction::SetTrue)]
235    enable_cpu_bw: bool,
236
237    /// If specified, only tasks which have their scheduling policy set to
238    /// SCHED_EXT using sched_setscheduler(2) are switched. Otherwise, all
239    /// tasks are switched.
240    #[clap(long = "partial", action = clap::ArgAction::SetTrue)]
241    partial: bool,
242
243    ///
244    /// Disable core compaction so the scheduler uses all the online CPUs.
245    /// The core compaction attempts to minimize the number of actively used
246    /// CPUs for unaffinitized tasks, respecting the CPU preference order.
247    /// Normally, the core compaction is enabled by the power mode (i.e.,
248    /// balanced or powersave mode is specified as an option or chosen in
249    /// the autopilot or autopower mode). This option cannot be used with the
250    /// other options that control the core compaction (--autopilot,
251    /// --autopower, --performance, --balanced, --powersave).
252    #[clap(long = "no-core-compaction", action = clap::ArgAction::SetTrue)]
253    no_core_compaction: bool,
254
255    /// Disable controlling the CPU frequency.
256    #[clap(long = "no-freq-scaling", action = clap::ArgAction::SetTrue)]
257    no_freq_scaling: bool,
258
259    /// Enable stats monitoring with the specified interval.
260    #[clap(long)]
261    stats: Option<f64>,
262
263    /// Run in stats monitoring mode with the specified interval. Scheduler is not launched.
264    #[clap(long)]
265    monitor: Option<f64>,
266
267    /// Run in monitoring mode. Show the specified number of scheduling
268    /// samples every second.
269    #[clap(long)]
270    monitor_sched_samples: Option<u64>,
271
272    /// Specify the logging level. Accepts rust's envfilter syntax for modular
273    /// logging: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax. Examples: ["info", "warn,tokio=info"]
274    #[clap(long, default_value = "info")]
275    log_level: String,
276
277    /// Exit debug dump buffer length in bytes. 0 selects the kernel default
278    /// of 32 KiB, which lavd overruns: ops.dump_task() adds three lines per
279    /// runnable task on top of the five lines and stack trace the kernel
280    /// already emits, so the dump grows with runqueue depth, not CPU count.
281    #[clap(long, default_value = "262144")]
282    exit_dump_len: u32,
283
284    /// Print scheduler version and exit.
285    #[clap(short = 'V', long, action = clap::ArgAction::SetTrue)]
286    version: bool,
287
288    /// Optional run ID for tracking scheduler instances.
289    #[clap(long)]
290    run_id: Option<u64>,
291
292    /// Show descriptions for statistics.
293    #[clap(long)]
294    help_stats: bool,
295
296    #[clap(flatten, next_help_heading = "Libbpf Options")]
297    pub libbpf: LibbpfOpts,
298
299    /// Topology configuration options
300    #[clap(flatten)]
301    topology: Option<TopologyArgs>,
302}
303
304impl Opts {
305    fn can_autopilot(&self) -> bool {
306        self.autopower == false
307            && self.performance == false
308            && self.powersave == false
309            && self.balanced == false
310            && self.no_core_compaction == false
311    }
312
313    fn can_autopower(&self) -> bool {
314        self.autopilot == false
315            && self.performance == false
316            && self.powersave == false
317            && self.balanced == false
318            && self.no_core_compaction == false
319    }
320
321    fn can_performance(&self) -> bool {
322        self.autopilot == false
323            && self.autopower == false
324            && self.powersave == false
325            && self.balanced == false
326    }
327
328    fn can_balanced(&self) -> bool {
329        self.autopilot == false
330            && self.autopower == false
331            && self.performance == false
332            && self.powersave == false
333            && self.no_core_compaction == false
334    }
335
336    fn can_powersave(&self) -> bool {
337        self.autopilot == false
338            && self.autopower == false
339            && self.performance == false
340            && self.balanced == false
341            && self.no_core_compaction == false
342    }
343
344    fn proc(&mut self) -> Option<&mut Self> {
345        if !self.autopilot {
346            self.autopilot = self.can_autopilot();
347        }
348
349        if self.autopilot {
350            if !self.can_autopilot() {
351                info!("Autopilot mode cannot be used with conflicting options.");
352                return None;
353            }
354            info!("Autopilot mode is enabled.");
355        }
356
357        if self.autopower {
358            if !self.can_autopower() {
359                info!("Autopower mode cannot be used with conflicting options.");
360                return None;
361            }
362            info!("Autopower mode is enabled.");
363        }
364
365        if self.performance {
366            if !self.can_performance() {
367                info!("Performance mode cannot be used with conflicting options.");
368                return None;
369            }
370            info!("Performance mode is enabled.");
371            self.no_core_compaction = true;
372        }
373
374        if self.powersave {
375            if !self.can_powersave() {
376                info!("Powersave mode cannot be used with conflicting options.");
377                return None;
378            }
379            info!("Powersave mode is enabled.");
380            self.no_core_compaction = false;
381        }
382
383        if self.balanced {
384            if !self.can_balanced() {
385                info!("Balanced mode cannot be used with conflicting options.");
386                return None;
387            }
388            info!("Balanced mode is enabled.");
389            self.no_core_compaction = false;
390        }
391
392        if !EnergyModel::has_energy_model() || !self.cpu_pref_order.is_empty() {
393            self.no_use_em = true;
394        }
395        if self.no_use_em {
396            info!("Energy model won't be used for CPU preference order.");
397        }
398
399        if let Some(pinned_slice) = self.pinned_slice_us {
400            if pinned_slice == 0 {
401                info!("Pinned task slice mode is disabled. Pinned tasks will use per-domain DSQs.");
402            } else if pinned_slice < self.slice_min_us || pinned_slice > self.slice_max_us {
403                info!(
404                    "pinned-slice-us ({}) must be between slice-min-us ({}) and slice-max-us ({})",
405                    pinned_slice, self.slice_min_us, self.slice_max_us
406                );
407                return None;
408            } else {
409                info!(
410                "Pinned task slice mode is enabled ({} us). Pinned tasks will use per-CPU DSQs.",
411                pinned_slice
412            );
413            }
414        }
415
416        Some(self)
417    }
418
419    fn preempt_shift_range(s: &str) -> Result<u8, String> {
420        number_range(s, 0, 10)
421    }
422
423    fn lat_load_target_pct_range(s: &str) -> Result<u16, String> {
424        number_range(s, 0, 200)
425    }
426
427    fn mig_delta_pct_range(s: &str) -> Result<u8, String> {
428        number_range(s, 0, 100)
429    }
430
431    fn lb_low_util_pct_range(s: &str) -> Result<u8, String> {
432        number_range(s, 0, 100)
433    }
434
435    fn lb_local_dsq_util_pct_range(s: &str) -> Result<u8, String> {
436        number_range(s, 0, 100)
437    }
438}
439
440unsafe impl Plain for msg_task_ctx {}
441
442impl msg_task_ctx {
443    fn from_bytes(buf: &[u8]) -> &msg_task_ctx {
444        plain::from_bytes(buf).expect("The buffer is either too short or not aligned!")
445    }
446}
447
448impl introspec {
449    fn new() -> Self {
450        let intrspc = unsafe { mem::MaybeUninit::<introspec>::zeroed().assume_init() };
451        intrspc
452    }
453}
454
455struct Scheduler<'a> {
456    skel: BpfSkel<'a>,
457    struct_ops: Option<libbpf_rs::Link>,
458    intrspc: introspec,
459    monitor_tid: Option<ThreadId>,
460    stats_server: StatsServer<StatsReq, StatsRes>,
461    mseq_id: u64,
462}
463
464impl<'a> Scheduler<'a> {
465    fn init(opts: &'a Opts, open_object: &'a mut MaybeUninit<OpenObject>) -> Result<Self> {
466        if *NR_CPU_IDS > LAVD_CPU_ID_MAX as usize {
467            panic!(
468                "Num possible CPU IDs ({}) exceeds maximum of ({})",
469                *NR_CPU_IDS, LAVD_CPU_ID_MAX
470            );
471        }
472
473        try_set_rlimit_infinity();
474
475        // Open the BPF prog first for verification.
476        let debug_level = if opts.log_level.contains("trace") {
477            2
478        } else if opts.log_level.contains("debug") {
479            1
480        } else {
481            0
482        };
483        let mut skel_builder = BpfSkelBuilder::default();
484        skel_builder.obj_builder.debug(debug_level > 1);
485        init_libbpf_logging(Some(PrintLevel::Debug));
486
487        let open_opts = opts.libbpf.clone().into_bpf_open_opts();
488        let mut skel = scx_ops_open!(skel_builder, open_object, lavd_ops, open_opts)?;
489
490        // Enable futex tracing using ftrace if available. If the ftrace is not
491        // available, use tracepoint, which is known to be slower than ftrace.
492        if !opts.no_futex_boost {
493            if Self::attach_futex_ftraces(&mut skel)? == false {
494                info!("Fail to attach futex ftraces. Try with tracepoints.");
495                if Self::attach_futex_tracepoints(&mut skel)? == false {
496                    info!("Fail to attach futex tracepoints.");
497                }
498            }
499        }
500
501        // Initialize CPU topology with CLI arguments
502        let order = CpuOrder::new(opts.topology.as_ref(), opts.no_use_em).unwrap();
503        Self::init_cpus(&mut skel, &order);
504        Self::init_cpdoms(&mut skel, &order);
505
506        // When there are multiple domains, hook the execve() syscall family
507        // to enable aggressive cross-domain migration when execve() is called.
508        if order.cpdom_map.len() > 1 {
509            Self::attach_execve_tracepoints(&mut skel)?;
510        }
511
512        // Initialize skel according to @opts.
513        Self::init_globals(&mut skel, &opts, &order, debug_level);
514
515        // Size the cpu.max per-(cgroup, LLC) map to this system's LLC count
516        // before loading (a map's max_entries is fixed at load time).
517        scx_utils::resize_cgroup_bw_llc_map(skel.open_object_mut(), order.nr_llcs)?;
518
519        // Initialize arena
520        let mut skel = scx_ops_load!(skel, lavd_ops, uei)?;
521        let task_size = std::mem::size_of::<types::task_ctx>();
522        let arenalib = ArenaLib::init(skel.object_mut(), task_size, *NR_CPU_IDS)?;
523        arenalib.setup()?;
524
525        // Attach.
526        let struct_ops = Some(scx_ops_attach!(skel, lavd_ops)?);
527        let stats_server = StatsServer::new(stats::server_data(*NR_CPU_IDS as u64)).launch()?;
528
529        Ok(Self {
530            skel,
531            struct_ops,
532            intrspc: introspec::new(),
533            monitor_tid: None,
534            stats_server,
535            mseq_id: 0,
536        })
537    }
538
539    fn attach_futex_ftraces(skel: &mut OpenBpfSkel) -> Result<bool> {
540        let ftraces = vec![
541            ("__futex_wait", &skel.progs.fexit___futex_wait),
542            ("futex_wait_multiple", &skel.progs.fexit_futex_wait_multiple),
543            (
544                "futex_wait_requeue_pi",
545                &skel.progs.fexit_futex_wait_requeue_pi,
546            ),
547            ("futex_wake", &skel.progs.fexit_futex_wake),
548            ("futex_wake_op", &skel.progs.fexit_futex_wake_op),
549            ("futex_lock_pi", &skel.progs.fexit_futex_lock_pi),
550            ("futex_unlock_pi", &skel.progs.fexit_futex_unlock_pi),
551        ];
552
553        if compat::tracer_available("function")? == false {
554            info!("Ftrace is not enabled in the kernel.");
555            return Ok(false);
556        }
557
558        compat::cond_kprobes_enable(ftraces)
559    }
560
561    fn attach_futex_tracepoints(skel: &mut OpenBpfSkel) -> Result<bool> {
562        let tracepoints = vec![
563            ("syscalls:sys_enter_futex", &skel.progs.rtp_sys_enter_futex),
564            ("syscalls:sys_exit_futex", &skel.progs.rtp_sys_exit_futex),
565            (
566                "syscalls:sys_exit_futex_wait",
567                &skel.progs.rtp_sys_exit_futex_wait,
568            ),
569            (
570                "syscalls:sys_exit_futex_waitv",
571                &skel.progs.rtp_sys_exit_futex_waitv,
572            ),
573            (
574                "syscalls:sys_exit_futex_wake",
575                &skel.progs.rtp_sys_exit_futex_wake,
576            ),
577        ];
578
579        compat::cond_tracepoints_enable(tracepoints)
580    }
581
582    fn attach_execve_tracepoints(skel: &mut OpenBpfSkel) -> Result<bool> {
583        let tracepoints = vec![
584            (
585                "syscalls:sys_enter_execve",
586                &skel.progs.cond_hook_sys_enter_execve,
587            ),
588            (
589                "syscalls:sys_enter_execveat",
590                &skel.progs.cond_hook_sys_enter_execveat,
591            ),
592        ];
593
594        compat::cond_tracepoints_enable(tracepoints)
595    }
596
597    fn init_cpus(skel: &mut OpenBpfSkel, order: &CpuOrder) {
598        debug!("{:#?}", order);
599
600        // Initialize CPU capacity and sibling
601        for cpu in order.cpuids.iter() {
602            skel.maps.rodata_data.as_mut().unwrap().cpu_capacity[cpu.cpu_adx] = cpu.cpu_cap as u16;
603            skel.maps.rodata_data.as_mut().unwrap().cpu_big[cpu.cpu_adx] = cpu.big_core as u8;
604            skel.maps.rodata_data.as_mut().unwrap().cpu_turbo[cpu.cpu_adx] = cpu.turbo_core as u8;
605            skel.maps.rodata_data.as_mut().unwrap().cpu_sibling[cpu.cpu_adx] =
606                cpu.cpu_sibling as u32;
607        }
608
609        // Initialize performance vs. CPU order table.
610        let nr_pco_states: u8 = order.perf_cpu_order.len() as u8;
611        if nr_pco_states > LAVD_PCO_STATE_MAX as u8 {
612            panic!("Generated performance vs. CPU order stats are too complex ({nr_pco_states}) to handle");
613        }
614
615        skel.maps.rodata_data.as_mut().unwrap().nr_pco_states = nr_pco_states;
616        for (i, (_, pco)) in order.perf_cpu_order.iter().enumerate() {
617            Self::init_pco_tuple(skel, i, &pco);
618            info!("{:#}", pco);
619        }
620
621        let (_, last_pco) = order.perf_cpu_order.last_key_value().unwrap();
622        for i in nr_pco_states..LAVD_PCO_STATE_MAX as u8 {
623            Self::init_pco_tuple(skel, i as usize, &last_pco);
624        }
625    }
626
627    fn init_pco_tuple(skel: &mut OpenBpfSkel, i: usize, pco: &PerfCpuOrder) {
628        let cpus_perf = pco.cpus_perf.borrow();
629        let cpus_ovflw = pco.cpus_ovflw.borrow();
630        let pco_nr_primary = cpus_perf.len();
631
632        skel.maps.rodata_data.as_mut().unwrap().pco_bounds[i] = pco.perf_cap as u32;
633        skel.maps.rodata_data.as_mut().unwrap().pco_nr_primary[i] = pco_nr_primary as u16;
634
635        for (j, &cpu_adx) in cpus_perf.iter().enumerate() {
636            skel.maps.rodata_data.as_mut().unwrap().pco_table[i][j] = cpu_adx as u16;
637        }
638
639        for (j, &cpu_adx) in cpus_ovflw.iter().enumerate() {
640            let k = j + pco_nr_primary;
641            skel.maps.rodata_data.as_mut().unwrap().pco_table[i][k] = cpu_adx as u16;
642        }
643    }
644
645    fn init_cpdoms(skel: &mut OpenBpfSkel, order: &CpuOrder) {
646        // Initialize compute domain contexts
647        for (k, v) in order.cpdom_map.iter() {
648            skel.maps.bss_data.as_mut().unwrap().cpdom_ctxs[v.cpdom_id].id = v.cpdom_id as u64;
649            skel.maps.bss_data.as_mut().unwrap().cpdom_ctxs[v.cpdom_id].alt_id =
650                v.cpdom_alt_id.get() as u64;
651            skel.maps.bss_data.as_mut().unwrap().cpdom_ctxs[v.cpdom_id].numa_id = k.numa_adx as u8;
652            skel.maps.bss_data.as_mut().unwrap().cpdom_ctxs[v.cpdom_id].llc_id = k.llc_adx as u8;
653            skel.maps.bss_data.as_mut().unwrap().cpdom_ctxs[v.cpdom_id].is_big = k.is_big as u8;
654            skel.maps.bss_data.as_mut().unwrap().cpdom_ctxs[v.cpdom_id].is_valid = 1;
655            for cpu_id in v.cpu_ids.iter() {
656                let i = cpu_id / 64;
657                let j = cpu_id % 64;
658                skel.maps.bss_data.as_mut().unwrap().cpdom_ctxs[v.cpdom_id].__cpumask[i] |=
659                    0x01 << j;
660            }
661
662            if v.neighbor_map.borrow().iter().len() > LAVD_CPDOM_MAX_DIST as usize {
663                panic!("The processor topology is too complex to handle in BPF.");
664            }
665
666            for (k, (_d, neighbors)) in v.neighbor_map.borrow().iter().enumerate() {
667                let nr_neighbors = neighbors.borrow().len() as u8;
668                if nr_neighbors > LAVD_CPDOM_MAX_NR as u8 {
669                    panic!("The processor topology is too complex to handle in BPF.");
670                }
671                skel.maps.bss_data.as_mut().unwrap().cpdom_ctxs[v.cpdom_id].nr_neighbors[k] =
672                    nr_neighbors;
673                for (i, &id) in neighbors.borrow().iter().enumerate() {
674                    let idx = (k * LAVD_CPDOM_MAX_NR as usize) + i;
675                    skel.maps.bss_data.as_mut().unwrap().cpdom_ctxs[v.cpdom_id].neighbor_ids[idx] =
676                        id as u8;
677                }
678            }
679        }
680    }
681
682    fn init_globals(skel: &mut OpenBpfSkel, opts: &Opts, order: &CpuOrder, debug_level: u8) {
683        let bss_data = skel.maps.bss_data.as_mut().unwrap();
684        bss_data.no_preemption = opts.no_preemption;
685        bss_data.no_core_compaction = opts.no_core_compaction;
686        bss_data.no_freq_scaling = opts.no_freq_scaling;
687        bss_data.is_powersave_mode = opts.powersave;
688        let rodata = skel.maps.rodata_data.as_mut().unwrap();
689        rodata.nr_llcs = order.nr_llcs as u64;
690        rodata.nr_cpu_ids = *NR_CPU_IDS as u32;
691        rodata.is_smt_active = order.smt_enabled;
692        rodata.is_autopilot_on = opts.autopilot;
693        rodata.verbose = debug_level;
694        rodata.slice_max_ns = opts.slice_max_us * 1000;
695        rodata.slice_min_ns = opts.slice_min_us * 1000;
696        rodata.pinned_slice_ns = opts.pinned_slice_us.map(|v| v * 1000).unwrap_or(0);
697        rodata.preempt_shift = opts.preempt_shift;
698        rodata.lat_load_target_pct = opts.lat_load_target_pct;
699        rodata.mig_delta_pct = opts.mig_delta_pct;
700        rodata.warm_cpu_ns = opts.warm_cpu_us * 1000;
701        rodata.lb_low_util_wall = ((opts.lb_low_util_pct as u64) << 10) / 100;
702        rodata.lb_local_dsq_util_wall = ((opts.lb_local_dsq_util_pct as u64) << 10) / 100;
703        rodata.no_use_em = opts.no_use_em as u8;
704        rodata.no_fast_lb = opts.no_fast_lb as u8;
705        rodata.no_wake_sync = opts.no_wake_sync;
706        rodata.no_slice_boost = opts.no_slice_boost;
707        rodata.per_cpu_dsq = opts.per_cpu_dsq;
708        rodata.enable_cpu_bw = opts.enable_cpu_bw;
709
710        if !ksym_exists("scx_group_set_bandwidth").unwrap() {
711            skel.struct_ops.lavd_ops_mut().cgroup_set_bandwidth = std::ptr::null_mut();
712            warn!("Kernel does not support ops.cgroup_set_bandwidth(), so disable it.");
713        }
714
715        /*
716         * Two-way selection for "drain the local DSQ when a
717         * higher-priority class takes the CPU":
718         *
719         *   kernel >= 6.19 (call-from-anywhere reenqueue):
720         *     -> drop ops.cpu_release; enable sched_switch hook.
721         *
722         *   kernel < 6.19 (cpu_release-restricted reenqueue only):
723         *     -> keep ops.cpu_release; sched_switch stays disabled.
724         */
725        if ksym_exists("scx_bpf_reenqueue_local___v2").unwrap() {
726            skel.struct_ops.lavd_ops_mut().cpu_release = std::ptr::null_mut();
727            unsafe {
728                libbpf_rs::libbpf_sys::bpf_program__set_autoload(
729                    skel.progs.lavd_sched_switch.as_libbpf_object().as_ptr(),
730                    true,
731                );
732            }
733        }
734
735        skel.struct_ops.lavd_ops_mut().flags = *compat::SCX_OPS_ENQ_EXITING
736            | *compat::SCX_OPS_ENQ_LAST
737            | *compat::SCX_OPS_ENQ_MIGRATION_DISABLED
738            | *compat::SCX_OPS_KEEP_BUILTIN_IDLE;
739
740        if opts.partial {
741            skel.struct_ops.lavd_ops_mut().flags |= *compat::SCX_OPS_SWITCH_PARTIAL;
742        }
743
744        skel.struct_ops.lavd_ops_mut().exit_dump_len = opts.exit_dump_len;
745    }
746
747    fn get_msg_seq_id() -> u64 {
748        static mut MSEQ: u64 = 0;
749        unsafe {
750            MSEQ += 1;
751            MSEQ
752        }
753    }
754
755    fn relay_introspec(data: &[u8], intrspc_tx: &Sender<SchedSample>) -> i32 {
756        let mt = msg_task_ctx::from_bytes(data);
757        let tx = mt.taskc_x;
758
759        // No idea how to print other types than LAVD_MSG_TASKC
760        if mt.hdr.kind != LAVD_MSG_TASKC {
761            return 0;
762        }
763
764        let mseq = Scheduler::get_msg_seq_id();
765
766        let c_tx_cm: *const c_char = (&tx.comm as *const [c_char; 17]) as *const c_char;
767        let c_tx_cm_str: &CStr = unsafe { CStr::from_ptr(c_tx_cm) };
768        let tx_comm: &str = c_tx_cm_str.to_str().unwrap();
769
770        let c_waker_cm: *const c_char = (&tx.waker_comm as *const [c_char; 17]) as *const c_char;
771        let c_waker_cm_str: &CStr = unsafe { CStr::from_ptr(c_waker_cm) };
772        let waker_comm: &str = c_waker_cm_str.to_str().unwrap();
773
774        let c_tx_st: *const c_char = (&tx.stat as *const [c_char; 5]) as *const c_char;
775        let c_tx_st_str: &CStr = unsafe { CStr::from_ptr(c_tx_st) };
776        let tx_stat: &str = c_tx_st_str.to_str().unwrap();
777
778        match intrspc_tx.try_send(SchedSample {
779            mseq,
780            pid: tx.pid,
781            comm: tx_comm.into(),
782            stat: tx_stat.into(),
783            cpu_id: tx.cpu_id,
784            prev_cpu_id: tx.prev_cpu_id,
785            suggested_cpu_id: tx.suggested_cpu_id,
786            waker_pid: tx.waker_pid,
787            waker_comm: waker_comm.into(),
788            slice_wall: tx.slice_wall,
789            lat_cri: tx.lat_cri,
790            avg_lat_cri: tx.avg_lat_cri,
791            static_prio: tx.static_prio,
792            rerunnable_interval_wall: tx.rerunnable_interval_wall,
793            resched_interval_wall: tx.resched_interval_wall,
794            run_freq: tx.run_freq,
795            avg_runtime_wall: tx.avg_runtime_wall,
796            wait_freq: tx.wait_freq,
797            wake_freq: tx.wake_freq,
798            perf_cri: tx.perf_cri,
799            thr_perf_cri: tx.thr_perf_cri,
800            cpuperf_cur: tx.cpuperf_cur,
801            cpu_util_wall: tx.cpu_util_wall,
802            cpu_util_invr: tx.cpu_util_invr,
803            steal_util_wall: tx.steal_util_wall,
804            steal_util_invr: tx.steal_util_invr,
805            dom_pinned_util_wall: tx.dom_pinned_util_wall,
806            dom_pinned_util_invr: tx.dom_pinned_util_invr,
807            nr_active: tx.nr_active,
808            dsq_id: tx.dsq_id,
809            dsq_consume_lat: tx.dsq_consume_lat,
810            lat_headroom: tx.lat_headroom,
811            vuln_thresh: tx.vuln_thresh,
812            task_util_est: tx.task_util_est,
813            norm_lat_cri: tx.norm_lat_cri,
814            cpu_heat: tx.cpu_heat,
815            warm_cpu_id: tx.warm_cpu_id,
816            slice_used_wall: tx.last_slice_used_wall,
817        }) {
818            Ok(()) | Err(TrySendError::Full(_)) => 0,
819            Err(e) => panic!("failed to send on intrspc_tx ({})", e),
820        }
821    }
822
823    fn prep_introspec(&mut self) {
824        if !self.skel.maps.bss_data.as_ref().unwrap().is_monitored {
825            self.skel.maps.bss_data.as_mut().unwrap().is_monitored = true;
826        }
827        self.skel.maps.bss_data.as_mut().unwrap().intrspc.cmd = self.intrspc.cmd;
828        self.skel.maps.bss_data.as_mut().unwrap().intrspc.arg = self.intrspc.arg;
829    }
830
831    fn cleanup_introspec(&mut self) {
832        self.skel.maps.bss_data.as_mut().unwrap().intrspc.cmd = LAVD_CMD_NOP;
833    }
834
835    fn get_pc(x: u64, y: u64) -> f64 {
836        return 100. * x as f64 / y as f64;
837    }
838
839    fn get_power_mode(power_mode: i32) -> &'static str {
840        match power_mode as u32 {
841            LAVD_PM_PERFORMANCE => "performance",
842            LAVD_PM_BALANCED => "balanced",
843            LAVD_PM_POWERSAVE => "powersave",
844            _ => "unknown",
845        }
846    }
847
848    /// Collect the scheduling samples the BPF side has queued, waiting up to
849    /// @timeout for them, or draining without waiting when it is None.
850    ///
851    /// Built per request rather than kept: crossbeam initializes every slot up
852    /// front, so a standing bounded(65536) channel cost 17.5MB resident. Unbounded
853    /// because the count depends on load, not on the request; intrspc.arg caps it.
854    fn drain_sched_samples(&mut self, timeout: Option<Duration>) -> Result<Vec<SchedSample>> {
855        let (intrspc_tx, intrspc_rx) = channel::unbounded();
856
857        {
858            let mut builder = libbpf_rs::RingBufferBuilder::new();
859            builder.add(&mut self.skel.maps.introspec_msg, move |data| {
860                Scheduler::relay_introspec(data, &intrspc_tx)
861            })?;
862            let rb_mgr = builder.build()?;
863
864            match timeout {
865                Some(timeout) => rb_mgr.poll(timeout)?,
866                None => rb_mgr.consume()?,
867            }
868        }
869
870        Ok(intrspc_rx.try_iter().collect())
871    }
872
873    fn stats_req_to_res(&mut self, req: &StatsReq) -> Result<StatsRes> {
874        Ok(match req {
875            StatsReq::NewSampler(tid) => {
876                /* Discard whatever the BPF side queued before this client. */
877                self.drain_sched_samples(None)?;
878                self.monitor_tid = Some(*tid);
879                StatsRes::Ack
880            }
881            StatsReq::SysStatsReq { tid } => {
882                if Some(*tid) != self.monitor_tid {
883                    return Ok(StatsRes::Bye);
884                }
885                self.mseq_id += 1;
886
887                let bss_data = self.skel.maps.bss_data.as_ref().unwrap();
888                let st = bss_data.sys_stat;
889
890                let mseq = self.mseq_id;
891                let nr_queued_task = st.nr_queued_task;
892                let nr_active = st.nr_active;
893                let nr_sched = st.nr_sched;
894                let nr_preempt = st.nr_preempt;
895                let pc_pc = Self::get_pc(st.nr_perf_cri, nr_sched);
896                let pc_lc = Self::get_pc(st.nr_lat_cri, nr_sched);
897                let pc_x_migration = Self::get_pc(st.nr_x_migration, nr_sched);
898                let nr_stealee = st.nr_stealee;
899                let nr_big = st.nr_big;
900                let pc_big = Self::get_pc(nr_big, nr_sched);
901                let pc_pc_on_big = Self::get_pc(st.nr_pc_on_big, nr_big);
902                let pc_lc_on_big = Self::get_pc(st.nr_lc_on_big, nr_big);
903                let power_mode = Self::get_power_mode(bss_data.power_mode);
904                let total_time = bss_data.performance_mode_ns
905                    + bss_data.balanced_mode_ns
906                    + bss_data.powersave_mode_ns;
907                let pc_performance = Self::get_pc(bss_data.performance_mode_ns, total_time);
908                let pc_balanced = Self::get_pc(bss_data.balanced_mode_ns, total_time);
909                let pc_powersave = Self::get_pc(bss_data.powersave_mode_ns, total_time);
910
911                StatsRes::SysStats(SysStats {
912                    mseq,
913                    nr_queued_task,
914                    nr_active,
915                    nr_sched,
916                    nr_preempt,
917                    pc_pc,
918                    pc_lc,
919                    pc_x_migration,
920                    nr_stealee,
921                    pc_big,
922                    pc_pc_on_big,
923                    pc_lc_on_big,
924                    power_mode: power_mode.to_string(),
925                    pc_performance,
926                    pc_balanced,
927                    pc_powersave,
928                })
929            }
930            StatsReq::SchedSamplesNr {
931                tid,
932                nr_samples,
933                interval_ms,
934            } => {
935                if Some(*tid) != self.monitor_tid {
936                    return Ok(StatsRes::Bye);
937                }
938
939                self.intrspc.cmd = LAVD_CMD_SCHED_N;
940                self.intrspc.arg = *nr_samples;
941                self.prep_introspec();
942                std::thread::sleep(Duration::from_millis(*interval_ms));
943
944                let samples = self.drain_sched_samples(Some(Duration::from_millis(100)))?;
945
946                self.cleanup_introspec();
947
948                StatsRes::SchedSamples(SchedSamples { samples })
949            }
950        })
951    }
952
953    fn stop_monitoring(&mut self) {
954        if self.skel.maps.bss_data.as_ref().unwrap().is_monitored {
955            self.skel.maps.bss_data.as_mut().unwrap().is_monitored = false;
956        }
957    }
958
959    pub fn exited(&mut self) -> bool {
960        uei_exited!(&self.skel, uei)
961    }
962
963    fn set_power_profile(&mut self, mode: u32) -> Result<(), u32> {
964        let prog = &mut self.skel.progs.set_power_profile;
965        let mut args = power_arg {
966            power_mode: mode as c_int,
967        };
968        let input = ProgramInput {
969            context_in: Some(unsafe {
970                std::slice::from_raw_parts_mut(
971                    &mut args as *mut _ as *mut u8,
972                    std::mem::size_of_val(&args),
973                )
974            }),
975            ..Default::default()
976        };
977        let out = prog.test_run(input).unwrap();
978        if out.return_value != 0 {
979            return Err(out.return_value);
980        }
981
982        Ok(())
983    }
984
985    fn update_power_profile(&mut self, prev_profile: PowerProfile) -> (bool, PowerProfile) {
986        let profile = fetch_power_profile(false);
987        if profile == prev_profile {
988            // If the profile is the same, skip updating the profile for BPF.
989            return (true, profile);
990        }
991
992        let _ = match profile {
993            PowerProfile::Performance => self.set_power_profile(LAVD_PM_PERFORMANCE),
994            PowerProfile::Balanced { .. } => self.set_power_profile(LAVD_PM_BALANCED),
995            PowerProfile::Powersave => self.set_power_profile(LAVD_PM_POWERSAVE),
996            PowerProfile::Unknown => {
997                // We don't know how to handle an unknown energy profile,
998                // so we just give up updating the profile from now on.
999                return (false, profile);
1000            }
1001        };
1002
1003        info!("Set the scheduler's power profile to {profile} mode.");
1004        (true, profile)
1005    }
1006
1007    fn run(&mut self, opts: &Opts, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
1008        let (res_ch, req_ch) = self.stats_server.channels();
1009        let mut autopower = opts.autopower;
1010        let mut profile = PowerProfile::Unknown;
1011
1012        if opts.performance {
1013            let _ = self.set_power_profile(LAVD_PM_PERFORMANCE);
1014        } else if opts.powersave {
1015            let _ = self.set_power_profile(LAVD_PM_POWERSAVE);
1016        } else {
1017            let _ = self.set_power_profile(LAVD_PM_BALANCED);
1018        }
1019
1020        while !shutdown.load(Ordering::Relaxed) && !self.exited() {
1021            if autopower {
1022                (autopower, profile) = self.update_power_profile(profile);
1023            }
1024
1025            match req_ch.recv_timeout(Duration::from_secs(1)) {
1026                Ok(req) => {
1027                    let res = self.stats_req_to_res(&req)?;
1028                    res_ch.send(res)?;
1029                }
1030                Err(RecvTimeoutError::Timeout) => {
1031                    self.stop_monitoring();
1032                }
1033                Err(e) => {
1034                    self.stop_monitoring();
1035                    Err(e)?
1036                }
1037            }
1038            self.cleanup_introspec();
1039        }
1040
1041        bpf_streams::dump_bpf_streams(&mut self.skel);
1042        let _ = self.struct_ops.take();
1043        uei_report!(&self.skel, uei)
1044    }
1045}
1046
1047impl Drop for Scheduler<'_> {
1048    fn drop(&mut self) {
1049        info!("Unregister {SCHEDULER_NAME} scheduler");
1050
1051        if let Some(struct_ops) = self.struct_ops.take() {
1052            drop(struct_ops);
1053        }
1054    }
1055}
1056
1057/// Return heap freed during initialization to the OS.
1058///
1059/// libbpf drops its copy of the BPF object's sections and its BTF once loading
1060/// finishes, and the CPU topology and energy model are dropped once the BPF side
1061/// has been initialized from them. glibc holds those pages until asked.
1062///
1063/// glibc only: musl has no malloc_trim(), and needs none, since it returns freed
1064/// memory to the kernel rather than parking it in per-arena free lists.
1065fn trim_heap_after_init() {
1066    #[cfg(target_env = "gnu")]
1067    unsafe {
1068        libc::malloc_trim(0);
1069    }
1070}
1071
1072fn init_log(opts: &Opts) {
1073    let env_filter = EnvFilter::try_from_default_env()
1074        .or_else(|_| match EnvFilter::try_new(&opts.log_level) {
1075            Ok(filter) => Ok(filter),
1076            Err(e) => {
1077                eprintln!(
1078                    "invalid log envvar: {}, using info, err is: {}",
1079                    opts.log_level, e
1080                );
1081                EnvFilter::try_new("info")
1082            }
1083        })
1084        .unwrap_or_else(|_| EnvFilter::new("info"));
1085
1086    match tracing_subscriber::fmt()
1087        .with_env_filter(env_filter)
1088        .with_target(true)
1089        .with_thread_ids(true)
1090        .with_file(true)
1091        .with_line_number(true)
1092        .try_init()
1093    {
1094        Ok(()) => {}
1095        Err(e) => eprintln!("failed to init logger: {}", e),
1096    }
1097}
1098
1099#[clap_main::clap_main]
1100fn main(mut opts: Opts) -> Result<()> {
1101    if opts.version {
1102        println!(
1103            "scx_lavd {}",
1104            build_id::full_version(env!("CARGO_PKG_VERSION"))
1105        );
1106        return Ok(());
1107    }
1108
1109    if opts.help_stats {
1110        let sys_stats_meta_name = SysStats::meta().name;
1111        let sched_sample_meta_name = SchedSample::meta().name;
1112        let stats_meta_names: &[&str] = &[
1113            sys_stats_meta_name.as_str(),
1114            sched_sample_meta_name.as_str(),
1115        ];
1116        stats::server_data(0).describe_meta(&mut std::io::stdout(), Some(&stats_meta_names))?;
1117        return Ok(());
1118    }
1119
1120    init_log(&opts);
1121
1122    if opts.verbose > 0 {
1123        warn!("Setting verbose via -v is deprecated and will be an error in future releases.");
1124    }
1125
1126    if let Some(run_id) = opts.run_id {
1127        info!("scx_lavd run_id: {}", run_id);
1128    }
1129
1130    if opts.monitor.is_none() && opts.monitor_sched_samples.is_none() {
1131        opts.proc().unwrap();
1132        info!("{:#?}", opts);
1133    }
1134
1135    let shutdown = Arc::new(AtomicBool::new(false));
1136    let shutdown_clone = shutdown.clone();
1137    ctrlc::set_handler(move || {
1138        shutdown_clone.store(true, Ordering::Relaxed);
1139    })
1140    .context("Error setting Ctrl-C handler")?;
1141
1142    if let Some(nr_samples) = opts.monitor_sched_samples {
1143        let shutdown_copy = shutdown.clone();
1144        let jh = std::thread::spawn(move || {
1145            stats::monitor_sched_samples(nr_samples, shutdown_copy).unwrap()
1146        });
1147        let _ = jh.join();
1148        return Ok(());
1149    }
1150
1151    if let Some(intv) = opts.monitor.or(opts.stats) {
1152        let shutdown_copy = shutdown.clone();
1153        let jh = std::thread::spawn(move || {
1154            stats::monitor(Duration::from_secs_f64(intv), shutdown_copy).unwrap()
1155        });
1156        if opts.monitor.is_some() {
1157            let _ = jh.join();
1158            return Ok(());
1159        }
1160    }
1161
1162    let mut open_object = MaybeUninit::uninit();
1163    loop {
1164        let mut sched = Scheduler::init(&opts, &mut open_object)?;
1165        info!(
1166            "scx_lavd scheduler is initialized (build ID: {})",
1167            build_id::full_version(env!("CARGO_PKG_VERSION"))
1168        );
1169        info!("scx_lavd scheduler starts running.");
1170        trim_heap_after_init();
1171        if !sched.run(&opts, shutdown.clone())?.should_restart() {
1172            break;
1173        }
1174    }
1175
1176    Ok(())
1177}