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