Skip to main content

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