Skip to main content

scx_beerland/
main.rs

1// SPDX-License-Identifier: GPL-2.0
2//
3// Copyright (c) 2025 Andrea Righi <arighi@nvidia.com>
4
5// This software may be used and distributed according to the terms of the
6// GNU General Public License version 2.
7
8mod bpf_skel;
9pub use bpf_skel::*;
10pub mod bpf_intf;
11pub use bpf_intf::*;
12
13mod stats;
14use std::collections::HashSet;
15use std::ffi::{c_int, c_ulong};
16use std::mem::MaybeUninit;
17use std::sync::atomic::AtomicBool;
18use std::sync::atomic::Ordering;
19use std::sync::Arc;
20use std::time::Duration;
21
22use anyhow::bail;
23use anyhow::Context;
24use anyhow::Result;
25use clap::Parser;
26use crossbeam::channel::RecvTimeoutError;
27use libbpf_rs::OpenObject;
28use libbpf_rs::ProgramInput;
29use log::{debug, info, warn};
30use scx_stats::prelude::*;
31use scx_utils::build_id;
32use scx_utils::compat;
33use scx_utils::get_primary_cpus;
34use scx_utils::libbpf_clap_opts::LibbpfOpts;
35use scx_utils::scx_ops_attach;
36use scx_utils::scx_ops_load;
37use scx_utils::scx_ops_open;
38use scx_utils::try_set_rlimit_infinity;
39use scx_utils::uei_exited;
40use scx_utils::uei_report;
41use scx_utils::Powermode;
42use scx_utils::Topology;
43use scx_utils::UserExitInfo;
44use scx_utils::NR_CPU_IDS;
45use stats::Metrics;
46
47const SCHEDULER_NAME: &str = "scx_beerland";
48
49#[derive(Debug, clap::Parser)]
50#[command(
51    name = "scx_beerland",
52    version,
53    disable_version_flag = true,
54    about = "Scheduler designed to prioritize locality and scalability."
55)]
56struct Opts {
57    /// Exit debug dump buffer length. 0 indicates default.
58    #[clap(long, default_value = "0")]
59    exit_dump_len: u32,
60
61    /// Maximum scheduling slice duration in microseconds.
62    #[clap(short = 's', long, default_value = "1000")]
63    slice_us: u64,
64
65    /// Maximum time slice lag in microseconds.
66    ///
67    /// A positive value can help to enhance the responsiveness of interactive tasks, but it can
68    /// also make performance more "spikey".
69    #[clap(short = 'l', long, default_value = "20000")]
70    slice_us_lag: u64,
71
72    /// CPU utilization percentage required to steal from a CPU.
73    ///
74    /// CPUs running tasks below this percentage are treated as not overloaded, so load balancer
75    /// will not pull tasks from their DSQ (0 disables the filter).
76    #[clap(short = 'c', long, default_value = "0")]
77    cpu_busy_thresh: u64,
78
79    /// Specifies a list of CPUs to prioritize.
80    ///
81    /// Accepts a comma-separated list of CPUs or ranges (i.e., 0-3,12-15) or the following special
82    /// keywords:
83    ///
84    /// "turbo" = automatically detect and prioritize the CPUs with the highest max frequency,
85    /// "performance" = automatically detect and prioritize the fastest CPUs,
86    /// "powersave" = automatically detect and prioritize the slowest CPUs,
87    /// "all" = all CPUs assigned to the primary domain.
88    ///
89    /// By default "all" CPUs are used.
90    #[clap(short = 'm', long)]
91    primary_domain: Option<String>,
92
93    /// Disable NUMA optimizations.
94    #[clap(short = 'n', long, action = clap::ArgAction::SetTrue)]
95    disable_numa: bool,
96
97    /// Enable stats monitoring with the specified interval.
98    #[clap(long)]
99    stats: Option<f64>,
100
101    /// Run in stats monitoring mode with the specified interval. Scheduler
102    /// is not launched.
103    #[clap(long)]
104    monitor: Option<f64>,
105
106    /// Enable verbose output, including libbpf details.
107    #[clap(short = 'v', long, action = clap::ArgAction::SetTrue)]
108    verbose: bool,
109
110    /// Print scheduler version and exit.
111    #[clap(short = 'V', long, action = clap::ArgAction::SetTrue)]
112    version: bool,
113
114    /// Show descriptions for statistics.
115    #[clap(long)]
116    help_stats: bool,
117
118    #[clap(flatten, next_help_heading = "Libbpf Options")]
119    pub libbpf: LibbpfOpts,
120}
121
122pub fn parse_cpu_list(optarg: &str) -> Result<Vec<usize>, String> {
123    let mut cpus = Vec::new();
124    let mut seen = HashSet::new();
125
126    // Handle special keywords
127    if let Some(mode) = match optarg {
128        "powersave" => Some(Powermode::Powersave),
129        "performance" => Some(Powermode::Performance),
130        "turbo" => Some(Powermode::Turbo),
131        "all" => Some(Powermode::Any),
132        _ => None,
133    } {
134        return get_primary_cpus(mode).map_err(|e| e.to_string());
135    }
136
137    // Validate input characters
138    if optarg
139        .chars()
140        .any(|c| !c.is_ascii_digit() && c != '-' && c != ',' && !c.is_whitespace())
141    {
142        return Err("Invalid character in CPU list".to_string());
143    }
144
145    // Replace all whitespace with tab (or just trim later)
146    let cleaned = optarg.replace(' ', "\t");
147
148    for token in cleaned.split(',') {
149        let token = token.trim_matches(|c: char| c.is_whitespace());
150
151        if token.is_empty() {
152            continue;
153        }
154
155        if let Some((start_str, end_str)) = token.split_once('-') {
156            let start = start_str
157                .trim()
158                .parse::<usize>()
159                .map_err(|_| "Invalid range start")?;
160            let end = end_str
161                .trim()
162                .parse::<usize>()
163                .map_err(|_| "Invalid range end")?;
164
165            if start > end {
166                return Err(format!("Invalid CPU range: {}-{}", start, end));
167            }
168
169            for i in start..=end {
170                if cpus.len() >= *NR_CPU_IDS {
171                    return Err(format!("Too many CPUs specified (max {})", *NR_CPU_IDS));
172                }
173                if seen.insert(i) {
174                    cpus.push(i);
175                }
176            }
177        } else {
178            let cpu = token
179                .parse::<usize>()
180                .map_err(|_| format!("Invalid CPU: {}", token))?;
181            if cpus.len() >= *NR_CPU_IDS {
182                return Err(format!("Too many CPUs specified (max {})", *NR_CPU_IDS));
183            }
184            if seen.insert(cpu) {
185                cpus.push(cpu);
186            }
187        }
188    }
189
190    Ok(cpus)
191}
192
193struct Scheduler<'a> {
194    skel: BpfSkel<'a>,
195    struct_ops: Option<libbpf_rs::Link>,
196    stats_server: StatsServer<(), Metrics>,
197}
198
199impl<'a> Scheduler<'a> {
200    fn init(opts: &'a Opts, open_object: &'a mut MaybeUninit<OpenObject>) -> Result<Self> {
201        try_set_rlimit_infinity();
202
203        // Initialize CPU topology.
204        let topo = Topology::new().unwrap();
205
206        // Check host topology to determine if we need to enable SMT capabilities.
207        let smt_enabled = topo.smt_enabled;
208
209        // Determine the amount of non-empty NUMA nodes in the system.
210        let nr_nodes = topo
211            .nodes
212            .values()
213            .filter(|node| !node.all_cpus.is_empty())
214            .count();
215        info!("NUMA nodes: {}", nr_nodes);
216
217        // Automatically disable NUMA optimizations when running on non-NUMA systems.
218        let numa_enabled = !opts.disable_numa && nr_nodes > 1;
219        if !numa_enabled {
220            info!("Disabling NUMA optimizations");
221        }
222
223        info!(
224            "{} {} {}",
225            SCHEDULER_NAME,
226            build_id::full_version(env!("CARGO_PKG_VERSION")),
227            if smt_enabled { "SMT on" } else { "SMT off" }
228        );
229
230        // Print command line.
231        info!(
232            "scheduler options: {}",
233            std::env::args().collect::<Vec<_>>().join(" ")
234        );
235
236        // Initialize BPF connector.
237        let mut skel_builder = BpfSkelBuilder::default();
238        skel_builder.obj_builder.debug(opts.verbose);
239        let open_opts = opts.libbpf.clone().into_bpf_open_opts();
240        let mut skel = scx_ops_open!(skel_builder, open_object, beerland_ops, open_opts)?;
241
242        skel.struct_ops.beerland_ops_mut().exit_dump_len = opts.exit_dump_len;
243
244        // Override default BPF scheduling parameters.
245        let rodata = skel.maps.rodata_data.as_mut().unwrap();
246        rodata.slice_ns = opts.slice_us * 1000;
247        rodata.slice_lag = opts.slice_us_lag * 1000;
248        rodata.smt_enabled = smt_enabled;
249        rodata.numa_enabled = numa_enabled;
250        let cpu_busy_thresh = opts.cpu_busy_thresh.min(100);
251        let cpu_busy_filter = cpu_busy_thresh > 0;
252        let busy_threshold = if cpu_busy_filter {
253            cpu_busy_thresh * 1024 / 100
254        } else {
255            0
256        };
257        rodata.busy_threshold = busy_threshold;
258        if cpu_busy_filter {
259            info!(
260                "BPF accounted-time CPU steal filter: threshold {}%",
261                cpu_busy_thresh
262            );
263        } else {
264            info!("BPF accounted-time CPU steal filter disabled");
265        }
266
267        // Define the primary scheduling domain.
268        let primary_cpus = if let Some(ref domain) = opts.primary_domain {
269            match parse_cpu_list(domain) {
270                Ok(cpus) => cpus,
271                Err(e) => bail!("Error parsing primary domain: {}", e),
272            }
273        } else {
274            (0..*NR_CPU_IDS).collect()
275        };
276        if primary_cpus.len() < *NR_CPU_IDS {
277            info!("Primary CPUs: {:?}", primary_cpus);
278            rodata.primary_all = false;
279        } else {
280            rodata.primary_all = true;
281        }
282
283        // Cache CPU capacity values for wakeup placement decisions.
284        for cpu in topo.all_cpus.values() {
285            rodata.cpu_capacity[cpu.id] = cpu.cpu_capacity as c_ulong;
286        }
287
288        // Set scheduler flags.
289        skel.struct_ops.beerland_ops_mut().flags = *compat::SCX_OPS_ENQ_EXITING
290            | *compat::SCX_OPS_ENQ_LAST
291            | *compat::SCX_OPS_ENQ_MIGRATION_DISABLED
292            | *compat::SCX_OPS_ALWAYS_ENQ_IMMED
293            | *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP
294            | if numa_enabled {
295                *compat::SCX_OPS_BUILTIN_IDLE_PER_NODE
296            } else {
297                0
298            };
299        info!(
300            "scheduler flags: {:#x}",
301            skel.struct_ops.beerland_ops_mut().flags
302        );
303
304        // Load the BPF program for validation.
305        let mut skel = scx_ops_load!(skel, beerland_ops, uei)?;
306
307        // Initialize SMT domains.
308        if smt_enabled {
309            Self::init_smt_domains(&mut skel, &topo)?;
310        }
311
312        // Enable primary scheduling domain, if defined.
313        if primary_cpus.len() < *NR_CPU_IDS {
314            for cpu in primary_cpus {
315                if let Err(err) = Self::enable_primary_cpu(&mut skel, cpu as i32) {
316                    bail!("failed to add CPU {} to primary domain: error {}", cpu, err);
317                }
318            }
319        }
320
321        // Attach the scheduler.
322        let struct_ops = Some(scx_ops_attach!(skel, beerland_ops)?);
323        let stats_server = StatsServer::new(stats::server_data()).launch()?;
324
325        Ok(Self {
326            skel,
327            struct_ops,
328            stats_server,
329        })
330    }
331
332    fn enable_sibling_cpu(
333        skel: &mut BpfSkel<'_>,
334        cpu: usize,
335        sibling_cpu: usize,
336    ) -> Result<(), u32> {
337        let prog = &mut skel.progs.enable_sibling_cpu;
338        let mut args = domain_arg {
339            cpu_id: cpu as c_int,
340            sibling_cpu_id: sibling_cpu as c_int,
341        };
342        let input = ProgramInput {
343            context_in: Some(unsafe {
344                std::slice::from_raw_parts_mut(
345                    &mut args as *mut _ as *mut u8,
346                    std::mem::size_of_val(&args),
347                )
348            }),
349            ..Default::default()
350        };
351        let out = prog.test_run(input).unwrap();
352        if out.return_value != 0 {
353            return Err(out.return_value);
354        }
355
356        Ok(())
357    }
358
359    fn enable_primary_cpu(skel: &mut BpfSkel<'_>, cpu: i32) -> Result<(), u32> {
360        let prog = &mut skel.progs.enable_primary_cpu;
361        let mut args = cpu_arg {
362            cpu_id: cpu as c_int,
363        };
364        let input = ProgramInput {
365            context_in: Some(unsafe {
366                std::slice::from_raw_parts_mut(
367                    &mut args as *mut _ as *mut u8,
368                    std::mem::size_of_val(&args),
369                )
370            }),
371            ..Default::default()
372        };
373        let out = prog.test_run(input).unwrap();
374        if out.return_value != 0 {
375            return Err(out.return_value);
376        }
377
378        Ok(())
379    }
380
381    fn init_smt_domains(skel: &mut BpfSkel<'_>, topo: &Topology) -> Result<(), std::io::Error> {
382        let smt_siblings = topo.sibling_cpus();
383
384        info!("SMT sibling CPUs: {:?}", smt_siblings);
385        for (cpu, sibling_cpu) in smt_siblings.iter().enumerate() {
386            Self::enable_sibling_cpu(skel, cpu, *sibling_cpu as usize).unwrap();
387        }
388
389        Ok(())
390    }
391
392    fn get_metrics(&mut self) -> Metrics {
393        let bss_data = self.skel.maps.bss_data.as_ref().unwrap();
394        Metrics {
395            nr_local_dispatch: bss_data.nr_local_dispatch,
396            nr_remote_dispatch: bss_data.nr_remote_dispatch,
397            nr_keep_running: bss_data.nr_keep_running,
398        }
399    }
400
401    pub fn exited(&mut self) -> bool {
402        uei_exited!(&self.skel, uei)
403    }
404
405    fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
406        let (res_ch, req_ch) = self.stats_server.channels();
407
408        while !shutdown.load(Ordering::Relaxed) && !self.exited() {
409            // Update statistics and check for exit condition.
410            match req_ch.recv_timeout(Duration::from_secs(1)) {
411                Ok(()) => res_ch.send(self.get_metrics())?,
412                Err(RecvTimeoutError::Timeout) => {}
413                Err(e) => Err(e)?,
414            }
415        }
416
417        let _ = self.struct_ops.take();
418        uei_report!(&self.skel, uei)
419    }
420}
421
422impl Drop for Scheduler<'_> {
423    fn drop(&mut self) {
424        info!("Unregister {SCHEDULER_NAME} scheduler");
425    }
426}
427
428fn main() -> Result<()> {
429    let opts = Opts::parse();
430
431    if opts.version {
432        println!(
433            "{} {}",
434            SCHEDULER_NAME,
435            build_id::full_version(env!("CARGO_PKG_VERSION"))
436        );
437        return Ok(());
438    }
439
440    if opts.help_stats {
441        stats::server_data().describe_meta(&mut std::io::stdout(), None)?;
442        return Ok(());
443    }
444
445    let loglevel = simplelog::LevelFilter::Info;
446
447    let mut lcfg = simplelog::ConfigBuilder::new();
448    lcfg.set_time_offset_to_local()
449        .expect("Failed to set local time offset")
450        .set_time_level(simplelog::LevelFilter::Error)
451        .set_location_level(simplelog::LevelFilter::Off)
452        .set_target_level(simplelog::LevelFilter::Off)
453        .set_thread_level(simplelog::LevelFilter::Off);
454    simplelog::TermLogger::init(
455        loglevel,
456        lcfg.build(),
457        simplelog::TerminalMode::Stderr,
458        simplelog::ColorChoice::Auto,
459    )?;
460
461    let shutdown = Arc::new(AtomicBool::new(false));
462    let shutdown_clone = shutdown.clone();
463    ctrlc::set_handler(move || {
464        shutdown_clone.store(true, Ordering::Relaxed);
465    })
466    .context("Error setting Ctrl-C handler")?;
467
468    if let Some(intv) = opts.monitor.or(opts.stats) {
469        let shutdown_copy = shutdown.clone();
470        let jh = std::thread::spawn(move || {
471            match stats::monitor(Duration::from_secs_f64(intv), shutdown_copy) {
472                Ok(_) => {
473                    debug!("stats monitor thread finished successfully")
474                }
475                Err(error_object) => {
476                    warn!(
477                        "stats monitor thread finished because of an error {}",
478                        error_object
479                    )
480                }
481            }
482        });
483        if opts.monitor.is_some() {
484            let _ = jh.join();
485            return Ok(());
486        }
487    }
488
489    let mut open_object = MaybeUninit::uninit();
490    loop {
491        let mut sched = Scheduler::init(&opts, &mut open_object)?;
492        if !sched.run(shutdown.clone())?.should_restart() {
493            break;
494        }
495    }
496
497    Ok(())
498}