Skip to main content

scx_bpfland/
main.rs

1// SPDX-License-Identifier: GPL-2.0
2//
3// Copyright (c) 2024 Andrea Righi <andrea.righi@linux.dev>
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::BTreeMap;
15use std::mem::MaybeUninit;
16use std::sync::Arc;
17use std::sync::atomic::AtomicBool;
18use std::sync::atomic::Ordering;
19use std::time::Duration;
20
21use anyhow::Context;
22use anyhow::Result;
23use anyhow::bail;
24use clap::Parser;
25use crossbeam::channel::RecvTimeoutError;
26use libbpf_rs::MapCore;
27use libbpf_rs::OpenObject;
28use libbpf_rs::ProgramInput;
29use libbpf_rs::skel::Skel;
30use log::{debug, info, warn};
31use scx_arena::ArenaLib;
32use scx_stats::prelude::*;
33use scx_utils::CoreType;
34use scx_utils::Cpumask;
35use scx_utils::NR_CPU_IDS;
36use scx_utils::Topology;
37use scx_utils::UserExitInfo;
38use scx_utils::autopower::{PowerProfile, fetch_power_profile};
39use scx_utils::build_id;
40use scx_utils::compat;
41use scx_utils::libbpf_clap_opts::LibbpfOpts;
42use scx_utils::scx_ops_attach;
43use scx_utils::scx_ops_load;
44use scx_utils::scx_ops_open;
45use scx_utils::try_set_rlimit_infinity;
46use scx_utils::uei_exited;
47use scx_utils::uei_report;
48use stats::Metrics;
49
50const SCHEDULER_NAME: &str = "scx_bpfland";
51
52/// The most CPUs the BPF side handles, MAX_CPUS in src/bpf/intf.h.
53const MAX_CPUS: usize = 1024;
54
55/// The bits of the queued mask of the BPF side, QUEUED_WORDS in src/bpf/intf.h.
56const QUEUED_BITS: usize = 2048 * 64;
57
58/// The bits of a cache line.
59const LINE_BITS: usize = 512;
60
61/// The most CPU tiers the BPF side tells apart, MAX_TIERS in src/bpf/main.bpf.c.
62const MAX_TIERS: u32 = 8;
63
64/// How much less capacity a CPU may have than the largest of its tier, in percent. Cores of one
65/// type can differ by a few percent, and a move between two of them would gain nothing.
66const CAPACITY_TOLERANCE_PCT: usize = 5;
67
68/// scx_bpfland: an EEVDF-style scheduler with one queue per CPU, virtual-time borrowing and
69/// requests sized by the CPU burst.
70///
71/// Each CPU runs the eligible task with the earliest virtual deadline from its own queue. A task's
72/// deadline is its virtual time plus how long it runs between two sleeps, so tasks that run in
73/// short bursts take the CPU from those that compute without a pause. A waking task also borrows
74/// virtual time, which keeps interactive workloads responsive beside CPU-intensive ones.
75///
76/// The BPF part makes all the scheduling decisions (see src/bpf/main.bpf.c).
77#[derive(Debug, Parser)]
78struct Opts {
79    /// Exit debug dump buffer length. 0 indicates default.
80    #[clap(long, default_value = "0")]
81    exit_dump_len: u32,
82
83    /// Scheduling slice duration in microseconds.
84    #[clap(short = 's', long, default_value = "1600")]
85    slice_us: u64,
86
87    /// The CPUs to place tasks on: a preference among all of them, or a domain they are kept in.
88    ///
89    /// A preference orders the CPUs that placement tries first, and tasks still use every CPU
90    /// when those are busy:
91    ///  - "performance" = the fastest CPUs first, the default where the CPUs rank
92    ///  - "powersave" = the slowest CPUs first
93    ///  - "turbo" = the CPUs that boost highest first, then the fastest
94    ///  - "auto" = "powersave" under the power-saver profile, otherwise "performance"
95    ///  - "cores" = one thread of each physical core first, then its SMT siblings
96    ///
97    /// A CPU list is a domain: it keeps tasks on its CPUs, queued there rather than spilled onto
98    /// other CPUs when those are busy. Tasks whose affinity excludes the domain, and
99    /// migration-disabled tasks, may run outside it. "all" uses every online CPU, which is the
100    /// default.
101    #[clap(short = 'm', long, value_name = "DOMAIN")]
102    primary_domain: Option<String>,
103
104    /// Do not place tasks on the CPUs they are better off on, where the CPUs rank: by the
105    /// kernel's packing priorities, or by their capacities on a machine with big and little cores.
106    #[clap(long)]
107    no_asym_packing: bool,
108
109    /// Do not let a waking task with a shorter request and an earlier deadline take the CPU from
110    /// the running task.
111    #[clap(long)]
112    no_preempt: bool,
113
114    /// Pick the earliest deadline whatever the virtual time, instead of among the eligible tasks,
115    /// the ones not ahead of the clock.
116    #[clap(long)]
117    no_eligibility: bool,
118
119    /// Do not keep a task's lag across a sleep, as EEVDF does: sleeping then earns service, up to
120    /// the lag bound.
121    #[clap(long)]
122    no_vlag: bool,
123
124    /// Maximum lag of a task when it is queued, behind the clock or ahead of it, in microseconds
125    /// of service at nice 0.
126    #[clap(short = 'l', long, default_value = "10000")]
127    lag_bound_us: u64,
128
129    /// Minimum time a task runs before a waking task can take the CPU from it, in microseconds.
130    #[clap(short = 'g', long, default_value = "100")]
131    min_granularity_us: u64,
132
133    /// Do not let a waking task borrow virtual time. By default it is queued at least
134    /// --vtime-borrowing-us behind the clock, ahead of the tasks that stayed runnable, and on a
135    /// CPU where it preempts the one that runs.
136    #[clap(long)]
137    no_vtime_borrowing: bool,
138
139    /// How much a waking task borrows, in microseconds of service at nice 0.
140    #[clap(long, default_value = "20000")]
141    vtime_borrowing_us: u64,
142
143    /// Maximum time (us) that a task can run on the CPUs of a worse tier before it is handed to
144    /// a CPU of the best one, where the CPUs rank.
145    ///
146    /// A task that never sleeps keeps its CPU slice after slice. On a machine with big and
147    /// little cores, CPU-bound tasks then stay where they started, and the ones on the little
148    /// cores fall behind. Past this threshold, a task whose slice ends on a worse CPU swaps
149    /// places with one that has run on CPUs of the best tier of its LLC for as long without
150    /// sleeping, so that CPU-bound tasks take turns on the best CPUs.
151    ///
152    /// 0 = rotation disabled.
153    #[clap(long, default_value = "0")]
154    rotate_us: u64,
155
156    /// Enable stats monitoring with the specified interval.
157    #[clap(long)]
158    stats: Option<f64>,
159
160    /// Run in stats monitoring mode with the specified interval. Scheduler is not launched.
161    #[clap(long)]
162    monitor: Option<f64>,
163
164    /// Enable verbose output, including libbpf details.
165    #[clap(short = 'v', long, action = clap::ArgAction::SetTrue)]
166    verbose: bool,
167
168    /// Print scheduler version and exit.
169    #[clap(short = 'V', long, action = clap::ArgAction::SetTrue)]
170    version: bool,
171
172    /// Show descriptions for statistics.
173    #[clap(long)]
174    help_stats: bool,
175
176    #[clap(flatten, next_help_heading = "Libbpf Options")]
177    pub libbpf: LibbpfOpts,
178}
179
180/// The order in which placement tries the CPUs, see --primary-domain.
181#[derive(Clone, Copy, Debug, PartialEq)]
182enum Preference {
183    Performance,
184    Powersave,
185    Turbo,
186    Cores,
187}
188
189/// The preference --primary-domain names, if it names one, and whether it follows the power
190/// profile.
191fn preference(spec: &str, profile: PowerProfile) -> Option<(Preference, bool)> {
192    match spec {
193        "performance" => Some((Preference::Performance, false)),
194        "powersave" => Some((Preference::Powersave, false)),
195        "turbo" => Some((Preference::Turbo, false)),
196        "cores" => Some((Preference::Cores, false)),
197        "auto" => Some((
198            if profile == PowerProfile::Powersave {
199                Preference::Powersave
200            } else {
201                Preference::Performance
202            },
203            true,
204        )),
205        _ => None,
206    }
207}
208
209/// The active power profile, from power-profiles-daemon where it runs.
210fn power_profile() -> PowerProfile {
211    let profile = fetch_power_profile(true);
212    if profile == PowerProfile::Unknown {
213        fetch_power_profile(false)
214    } else {
215        profile
216    }
217}
218
219fn primary_domain(spec: &str, topo: &Topology) -> Result<Cpumask> {
220    let mut mask = Cpumask::new();
221    if spec == "all" || preference(spec, PowerProfile::Unknown).is_some() {
222        for &cpu in topo.all_cpus.keys() {
223            mask.set_cpu(cpu)?;
224        }
225    } else {
226        for part in spec.split(',') {
227            let part = part.trim();
228            let (first, last) = match part.split_once('-') {
229                Some((first, last)) => (first.trim().parse::<usize>()?, last.trim().parse()?),
230                None => {
231                    let cpu = part.parse::<usize>()?;
232                    (cpu, cpu)
233                }
234            };
235            if first > last || last >= MAX_CPUS {
236                bail!("invalid primary CPU range: {part}");
237            }
238            for cpu in first..=last {
239                if !topo.all_cpus.contains_key(&cpu) {
240                    bail!("primary CPU {cpu} is not online");
241                }
242                mask.set_cpu(cpu)?;
243            }
244        }
245    }
246    if mask.iter().next().is_none() {
247        bail!("the primary domain must contain an online CPU");
248    }
249    Ok(mask)
250}
251
252/// The CPUs of each domain, listed one domain after the other: what the BPF side iterates a
253/// domain with, from its base for its length.
254struct Domains {
255    of_cpu: Vec<i32>,
256    base: Vec<u32>,
257    len: Vec<u32>,
258    cpus: Vec<u16>,
259}
260
261impl Domains {
262    /// Group the CPUs by `key`, numbering the domains densely in key order.
263    fn new(topo: &Topology, key: impl Fn(&scx_utils::Cpu) -> usize) -> Self {
264        let mut groups: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
265        for cpu in topo.all_cpus.values() {
266            groups.entry(key(cpu)).or_default().push(cpu.id);
267        }
268        let mut domains = Domains {
269            of_cpu: vec![0; MAX_CPUS],
270            base: vec![0; MAX_CPUS],
271            len: vec![0; MAX_CPUS],
272            cpus: Vec::with_capacity(MAX_CPUS),
273        };
274        for (dom, cpus) in groups.values().enumerate() {
275            domains.base[dom] = domains.cpus.len() as u32;
276            domains.len[dom] = cpus.len() as u32;
277            for &cpu in cpus {
278                domains.of_cpu[cpu] = dom as i32;
279                domains.cpus.push(cpu as u16);
280            }
281        }
282        domains.cpus.resize(MAX_CPUS, 0);
283        domains
284    }
285
286    /// The bit of each CPU in the queued mask of the BPF side: the bits of each domain from a
287    /// cache line of their own, or the CPU's number where the mask has too few lines for that.
288    fn queued_bits(&self) -> Vec<u32> {
289        let mut bits: Vec<u32> = (0..MAX_CPUS as u32).collect();
290        let lens: Vec<usize> = self
291            .len
292            .iter()
293            .take_while(|&&len| len > 0)
294            .map(|&len| len as usize)
295            .collect();
296        let lines: usize = lens.iter().map(|len| len.div_ceil(LINE_BITS)).sum();
297        if lines * LINE_BITS > QUEUED_BITS {
298            return bits;
299        }
300        let mut line = 0;
301        for (dom, &len) in lens.iter().enumerate() {
302            let base = self.base[dom] as usize;
303            for pos in 0..len {
304                bits[self.cpus[base + pos] as usize] = (line * LINE_BITS + pos) as u32;
305            }
306            line += len.div_ceil(LINE_BITS);
307        }
308        bits
309    }
310}
311
312struct Scheduler<'a> {
313    skel: BpfSkel<'a>,
314    struct_ops: Option<libbpf_rs::Link>,
315    stats_server: StatsServer<(), Metrics>,
316    _arenalib: ArenaLib,
317    /// The power profile "auto" was resolved by, to restart when it changes.
318    auto_profile: Option<PowerProfile>,
319    user_restart: bool,
320}
321
322impl<'a> Scheduler<'a> {
323    fn init(opts: &'a Opts, open_object: &'a mut MaybeUninit<OpenObject>) -> Result<Self> {
324        try_set_rlimit_infinity();
325
326        let topo = Topology::new().context("reading the CPU topology")?;
327        let nr_cpus = *NR_CPU_IDS;
328        if nr_cpus > MAX_CPUS {
329            bail!("{} CPUs, at most {} are supported", nr_cpus, MAX_CPUS);
330        }
331
332        let spec = opts.primary_domain.as_deref().unwrap_or("all");
333        let primary = primary_domain(spec, &topo).context("parsing the primary domain")?;
334        let profile = if spec == "auto" {
335            power_profile()
336        } else {
337            PowerProfile::Unknown
338        };
339        let (pref, auto) = preference(spec, profile).unwrap_or((Preference::Performance, false));
340
341        info!(
342            "{} {} {}",
343            SCHEDULER_NAME,
344            build_id::full_version(env!("CARGO_PKG_VERSION")),
345            if topo.smt_enabled {
346                "SMT on"
347            } else {
348                "SMT off"
349            }
350        );
351        info!(
352            "scheduler options: {}",
353            std::env::args().collect::<Vec<_>>().join(" ")
354        );
355
356        let mut skel_builder = BpfSkelBuilder::default();
357        skel_builder.obj_builder.debug(opts.verbose);
358        let open_opts = opts.libbpf.clone().into_bpf_open_opts();
359        let mut skel = scx_ops_open!(skel_builder, open_object, bpfland_ops, open_opts)?;
360
361        skel.struct_ops.bpfland_ops_mut().exit_dump_len = opts.exit_dump_len;
362
363        let rodata = skel.maps.rodata_data.as_mut().unwrap();
364        rodata.slice_ns = opts.slice_us * 1000;
365        rodata.lag_bound_ns = opts.lag_bound_us * 1000;
366        rodata.min_granularity_ns = opts.min_granularity_us * 1000;
367        rodata.vtime_borrowing_ns = opts.vtime_borrowing_us * 1000;
368        rodata.rotate_ns = opts.rotate_us * 1000;
369        rodata.no_asym_packing = opts.no_asym_packing;
370        rodata.no_preempt = opts.no_preempt;
371        rodata.no_eligibility = opts.no_eligibility;
372        rodata.no_vlag = opts.no_vlag;
373        rodata.no_vtime_borrowing = opts.no_vtime_borrowing;
374        rodata.stats_enabled = opts.stats.is_some();
375        rodata.smt_enabled = topo.smt_enabled;
376        rodata.nr_cpus = nr_cpus as u32;
377        rodata.primary_all = primary.iter().count() == topo.all_cpus.len();
378        for cpu in primary.iter() {
379            rodata.primary_cpus[cpu] = true;
380        }
381        if !rodata.primary_all {
382            info!("Primary CPUs: {} (no spillover)", primary.to_cpulist());
383        }
384
385        // The topology: the LLC, node and core of each CPU, and the CPUs of each.
386        let llcs = Domains::new(&topo, |cpu| cpu.llc_id);
387        let nodes = Domains::new(&topo, |cpu| cpu.node_id);
388        let cores = Domains::new(&topo, |cpu| cpu.core_id);
389        let queued_bits = llcs.queued_bits();
390        for i in 0..MAX_CPUS {
391            rodata.cpu_llc[i] = llcs.of_cpu[i];
392            rodata.cpu_node[i] = nodes.of_cpu[i];
393            rodata.cpu_core[i] = cores.of_cpu[i];
394            rodata.llc_base[i] = llcs.base[i];
395            rodata.llc_len[i] = llcs.len[i];
396            rodata.node_base[i] = nodes.base[i];
397            rodata.node_len[i] = nodes.len[i];
398            rodata.core_base[i] = cores.base[i];
399            rodata.core_len[i] = cores.len[i];
400            rodata.llc_cpus[i] = llcs.cpus[i];
401            rodata.node_cpus[i] = nodes.cpus[i];
402            rodata.core_cpus[i] = cores.cpus[i];
403            rodata.cpu_queued_bit[i] = queued_bits[i];
404        }
405
406        skel.struct_ops.bpfland_ops_mut().flags = *compat::SCX_OPS_ENQ_LAST
407            | *compat::SCX_OPS_ENQ_MIGRATION_DISABLED
408            | *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP
409            | *compat::SCX_OPS_KEEP_BUILTIN_IDLE
410            | *compat::SCX_OPS_TID_TO_TASK;
411        // Find queued tasks by their scx tid where the kernel can, which reads as zero on a
412        // kernel without it.
413        skel.maps.rodata_data.as_mut().unwrap().tid_lookup = *compat::SCX_OPS_TID_TO_TASK != 0;
414        // PID lookup can lose unhashed exiting tasks. Let the kernel dispatch them directly
415        // unless TID lookup can find them reliably.
416        if *compat::SCX_OPS_TID_TO_TASK != 0 {
417            skel.struct_ops.bpfland_ops_mut().flags |= *compat::SCX_OPS_ENQ_EXITING;
418        }
419        info!(
420            "scheduler flags: {:#x}",
421            skel.struct_ops.bpfland_ops_mut().flags
422        );
423
424        let mut skel = scx_ops_load!(skel, bpfland_ops, uei)?;
425
426        Self::init_place_tiers(&mut skel, &topo, &llcs, pref)?;
427
428        // Watch the BPF streams: an arena fault is reported and fatal rather than silently fixed
429        // up.
430        let arenalib =
431            ArenaLib::start(skel.object_mut()).context("starting arena userspace services")?;
432
433        let struct_ops = Some(scx_ops_attach!(skel, bpfland_ops)?);
434        let stats_server = StatsServer::new(stats::server_data()).launch()?;
435
436        info!("{} attached", SCHEDULER_NAME);
437
438        Ok(Self {
439            skel,
440            struct_ops,
441            stats_server,
442            _arenalib: arenalib,
443            auto_profile: auto.then_some(profile),
444            user_restart: false,
445        })
446    }
447
448    /// Rank the CPUs, after load and before attach: tier 0 is the one placement tries first. The
449    /// kernel's packing priorities rank them where every CPU packs asymmetrically,
450    /// SD_ASYM_PACKING, and the priorities differ. Otherwise their capacities rank them where
451    /// those differ. @pref then orders the tiers, see --primary-domain.
452    fn init_place_tiers(
453        skel: &mut BpfSkel<'_>,
454        topo: &Topology,
455        llcs: &Domains,
456        pref: Preference,
457    ) -> Result<()> {
458        let (mut tiers, source) = match Self::packing_tiers(skel, topo)? {
459            Some(tiers) => (tiers, "packing priority"),
460            None => match Self::capacity_tiers(topo) {
461                Some(tiers) => (tiers, "capacity"),
462                None => (topo.all_cpus.keys().map(|&cpu| (cpu, 0)).collect(), ""),
463            },
464        };
465        let mut nr_tiers = tiers.iter().map(|&(_, tier)| tier + 1).max().unwrap_or(1);
466        match pref {
467            Preference::Performance => {}
468            // The slowest first.
469            Preference::Powersave => {
470                for (_, tier) in tiers.iter_mut() {
471                    *tier = nr_tiers - 1 - *tier;
472                }
473            }
474            // The CPUs that boost highest ahead of the tier they are in.
475            Preference::Turbo if nr_tiers < MAX_TIERS => {
476                let turbo = |cpu: usize| {
477                    matches!(
478                        topo.all_cpus.get(&cpu).map(|cpu| &cpu.core_type),
479                        Some(CoreType::Big { turbo: true })
480                    )
481                };
482                if tiers.iter().any(|&(cpu, _)| turbo(cpu)) {
483                    for (cpu, tier) in tiers.iter_mut() {
484                        if !turbo(*cpu) {
485                            *tier += 1;
486                        }
487                    }
488                }
489            }
490            Preference::Turbo => {}
491            // Within each tier, the first thread of every core ahead of its siblings.
492            Preference::Cores => {
493                let first = |cpu: usize| {
494                    topo.all_cores.values().any(|core| {
495                        core.cpus.keys().find(|cpu| topo.all_cpus.contains_key(cpu)) == Some(&cpu)
496                    })
497                };
498                for (cpu, tier) in tiers.iter_mut() {
499                    *tier = (*tier * 2 + u32::from(!first(*cpu))).min(MAX_TIERS - 1);
500                }
501            }
502        }
503        // Number the tiers densely: one that the order left empty is dropped.
504        let mut levels: Vec<u32> = tiers.iter().map(|&(_, tier)| tier).collect();
505        levels.sort_unstable();
506        levels.dedup();
507        for (_, tier) in tiers.iter_mut() {
508            *tier = levels.iter().position(|&level| level == *tier).unwrap_or(0) as u32;
509        }
510        nr_tiers = levels.len().max(1) as u32;
511
512        let bss = skel.maps.bss_data.as_mut().unwrap();
513        bss.cpus_ranked = nr_tiers > 1;
514        bss.nr_place_tiers = nr_tiers;
515        for llc in 0..MAX_CPUS {
516            bss.llc_place_tier[llc] = u32::MAX;
517        }
518        for &(cpu, tier) in &tiers {
519            bss.place_tier[cpu] = tier;
520            let llc = llcs.of_cpu[cpu] as usize;
521            bss.llc_place_tier[llc] = bss.llc_place_tier[llc].min(tier);
522        }
523        if nr_tiers > 1 {
524            info!(
525                "CPU ranking: by {}, {} tiers, {} order",
526                source,
527                nr_tiers,
528                format!("{pref:?}").to_lowercase()
529            );
530        } else {
531            info!("CPU ranking: off");
532        }
533
534        Ok(())
535    }
536
537    /// Each CPU's tier by the kernel's packing priority, highest first, or None if the kernel
538    /// does not rank the CPUs.
539    fn packing_tiers(skel: &mut BpfSkel<'_>, topo: &Topology) -> Result<Option<Vec<(usize, u32)>>> {
540        let mut priorities = Vec::new();
541        let mut all_pack = true;
542        for cpu in topo.all_cpus.values() {
543            let mut args = cpu_priority_args {
544                cpu: cpu.id as u64,
545                priority: 0,
546                asym_packing: 0,
547            };
548            let input = ProgramInput {
549                context_in: Some(unsafe {
550                    std::slice::from_raw_parts_mut(
551                        &mut args as *mut _ as *mut u8,
552                        std::mem::size_of_val(&args),
553                    )
554                }),
555                ..Default::default()
556            };
557            let out = skel
558                .progs
559                .bpfland_cpu_priority
560                .test_run(input)
561                .context("running bpfland_cpu_priority")?;
562            if out.return_value != 0 {
563                bail!("bpfland_cpu_priority failed: {}", out.return_value);
564            }
565            all_pack &= args.asym_packing != 0;
566            priorities.push((cpu.id, args.priority));
567        }
568        let distinct = priorities.windows(2).any(|pair| pair[0].1 != pair[1].1);
569        if !all_pack || !distinct {
570            return Ok(None);
571        }
572
573        // The distinct priorities, highest first: a CPU's tier is the index of its own.
574        let mut levels: Vec<i64> = priorities.iter().map(|&(_, prio)| prio).collect();
575        levels.sort_unstable_by(|a, b| b.cmp(a));
576        levels.dedup();
577        Ok(Some(
578            priorities
579                .iter()
580                .map(|&(cpu, prio)| {
581                    let tier = levels.iter().position(|&level| level == prio).unwrap_or(0);
582                    (cpu, (tier as u32).min(MAX_TIERS - 1))
583                })
584                .collect(),
585        ))
586    }
587
588    /// Each CPU's tier by the capacity the kernel gives it, largest first, or None if the
589    /// capacities are not known or do not differ. A new tier begins at a capacity more than
590    /// CAPACITY_TOLERANCE_PCT below the largest of the tier before.
591    fn capacity_tiers(topo: &Topology) -> Option<Vec<(usize, u32)>> {
592        let mut capacities: Vec<(usize, usize)> = topo
593            .all_cpus
594            .values()
595            .map(|cpu| Some((cpu.id, cpu.kernel_cpu_capacity?)))
596            .collect::<Option<_>>()?;
597        capacities.sort_by_key(|&(cpu, capacity)| (std::cmp::Reverse(capacity), cpu));
598        let mut anchor = capacities.first()?.1;
599        let mut tier = 0;
600        let tiers: Vec<(usize, u32)> = capacities
601            .iter()
602            .map(|&(cpu, capacity)| {
603                if capacity * 100 < anchor * (100 - CAPACITY_TOLERANCE_PCT) {
604                    tier = (tier + 1).min(MAX_TIERS - 1);
605                    anchor = capacity;
606                }
607                (cpu, tier)
608            })
609            .collect();
610        (tier > 0).then_some(tiers)
611    }
612
613    fn get_metrics(&self) -> Metrics {
614        let mut metrics = Metrics::default();
615        let key = 0_u32.to_ne_bytes();
616        if let Ok(Some(values)) = self
617            .skel
618            .maps
619            .counters_stor
620            .lookup_percpu(&key, libbpf_rs::MapFlags::ANY)
621        {
622            for value in values {
623                if value.len() < std::mem::size_of::<counters>() {
624                    continue;
625                }
626                let c: counters =
627                    unsafe { std::ptr::read_unaligned(value.as_ptr() as *const counters) };
628                metrics.nr_direct += c.direct;
629                metrics.nr_borrowed += c.borrowed;
630                metrics.nr_moved += c.moved;
631                metrics.nr_missed += c.missed;
632                metrics.nr_preempts += c.preempts;
633                metrics.nr_steals += c.steals;
634                metrics.nr_balanced += c.balanced;
635                metrics.nr_rotated += c.rotated;
636            }
637        }
638        metrics
639    }
640
641    pub fn exited(&mut self) -> bool {
642        uei_exited!(&self.skel, uei)
643    }
644
645    fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
646        let (res_ch, req_ch) = self.stats_server.channels();
647        while !shutdown.load(Ordering::Relaxed) && !self.exited() {
648            // "auto" follows the power profile: a new one ranks the CPUs again.
649            if self
650                .auto_profile
651                .is_some_and(|profile| power_profile() != profile)
652            {
653                info!("power profile changed, restarting");
654                self.user_restart = true;
655                break;
656            }
657            match req_ch.recv_timeout(Duration::from_secs(1)) {
658                Ok(()) => res_ch.send(self.get_metrics())?,
659                Err(RecvTimeoutError::Timeout) => {}
660                Err(e) => Err(e)?,
661            }
662        }
663
664        let _ = self.struct_ops.take();
665        uei_report!(&self.skel, uei)
666    }
667}
668
669impl Drop for Scheduler<'_> {
670    fn drop(&mut self) {
671        info!("Unregister {SCHEDULER_NAME} scheduler");
672    }
673}
674
675fn main() -> Result<()> {
676    let opts = Opts::parse();
677
678    if opts.version {
679        println!(
680            "{} {}",
681            SCHEDULER_NAME,
682            build_id::full_version(env!("CARGO_PKG_VERSION"))
683        );
684        return Ok(());
685    }
686
687    if opts.help_stats {
688        stats::server_data().describe_meta(&mut std::io::stdout(), None)?;
689        return Ok(());
690    }
691
692    let release =
693        std::fs::read_to_string("/proc/sys/kernel/osrelease").context("reading kernel release")?;
694    let mut fields = release.trim().split(['.', '-']);
695    let major: u32 = fields
696        .next()
697        .context("missing kernel major version")?
698        .parse()?;
699    let minor: u32 = fields
700        .next()
701        .context("missing kernel minor version")?
702        .parse()?;
703    if (major, minor) < (6, 16) {
704        bail!(
705            "scx_bpfland requires Linux 6.16 or newer (running {})",
706            release.trim()
707        );
708    }
709
710    let loglevel = simplelog::LevelFilter::Info;
711
712    let mut lcfg = simplelog::ConfigBuilder::new();
713    lcfg.set_time_offset_to_local()
714        .expect("Failed to set local time offset")
715        .set_time_level(simplelog::LevelFilter::Error)
716        .set_location_level(simplelog::LevelFilter::Off)
717        .set_target_level(simplelog::LevelFilter::Off)
718        .set_thread_level(simplelog::LevelFilter::Off);
719    simplelog::TermLogger::init(
720        loglevel,
721        lcfg.build(),
722        simplelog::TerminalMode::Stderr,
723        simplelog::ColorChoice::Auto,
724    )?;
725
726    let shutdown = Arc::new(AtomicBool::new(false));
727    let shutdown_clone = shutdown.clone();
728    ctrlc::set_handler(move || {
729        shutdown_clone.store(true, Ordering::Relaxed);
730    })
731    .context("Error setting Ctrl-C handler")?;
732
733    if let Some(intv) = opts.monitor.or(opts.stats) {
734        let shutdown_copy = shutdown.clone();
735        let jh = std::thread::spawn(move || {
736            match stats::monitor(Duration::from_secs_f64(intv), shutdown_copy) {
737                Ok(_) => {
738                    debug!("stats monitor thread finished successfully")
739                }
740                Err(error_object) => {
741                    warn!(
742                        "stats monitor thread finished because of an error {}",
743                        error_object
744                    )
745                }
746            }
747        });
748        if opts.monitor.is_some() {
749            let _ = jh.join();
750            return Ok(());
751        }
752    }
753
754    let mut open_object = MaybeUninit::uninit();
755    loop {
756        let mut sched = Scheduler::init(&opts, &mut open_object)?;
757        if !sched.run(shutdown.clone())?.should_restart() {
758            if sched.user_restart {
759                continue;
760            }
761            break;
762        }
763    }
764
765    Ok(())
766}