Skip to main content

scx_p2dq/
main.rs

1// Copyright (c) Meta Platforms, Inc. and affiliates.
2
3// This software may be used and distributed according to the terms of the
4// GNU General Public License version 2.
5pub mod stats;
6use stats::Metrics;
7
8use std::mem::MaybeUninit;
9use std::sync::Arc;
10use std::sync::atomic::AtomicBool;
11use std::sync::atomic::Ordering;
12use std::time::Duration;
13
14use anyhow::Context;
15use anyhow::Result;
16use anyhow::bail;
17use clap::Parser;
18use crossbeam::channel::RecvTimeoutError;
19use libbpf_rs::AsRawLibbpf;
20use libbpf_rs::MapCore as _;
21use libbpf_rs::OpenObject;
22use libbpf_rs::ProgramInput;
23use libbpf_rs::skel::Skel;
24use scx_arena::ArenaLib;
25use scx_stats::prelude::*;
26use scx_utils::NR_CPU_IDS;
27use scx_utils::Topology;
28use scx_utils::UserExitInfo;
29use scx_utils::build_id;
30use scx_utils::compat;
31use scx_utils::init_libbpf_logging;
32use scx_utils::libbpf_clap_opts::LibbpfOpts;
33use scx_utils::pm::{
34    cpu_idle_resume_latency_supported, epp_supported, for_each_uncore_domain, get_epp,
35    get_turbo_enabled, get_uncore_max_freq_khz, get_uncore_min_freq_khz, set_epp,
36    set_turbo_enabled, set_uncore_max_freq_khz, turbo_supported, uncore_freq_supported,
37    update_cpu_idle_resume_latency,
38};
39use scx_utils::scx_ops_attach;
40use scx_utils::scx_ops_load;
41use scx_utils::scx_ops_open;
42use scx_utils::uei_exited;
43use scx_utils::uei_report;
44use tracing::{debug, info, warn};
45use tracing_subscriber::filter::EnvFilter;
46
47use bpf_intf::stat_idx_P2DQ_NR_STATS;
48use bpf_intf::stat_idx_P2DQ_STAT_ATQ_ENQ;
49use bpf_intf::stat_idx_P2DQ_STAT_ATQ_REENQ;
50use bpf_intf::stat_idx_P2DQ_STAT_DIRECT;
51use bpf_intf::stat_idx_P2DQ_STAT_DISPATCH_PICK2;
52use bpf_intf::stat_idx_P2DQ_STAT_DSQ_CHANGE;
53use bpf_intf::stat_idx_P2DQ_STAT_DSQ_SAME;
54use bpf_intf::stat_idx_P2DQ_STAT_EAS_BIG_SELECT;
55use bpf_intf::stat_idx_P2DQ_STAT_EAS_FALLBACK;
56use bpf_intf::stat_idx_P2DQ_STAT_EAS_LITTLE_SELECT;
57use bpf_intf::stat_idx_P2DQ_STAT_ENQ_CPU;
58use bpf_intf::stat_idx_P2DQ_STAT_ENQ_INTR;
59use bpf_intf::stat_idx_P2DQ_STAT_ENQ_LLC;
60use bpf_intf::stat_idx_P2DQ_STAT_ENQ_MIG;
61use bpf_intf::stat_idx_P2DQ_STAT_EXEC_BALANCE;
62use bpf_intf::stat_idx_P2DQ_STAT_EXEC_SAME_LLC;
63use bpf_intf::stat_idx_P2DQ_STAT_FORK_BALANCE;
64use bpf_intf::stat_idx_P2DQ_STAT_FORK_SAME_LLC;
65use bpf_intf::stat_idx_P2DQ_STAT_IDLE;
66use bpf_intf::stat_idx_P2DQ_STAT_KEEP;
67use bpf_intf::stat_idx_P2DQ_STAT_LLC_MIGRATION;
68use bpf_intf::stat_idx_P2DQ_STAT_NODE_MIGRATION;
69use bpf_intf::stat_idx_P2DQ_STAT_SELECT_PICK2;
70use bpf_intf::stat_idx_P2DQ_STAT_THERMAL_AVOID;
71use bpf_intf::stat_idx_P2DQ_STAT_THERMAL_KICK;
72use bpf_intf::stat_idx_P2DQ_STAT_WAKE_LLC;
73use bpf_intf::stat_idx_P2DQ_STAT_WAKE_MIG;
74use bpf_intf::stat_idx_P2DQ_STAT_WAKE_PREV;
75use scx_p2dq::SchedulerOpts;
76use scx_p2dq::TOPO;
77use scx_p2dq::bpf_intf;
78use scx_p2dq::bpf_skel::*;
79
80const SCHEDULER_NAME: &str = "scx_p2dq";
81/// scx_p2dq: A pick 2 dumb queuing load balancing scheduler.
82///
83/// The BPF part does simple vtime or round robin scheduling in each domain
84/// while tracking average load of each domain and duty cycle of each task.
85///
86#[derive(Debug, Parser)]
87struct CliOpts {
88    /// Deprecated, noop, use RUST_LOG or --log-level instead.
89    #[clap(short = 'v', long, action = clap::ArgAction::Count)]
90    verbose: u8,
91
92    /// Specify the logging level. Accepts rust's envfilter syntax for modular
93    /// logging: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax. Examples: ["info", "warn,tokio=info"]
94    #[clap(long, default_value = "info")]
95    pub log_level: String,
96
97    /// Enable stats monitoring with the specified interval.
98    #[clap(long)]
99    pub stats: Option<f64>,
100
101    /// Run in stats monitoring mode with the specified interval. Scheduler
102    /// is not launched.
103    #[clap(long)]
104    pub monitor: Option<f64>,
105
106    /// Print version and exit.
107    #[clap(long)]
108    pub version: bool,
109
110    /// Optional run ID for tracking scheduler instances.
111    #[clap(long)]
112    pub run_id: Option<u64>,
113
114    #[clap(flatten)]
115    pub sched: SchedulerOpts,
116
117    #[clap(flatten, next_help_heading = "Libbpf Options")]
118    pub libbpf: LibbpfOpts,
119}
120
121struct Scheduler<'a> {
122    skel: BpfSkel<'a>,
123    struct_ops: Option<libbpf_rs::Link>,
124    debug_level: u8,
125
126    stats_server: StatsServer<(), Metrics>,
127}
128
129impl<'a> Scheduler<'a> {
130    fn init(
131        opts: &SchedulerOpts,
132        libbpf_ops: &LibbpfOpts,
133        open_object: &'a mut MaybeUninit<OpenObject>,
134        log_level: &str,
135    ) -> Result<Self> {
136        // Open the BPF prog first for verification.
137        let debug_level = if log_level.contains("trace") {
138            2
139        } else if log_level.contains("debug") {
140            1
141        } else {
142            0
143        };
144        let mut skel_builder = BpfSkelBuilder::default();
145        skel_builder.obj_builder.debug(debug_level > 1);
146        init_libbpf_logging(None);
147        info!(
148            "Running scx_p2dq (build ID: {})",
149            build_id::full_version(env!("CARGO_PKG_VERSION"))
150        );
151        let topo = if opts.virt_llc_enabled {
152            Topology::with_args(&opts.topo)?
153        } else {
154            Topology::new()?
155        };
156        let open_opts = libbpf_ops.clone().into_bpf_open_opts();
157        let mut open_skel = scx_ops_open!(skel_builder, open_object, p2dq, open_opts).context(
158            "Failed to open BPF object. This can be caused by a mismatch between the kernel \
159            version and the BPF object, permission or other libbpf issues. Try running `dmesg \
160            | grep bpf` to see if there are any error messages related to the BPF object. See \
161            the LibbpfOptions section in the help for more information on configuration related \
162            to this issue or file an issue on the scx repo if the problem persists. \
163            https://github.com/sched-ext/scx/issues/new?labels=scx_p2dq&title=scx_p2dq:%20New%20Issue&assignees=hodgesds&body=Kernel%20version:%20(fill%20me%20out)%0ADistribution:%20(fill%20me%20out)%0AHardware:%20(fill%20me%20out)%0A%0AIssue:%20(fill%20me%20out)"
164        )?;
165
166        // Disable autoload for thermal pressure tracepoint by default
167        // Will be conditionally enabled if kernel supports it
168        // Note: This tracepoint only exists on ARM/ARM64 architectures
169        #[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
170        open_skel.progs.on_thermal_pressure.set_autoload(false);
171
172        // Apply hardware-specific optimizations before macro
173        let hw_profile = scx_p2dq::HardwareProfile::detect();
174        let mut opts_optimized = opts.clone();
175        if opts.hw_auto_optimize {
176            hw_profile.optimize_scheduler_opts(&mut opts_optimized);
177        }
178
179        scx_p2dq::init_open_skel!(
180            &mut open_skel,
181            topo,
182            &opts_optimized,
183            debug_level,
184            &hw_profile
185        )?;
186
187        // Thermal pressure tracking (ARM/ARM64 only)
188        #[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
189        {
190            let thermal_enabled = std::path::Path::new(
191                "/sys/kernel/tracing/events/thermal_pressure/hw_pressure_update",
192            )
193            .exists()
194                || std::path::Path::new(
195                    "/sys/kernel/debug/tracing/events/thermal_pressure/hw_pressure_update",
196                )
197                .exists();
198
199            if thermal_enabled {
200                debug!(
201                    "Kernel supports thermal pressure tracking, enabling hw_pressure_update tracepoint"
202                );
203                open_skel.progs.on_thermal_pressure.set_autoload(true);
204                stats::set_thermal_tracking_enabled(true);
205
206                open_skel
207                    .maps
208                    .rodata_data
209                    .as_mut()
210                    .unwrap()
211                    .p2dq_config
212                    .thermal_enabled = std::mem::MaybeUninit::new(true);
213            } else {
214                debug!(
215                    "Kernel does not support thermal pressure tracking (CONFIG_SCHED_HW_PRESSURE not enabled)"
216                );
217            }
218        }
219
220        if opts_optimized.enable_eas {
221            stats::set_eas_enabled(true);
222        }
223
224        if opts_optimized.atq_enabled && compat::ksym_exists("bpf_spin_unlock").unwrap_or(false) {
225            stats::set_atq_enabled(true);
226        }
227
228        if opts.queued_wakeup {
229            open_skel.struct_ops.p2dq_mut().flags |= *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP;
230        }
231        open_skel.struct_ops.p2dq_mut().flags |= *compat::SCX_OPS_KEEP_BUILTIN_IDLE;
232
233        // Disable autoattach for the struct_ops map since we attach it manually via
234        // attach_struct_ops() in scx_ops_attach!(). This prevents libbpf from warning
235        // about uninitialized skeleton link during attach().
236        unsafe {
237            libbpf_rs::libbpf_sys::bpf_map__set_autoattach(
238                open_skel.maps.p2dq.as_libbpf_object().as_ptr(),
239                false,
240            );
241        }
242
243        let mut skel = scx_ops_load!(open_skel, p2dq, uei)?;
244        scx_p2dq::init_skel!(&mut skel, topo);
245
246        let stats_server = StatsServer::new(stats::server_data()).launch()?;
247
248        Ok(Self {
249            skel,
250            struct_ops: None,
251            debug_level,
252            stats_server,
253        })
254    }
255
256    fn get_metrics(&self) -> Metrics {
257        let mut stats = vec![0u64; stat_idx_P2DQ_NR_STATS as usize];
258        let stats_map = &self.skel.maps.stats;
259        for stat in 0..stat_idx_P2DQ_NR_STATS {
260            let cpu_stat_vec: Vec<Vec<u8>> = stats_map
261                .lookup_percpu(&stat.to_ne_bytes(), libbpf_rs::MapFlags::ANY)
262                .unwrap()
263                .unwrap();
264            let sum: u64 = cpu_stat_vec
265                .iter()
266                .map(|val| u64::from_ne_bytes(val.as_slice().try_into().unwrap()))
267                .sum();
268            stats[stat as usize] = sum;
269        }
270        Metrics {
271            atq_enq: stats[stat_idx_P2DQ_STAT_ATQ_ENQ as usize],
272            atq_reenq: stats[stat_idx_P2DQ_STAT_ATQ_REENQ as usize],
273            direct: stats[stat_idx_P2DQ_STAT_DIRECT as usize],
274            idle: stats[stat_idx_P2DQ_STAT_IDLE as usize],
275            dsq_change: stats[stat_idx_P2DQ_STAT_DSQ_CHANGE as usize],
276            same_dsq: stats[stat_idx_P2DQ_STAT_DSQ_SAME as usize],
277            keep: stats[stat_idx_P2DQ_STAT_KEEP as usize],
278            enq_cpu: stats[stat_idx_P2DQ_STAT_ENQ_CPU as usize],
279            enq_intr: stats[stat_idx_P2DQ_STAT_ENQ_INTR as usize],
280            enq_llc: stats[stat_idx_P2DQ_STAT_ENQ_LLC as usize],
281            enq_mig: stats[stat_idx_P2DQ_STAT_ENQ_MIG as usize],
282            select_pick2: stats[stat_idx_P2DQ_STAT_SELECT_PICK2 as usize],
283            dispatch_pick2: stats[stat_idx_P2DQ_STAT_DISPATCH_PICK2 as usize],
284            llc_migrations: stats[stat_idx_P2DQ_STAT_LLC_MIGRATION as usize],
285            node_migrations: stats[stat_idx_P2DQ_STAT_NODE_MIGRATION as usize],
286            wake_prev: stats[stat_idx_P2DQ_STAT_WAKE_PREV as usize],
287            wake_llc: stats[stat_idx_P2DQ_STAT_WAKE_LLC as usize],
288            wake_mig: stats[stat_idx_P2DQ_STAT_WAKE_MIG as usize],
289            fork_balance: stats[stat_idx_P2DQ_STAT_FORK_BALANCE as usize],
290            exec_balance: stats[stat_idx_P2DQ_STAT_EXEC_BALANCE as usize],
291            fork_same_llc: stats[stat_idx_P2DQ_STAT_FORK_SAME_LLC as usize],
292            exec_same_llc: stats[stat_idx_P2DQ_STAT_EXEC_SAME_LLC as usize],
293            thermal_kick: stats[stat_idx_P2DQ_STAT_THERMAL_KICK as usize],
294            thermal_avoid: stats[stat_idx_P2DQ_STAT_THERMAL_AVOID as usize],
295            eas_little_select: stats[stat_idx_P2DQ_STAT_EAS_LITTLE_SELECT as usize],
296            eas_big_select: stats[stat_idx_P2DQ_STAT_EAS_BIG_SELECT as usize],
297            eas_fallback: stats[stat_idx_P2DQ_STAT_EAS_FALLBACK as usize],
298        }
299    }
300
301    fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
302        let (res_ch, req_ch) = self.stats_server.channels();
303
304        while !shutdown.load(Ordering::Relaxed) && !uei_exited!(&self.skel, uei) {
305            match req_ch.recv_timeout(Duration::from_secs(1)) {
306                Ok(()) => res_ch.send(self.get_metrics())?,
307                Err(RecvTimeoutError::Timeout) => {}
308                Err(e) => Err(e)?,
309            }
310        }
311
312        let _ = self.struct_ops.take();
313        uei_report!(&self.skel, uei)
314    }
315
316    fn print_topology(&mut self) -> Result<()> {
317        let input = ProgramInput {
318            ..Default::default()
319        };
320
321        let output = self.skel.progs.arena_topology_print.test_run(input)?;
322        if output.return_value != 0 {
323            bail!(
324                "Could not initialize arenas, topo_print returned {}",
325                output.return_value as i32
326            );
327        }
328
329        Ok(())
330    }
331
332    fn start(&mut self) -> Result<()> {
333        self.struct_ops = Some(scx_ops_attach!(self.skel, p2dq)?);
334
335        if self.debug_level > 0 {
336            self.print_topology()?;
337        }
338
339        info!("P2DQ scheduler started! Run `scx_p2dq --monitor` for metrics.");
340
341        Ok(())
342    }
343}
344
345impl Drop for Scheduler<'_> {
346    fn drop(&mut self) {
347        info!("Unregister {SCHEDULER_NAME} scheduler");
348
349        if let Some(struct_ops) = self.struct_ops.take() {
350            drop(struct_ops);
351        }
352    }
353}
354
355#[clap_main::clap_main]
356fn main(opts: CliOpts) -> Result<()> {
357    if opts.version {
358        println!(
359            "scx_p2dq: {}",
360            build_id::full_version(env!("CARGO_PKG_VERSION"))
361        );
362        return Ok(());
363    }
364
365    let env_filter = EnvFilter::try_from_default_env()
366        .or_else(|_| match EnvFilter::try_new(&opts.log_level) {
367            Ok(filter) => Ok(filter),
368            Err(e) => {
369                eprintln!(
370                    "invalid log envvar: {}, using info, err is: {}",
371                    opts.log_level, e
372                );
373                EnvFilter::try_new("info")
374            }
375        })
376        .unwrap_or_else(|_| EnvFilter::new("info"));
377
378    match tracing_subscriber::fmt()
379        .with_env_filter(env_filter)
380        .with_target(true)
381        .with_thread_ids(true)
382        .with_file(true)
383        .with_line_number(true)
384        .try_init()
385    {
386        Ok(()) => {}
387        Err(e) => eprintln!("failed to init logger: {}", e),
388    }
389
390    if opts.verbose > 0 {
391        warn!("Setting verbose via -v is deprecated and will be an error in future releases.");
392    }
393
394    if let Some(run_id) = opts.run_id {
395        info!("scx_p2dq run_id: {}", run_id);
396    }
397
398    let shutdown = Arc::new(AtomicBool::new(false));
399    let shutdown_clone = shutdown.clone();
400    ctrlc::set_handler(move || {
401        shutdown_clone.store(true, Ordering::Relaxed);
402    })
403    .context("Error setting Ctrl-C handler")?;
404
405    if let Some(intv) = opts.monitor.or(opts.stats) {
406        let shutdown_copy = shutdown.clone();
407        let jh = std::thread::spawn(move || {
408            match stats::monitor(Duration::from_secs_f64(intv), shutdown_copy) {
409                Ok(_) => {
410                    debug!("stats monitor thread finished successfully")
411                }
412                Err(error_object) => {
413                    warn!("stats monitor thread finished because of an error {error_object}")
414                }
415            }
416        });
417        if opts.monitor.is_some() {
418            let _ = jh.join();
419            return Ok(());
420        }
421    }
422
423    if let Some(idle_resume_us) = opts.sched.idle_resume_us {
424        if !cpu_idle_resume_latency_supported() {
425            warn!("idle resume latency not supported");
426        } else if idle_resume_us > 0 {
427            info!("Setting idle QoS to {idle_resume_us}us");
428            for cpu in TOPO.all_cpus.values() {
429                update_cpu_idle_resume_latency(cpu.id, idle_resume_us.try_into().unwrap())?;
430            }
431        }
432    }
433
434    let is_efficiency = opts.sched.sched_mode == scx_p2dq::SchedMode::Efficiency;
435    let is_performance = opts.sched.sched_mode == scx_p2dq::SchedMode::Performance;
436
437    let mut orig_uncore_freqs: Vec<(u32, u32, u32)> = Vec::new();
438    if opts.sched.uncore_max_freq_mhz.is_some() || is_efficiency || is_performance {
439        if !uncore_freq_supported() {
440            if opts.sched.uncore_max_freq_mhz.is_some() {
441                warn!("uncore frequency control not supported");
442            }
443        } else {
444            let _ = for_each_uncore_domain(|pkg, die| {
445                let freq_khz = if let Some(mhz) = opts.sched.uncore_max_freq_mhz {
446                    mhz * 1000
447                } else if is_efficiency {
448                    get_uncore_min_freq_khz(pkg, die)?
449                } else {
450                    get_uncore_max_freq_khz(pkg, die)?
451                };
452                if let Ok(orig) = get_uncore_max_freq_khz(pkg, die)
453                    && orig != freq_khz
454                {
455                    info!(
456                        "Setting max uncore frequency for package {} die {} to {} MHz",
457                        pkg,
458                        die,
459                        freq_khz / 1000
460                    );
461                    orig_uncore_freqs.push((pkg, die, orig));
462                    set_uncore_max_freq_khz(pkg, die, freq_khz)?;
463                }
464                Ok(())
465            });
466        }
467    }
468
469    let mut orig_epps: Vec<(usize, String)> = Vec::new();
470    if (is_efficiency || is_performance) && epp_supported() {
471        let target_epp = if is_efficiency {
472            "power"
473        } else {
474            "performance"
475        };
476        for cpu in TOPO.all_cpus.values() {
477            if let Ok(orig) = get_epp(cpu.id)
478                && orig != target_epp
479            {
480                if orig_epps.is_empty() {
481                    info!("Setting EPP to {} for all CPUs", target_epp);
482                }
483                orig_epps.push((cpu.id, orig));
484                let _ = set_epp(cpu.id, target_epp);
485            }
486        }
487    }
488
489    let orig_turbo = if turbo_supported() {
490        let target_turbo = opts.sched.turbo.or(if is_efficiency {
491            Some(false)
492        } else if is_performance {
493            Some(true)
494        } else {
495            None
496        });
497        if let Some(want_enabled) = target_turbo {
498            if let Ok(current) = get_turbo_enabled() {
499                if current != want_enabled {
500                    let mode_suffix = if opts.sched.turbo.is_none() {
501                        if is_efficiency {
502                            " for efficiency mode"
503                        } else {
504                            " for performance mode"
505                        }
506                    } else {
507                        ""
508                    };
509                    info!(
510                        "{} turbo{}",
511                        if want_enabled {
512                            "Enabling"
513                        } else {
514                            "Disabling"
515                        },
516                        mode_suffix
517                    );
518                    let _ = set_turbo_enabled(want_enabled);
519                    Some(current)
520                } else {
521                    None
522                }
523            } else {
524                None
525            }
526        } else {
527            None
528        }
529    } else {
530        if opts.sched.turbo.is_some() {
531            warn!("turbo control not supported");
532        }
533        None
534    };
535
536    let mut open_object = MaybeUninit::uninit();
537    loop {
538        /* declared before sched to drop, and join, after sched detaches */
539        let _arenalib;
540        let mut sched =
541            Scheduler::init(&opts.sched, &opts.libbpf, &mut open_object, &opts.log_level)?;
542        let task_size = std::mem::size_of::<types::task_p2dq>();
543        _arenalib = ArenaLib::setup(sched.skel.object_mut(), task_size, 0, *NR_CPU_IDS)?;
544
545        sched.start()?;
546
547        if !sched.run(shutdown.clone())?.should_restart() {
548            break;
549        }
550    }
551
552    if let Some(was_enabled) = orig_turbo {
553        info!(
554            "Restoring turbo to {}",
555            if was_enabled { "enabled" } else { "disabled" }
556        );
557        let _ = set_turbo_enabled(was_enabled);
558    }
559
560    if !orig_epps.is_empty() {
561        info!("Restoring EPP settings");
562        for (cpu, epp) in orig_epps {
563            let _ = set_epp(cpu, &epp);
564        }
565    }
566
567    for (pkg, die, orig_khz) in orig_uncore_freqs {
568        info!(
569            "Restoring uncore frequency for package {} die {} to {} MHz",
570            pkg,
571            die,
572            orig_khz / 1000
573        );
574        let _ = set_uncore_max_freq_khz(pkg, die, orig_khz);
575    }
576
577    Ok(())
578}