Skip to main content

scx_rustsched_loader/
lib.rs

1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES.
3
4//! Loading and running a Rust BPF sched_ext scheduler: the object, the
5//! struct_ops map the kernel can take, the configuration table's kernel
6//! entries resolved from BTF, the map sizes set before the load, the
7//! attach, the exit record, the arena statistics, the per-callback
8//! profiler, the sampler of the per-CID queues, and the CPU capacity and
9//! packing tiers the runtime ranks CIDs by. Nothing here is particular to
10//! a scheduling policy.
11
12use std::{
13    collections::BTreeMap,
14    ffi::{CString, OsStr},
15    os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd},
16    ptr::NonNull,
17    sync::{
18        Arc,
19        atomic::{AtomicBool, Ordering},
20    },
21    time::{Duration, Instant},
22};
23
24pub use anyhow::Result;
25use anyhow::{Context, bail, ensure};
26use libbpf_rs::{
27    Btf, Link, MapCore, MapFlags, ObjectBuilder, OpenObject, ProgramType,
28    btf::{
29        BtfType, HasSize, ReferencesType,
30        types::{
31            Composite, Enum, Enum64, Func, FuncProto, Int, IntEncoding, MemberAttr, Ptr, Struct,
32        },
33    },
34};
35use log::{info, warn};
36pub use scx_rustsched_abi as config;
37pub use scx_rustsched_macros::Opts;
38/// The kernel's sched_ext flags, 0 where it lacks one, for a scheduler's
39/// user space to choose the ops flags it loads with, see `Params::add_flags()`.
40pub use scx_utils::compat;
41use scx_utils::{UserExitInfo, libbpf_clap_opts::LibbpfOpts};
42
43/// The eligible deadline queue, from the source the BPF program is built
44/// from: the loader reads the queues in the arena through the same code.
45#[path = "../../scx_rustsched/src/edq.rs"]
46pub mod edq;
47mod kfunc_protos;
48mod queues;
49pub mod tiers;
50
51pub use libbpf_rs::Object;
52pub use queues::{ArenaView, CidTableReader, QueueSampler, QueueStats};
53pub use tiers::Tiers;
54
55/// The attached scheduler's object, shared. What reads the arena keeps a
56/// clone, and with it the arena's mapping, which goes with the object.
57#[derive(Clone)]
58pub struct Loaded(Arc<Object>);
59
60impl std::ops::Deref for Loaded {
61    type Target = Object;
62
63    fn deref(&self) -> &Object {
64        &self.0
65    }
66}
67
68/// One struct_ops program's run count, run time and recursion misses, as
69/// the kernel accounts them while the profiler is enabled.
70#[derive(Clone, Copy, Debug, Default)]
71pub struct OpStats {
72    pub calls: u64,
73    pub ns: u64,
74    pub misses: u64,
75}
76
77/// The struct_ops map of the BPF object whose cgroup callbacks have the
78/// names the running kernel gives them: cpuctl_*, or cgroup_* on a kernel
79/// from before the cid form renamed them. None for a kernel built without
80/// group scheduling, which has neither.
81pub fn cgroup_ops() -> Result<Option<&'static str>> {
82    let btf = Btf::from_vmlinux().context("read running kernel BTF")?;
83    let ops = btf
84        .type_by_name::<Struct<'_>>("sched_ext_ops_cid")
85        .context("find sched_ext_ops_cid")?;
86    let has = |name: &str| {
87        Composite::from(ops)
88            .iter()
89            .any(|member| member.name == Some(OsStr::new(name)))
90    };
91    Ok(if has("cpuctl_init") {
92        Some("rust_ops")
93    } else if has("cgroup_init") {
94        Some("rust_ops_cgroup")
95    } else {
96        None
97    })
98}
99
100/// The byte range of the member `name` of the BPF object's struct_ops type
101/// `ops`, read off its BTF.
102pub fn ops_member(
103    object_name: &str,
104    object: &[u8],
105    ops: &str,
106    name: &str,
107) -> Result<std::ops::Range<usize>> {
108    let btf = Btf::from_raw(object_name, object)?.context("read BPF object BTF")?;
109    let local = btf
110        .type_by_name::<Struct<'_>>(ops)
111        .with_context(|| format!("find the BPF object's {ops}"))?;
112    let (field, offset) = member(&btf, local, name)?;
113    let size = Int::try_from(field)
114        .map_err(|_| anyhow::anyhow!("{ops}.{name} is not an integer"))?
115        .size();
116    let start = offset as usize;
117    Ok(start..start + size)
118}
119
120/// The byte ranges, in the BPF object's struct_ops type `ops`, of the
121/// cgroup callbacks the running kernel's sched_ext_ops_cid lacks, read off
122/// both BTFs: all of them on a kernel built without group scheduling, the
123/// newer ones on an older kernel. libbpf skips a member the kernel lacks only
124/// when it is zero.
125pub fn missing_callbacks(
126    object_name: &str,
127    object: &[u8],
128    ops: &str,
129) -> Result<Vec<std::ops::Range<usize>>> {
130    let vmlinux = Btf::from_vmlinux().context("read running kernel BTF")?;
131    let kernel = vmlinux
132        .type_by_name::<Struct<'_>>("sched_ext_ops_cid")
133        .context("find sched_ext_ops_cid")?;
134    let kernel = Composite::from(kernel);
135    let kernel: Vec<_> = kernel.iter().filter_map(|member| member.name).collect();
136    let btf = Btf::from_raw(object_name, object)?.context("read BPF object BTF")?;
137    let local = btf
138        .type_by_name::<Struct<'_>>(ops)
139        .with_context(|| format!("find the BPF object's {ops}"))?;
140    Composite::from(local)
141        .iter()
142        .filter(|member| {
143            member.name.is_some_and(|name| {
144                let bytes = name.as_encoded_bytes();
145                (bytes.starts_with(b"cpuctl_") || bytes.starts_with(b"cgroup_"))
146                    && !kernel.contains(&name)
147            })
148        })
149        .map(|member| match member.attr {
150            MemberAttr::Normal { offset } if offset % 8 == 0 => {
151                let start = offset as usize / 8;
152                Ok(start..start + 8)
153            }
154            _ => bail!("unexpected cgroup callback member"),
155        })
156        .collect()
157}
158
159/// The value of the enumerator `name` in kernel BTF, if the kernel has it.
160pub fn enumerator(btf: &Btf<'_>, name: &str) -> Option<u64> {
161    btf.type_by_kind::<Enum<'_>>()
162        .find_map(|kind| {
163            kind.iter()
164                .find(|value| value.name == Some(OsStr::new(name)))
165                .map(|value| value.value as u64)
166        })
167        .or_else(|| {
168            btf.type_by_kind::<Enum64<'_>>().find_map(|kind| {
169                kind.iter()
170                    .find(|value| value.name == Some(OsStr::new(name)))
171                    .map(|value| value.value as u64)
172            })
173        })
174}
175
176/// Does the running kernel's scx_bpf_cid_topo() take the structure's
177/// size? Such a kernel copies only the fields it has and leaves the rest
178/// at -1, so its structure may still be the legacy one, without the
179/// cluster fields.
180pub fn sized_topology() -> Result<bool> {
181    let btf = Btf::from_vmlinux().context("read running kernel BTF")?;
182    let function = btf
183        .type_by_name::<Func<'_>>("scx_bpf_cid_topo")
184        .context("find scx_bpf_cid_topo")?;
185    let prototype = FuncProto::try_from(function.referenced_type())
186        .map_err(|_| anyhow::anyhow!("scx_bpf_cid_topo has no function prototype"))?;
187    let topology = btf
188        .type_by_name::<Struct<'_>>("scx_cid_topo")
189        .context("find scx_cid_topo")?;
190    match prototype.len() {
191        2 => {
192            ensure!(
193                matches!(topology.size(), 24 | 32),
194                "unexpected legacy CID topology size"
195            );
196            Ok(false)
197        }
198        3 => {
199            ensure!(
200                matches!(topology.size(), 24 | 32 | 40),
201                "unexpected sized CID topology size"
202            );
203            Ok(true)
204        }
205        count => bail!("unsupported scx_bpf_cid_topo argument count: {count}"),
206    }
207}
208
209pub fn member<'btf>(
210    btf: &'btf Btf<'_>,
211    structure: Struct<'btf>,
212    name: &str,
213) -> Result<(BtfType<'btf>, u64)> {
214    let composite = Composite::from(structure);
215    let member = composite
216        .iter()
217        .find(|member| member.name == Some(OsStr::new(name)))
218        .with_context(|| format!("kernel BTF is missing field {name}"))?;
219    let MemberAttr::Normal { offset } = member.attr else {
220        bail!("kernel field {name} is a bitfield");
221    };
222    ensure!(offset % 8 == 0, "kernel field {name} is not byte aligned");
223    let field = btf
224        .type_by_id::<BtfType<'_>>(member.ty)
225        .context("resolve field type")?
226        .skip_mods_and_typedefs();
227    Ok((field, u64::from(offset / 8)))
228}
229
230/// Pages the pool of task slots starts with. It grows as tasks need more.
231pub const POOL_PAGES: u32 = 512;
232
233/// The configuration table's kernel entries: the offsets and enumerators
234/// resolved from the running kernel's BTF, the pool size, the dump length,
235/// the tick period and the runtime's options, a mask of `config::runtime`'s
236/// bits.
237pub fn configuration(dump_len: u32, runtime_options: u64) -> Result<[u64; config::WORDS]> {
238    let btf = Btf::from_vmlinux().context("read running kernel BTF")?;
239    let task = btf
240        .type_by_name::<Struct<'_>>("task_struct")
241        .context("find task_struct")?;
242    let (entity, entity_offset) = member(&btf, task, "scx")?;
243    let entity =
244        Struct::try_from(entity).map_err(|_| anyhow::anyhow!("task_struct.scx is not a struct"))?;
245    let mut values = [0; config::WORDS];
246    values[config::ARENA_PAGES] = u64::from(POOL_PAGES);
247    values[config::RUNTIME_OPTIONS] = runtime_options;
248    values[config::TICK_NS] = tick_ns();
249    for (index, name, size) in [
250        (config::SCX_SLICE, "slice", 8),
251        (config::SCX_FLAGS, "flags", 4),
252        (config::SCX_TID, "tid", 8),
253        (config::DSQ_VTIME, "dsq_vtime", 8),
254    ] {
255        let (field, offset) = member(&btf, entity, name)?;
256        let integer =
257            Int::try_from(field).map_err(|_| anyhow::anyhow!("scx.{name} is not an integer"))?;
258        ensure!(
259            integer.size() == size
260                && integer.bits as usize == size * 8
261                && integer.offset == 0
262                && matches!(integer.encoding, IntEncoding::None),
263            "unexpected type for scx.{name}"
264        );
265        let offset = entity_offset + offset;
266        ensure!(
267            offset % size as u64 == 0 && offset + size as u64 <= task.size() as u64,
268            "invalid task offset for scx.{name}"
269        );
270        values[index] = offset;
271    }
272    let (cpus, cpus_offset) = member(&btf, task, "cpus_ptr")?;
273    Ptr::try_from(cpus).map_err(|_| anyhow::anyhow!("task_struct.cpus_ptr is not a pointer"))?;
274    ensure!(
275        cpus_offset % 8 == 0 && cpus_offset + 8 <= task.size() as u64,
276        "invalid cpus_ptr offset"
277    );
278    values[config::CPUS_PTR] = cpus_offset;
279    let (migration, migration_offset) = member(&btf, task, "migration_disabled")?;
280    let migration = Int::try_from(migration)
281        .map_err(|_| anyhow::anyhow!("task_struct.migration_disabled is not an integer"))?;
282    ensure!(
283        migration.size() == 2
284            && migration.bits == 16
285            && migration.offset == 0
286            && matches!(migration.encoding, IntEncoding::None)
287            && migration_offset % 2 == 0
288            && migration_offset + 2 <= task.size() as u64,
289        "invalid migration_disabled field"
290    );
291    values[config::MIGRATION_DISABLED] = migration_offset;
292    let (flags, flags_offset) = member(&btf, task, "flags")?;
293    let flags =
294        Int::try_from(flags).map_err(|_| anyhow::anyhow!("task_struct.flags is not an integer"))?;
295    ensure!(
296        flags.size() == 4
297            && flags.bits == 32
298            && flags.offset == 0
299            && matches!(flags.encoding, IntEncoding::None)
300            && flags_offset % 4 == 0
301            && flags_offset + 4 <= task.size() as u64,
302        "invalid task flags field"
303    );
304    values[config::TASK_FLAGS] = flags_offset;
305    // sched_attr.sched_runtime lands in se.slice and sets se.custom_slice.
306    let (fair, fair_offset) = member(&btf, task, "se")?;
307    let fair =
308        Struct::try_from(fair).map_err(|_| anyhow::anyhow!("task_struct.se is not a struct"))?;
309    for (index, name, size) in [
310        (config::CUSTOM_SLICE, "custom_slice", 1),
311        (config::SE_SLICE, "slice", 8),
312    ] {
313        let (field, offset) = member(&btf, fair, name)?;
314        let integer =
315            Int::try_from(field).map_err(|_| anyhow::anyhow!("se.{name} is not an integer"))?;
316        ensure!(
317            integer.size() == size && integer.bits as usize == size * 8 && integer.offset == 0,
318            "unexpected type for se.{name}"
319        );
320        let offset = fair_offset + offset;
321        ensure!(
322            offset % size as u64 == 0 && offset + size as u64 <= task.size() as u64,
323            "invalid task offset for se.{name}"
324        );
325        values[index] = offset;
326    }
327    // A task with fewer CPUs allowed than there are is restricted, and its
328    // fork placement counts only the CPUs it may use.
329    let (allowed, allowed_offset) = member(&btf, task, "nr_cpus_allowed")?;
330    let allowed = Int::try_from(allowed)
331        .map_err(|_| anyhow::anyhow!("task_struct.nr_cpus_allowed is not an integer"))?;
332    ensure!(
333        allowed.size() == 4 && allowed.offset == 0 && allowed_offset % 4 == 0,
334        "invalid nr_cpus_allowed field"
335    );
336    values[config::NR_CPUS_ALLOWED] = allowed_offset;
337    // rq->clock_task, rq_clock_task(): the runqueue's clock less IRQ and
338    // steal time, whose drift from the rq clock capacity pressure counts.
339    let runqueue = btf
340        .type_by_name::<Struct<'_>>("rq")
341        .context("find struct rq")?;
342    let (clock, clock_offset) = member(&btf, runqueue, "clock_task")?;
343    let clock =
344        Int::try_from(clock).map_err(|_| anyhow::anyhow!("rq.clock_task is not an integer"))?;
345    ensure!(
346        clock.size() == 8 && clock_offset % 8 == 0 && clock_offset + 8 <= runqueue.size() as u64,
347        "invalid rq.clock_task field"
348    );
349    values[config::RQ_CLOCK_TASK] = clock_offset;
350    // The cgroup fields group scheduling reads: a cgroup's depth, whether it
351    // is still online as its cpu controller exits it, and the cpu.weight it
352    // starts with. A kernel built without group scheduling has none of it.
353    if let Some(args) = btf.type_by_name::<Struct<'_>>("scx_cgroup_init_args") {
354        let cgroup = btf
355            .type_by_name::<Struct<'_>>("cgroup")
356            .context("find struct cgroup")?;
357        let (level, level_offset) = member(&btf, cgroup, "level")?;
358        let level =
359            Int::try_from(level).map_err(|_| anyhow::anyhow!("cgroup.level is not an integer"))?;
360        ensure!(
361            level.size() == 4 && level_offset % 4 == 0,
362            "invalid cgroup.level field"
363        );
364        values[config::TG_LEVEL] = level_offset;
365        let (state, state_offset) = member(&btf, cgroup, "self")?;
366        let state =
367            Struct::try_from(state).map_err(|_| anyhow::anyhow!("cgroup.self is not a struct"))?;
368        let (flags, flags_offset) = member(&btf, state, "flags")?;
369        let flags = Int::try_from(flags)
370            .map_err(|_| anyhow::anyhow!("cgroup.self.flags is not an integer"))?;
371        ensure!(
372            flags.size() == 4 && (state_offset + flags_offset) % 4 == 0,
373            "invalid cgroup.self.flags field"
374        );
375        values[config::TG_FLAGS] = state_offset + flags_offset;
376        values[config::TG_ONLINE] = btf
377            .type_by_kind::<Enum<'_>>()
378            .find_map(|kind| {
379                kind.iter()
380                    .find(|value| value.name == Some(OsStr::new("CSS_ONLINE")))
381                    .map(|value| value.value as u64)
382            })
383            .context("find CSS_ONLINE")?;
384        let (weight, weight_offset) = member(&btf, args, "weight")?;
385        let weight = Int::try_from(weight)
386            .map_err(|_| anyhow::anyhow!("scx_cgroup_init_args.weight is not an integer"))?;
387        ensure!(
388            weight.size() == 4 && weight_offset % 4 == 0,
389            "invalid scx_cgroup_init_args.weight field"
390        );
391        values[config::TG_WEIGHT] = weight_offset;
392        // The cpu.max a cgroup already has when the scheduler loads, which
393        // no set_bandwidth reports. Older kernels have none of it.
394        for (index, name) in [
395            (config::BW_PERIOD, "bw_period_us"),
396            (config::BW_QUOTA, "bw_quota_us"),
397            (config::BW_BURST, "bw_burst_us"),
398        ] {
399            if let Ok((field, offset)) = member(&btf, args, name)
400                && Int::try_from(field).is_ok_and(|field| field.size() == 8)
401                && offset % 8 == 0
402                && offset != 0
403            {
404                values[index] = offset;
405            }
406        }
407        // The cpu controller's css pointer, cgroup.subsys[cpu_cgrp_id],
408        // which is its task_group, and task_group.idle, for a cgroup made
409        // idle before the scheduler was loaded.
410        let (_, subsys_offset) = member(&btf, cgroup, "subsys")?;
411        let cpu = btf
412            .type_by_kind::<Enum<'_>>()
413            .find_map(|kind| {
414                kind.iter()
415                    .find(|value| value.name == Some(OsStr::new("cpu_cgrp_id")))
416                    .map(|value| value.value as u64)
417            })
418            .context("find cpu_cgrp_id")?;
419        let pointer = btf.ptr_size()?.get() as u64;
420        values[config::CPU_CSS] = subsys_offset + cpu * pointer;
421        if let Some(group) = btf.type_by_name::<Struct<'_>>("task_group")
422            && let Ok((_, css_offset)) = member(&btf, group, "css")
423            && let Ok((idle, idle_offset)) = member(&btf, group, "idle")
424            && css_offset == 0
425            && Int::try_from(idle).is_ok_and(|idle| idle.size() == 4)
426        {
427            values[config::TG_IDLE] = idle_offset;
428        }
429        // The cgroup's id, cgroup.kn->id, by which bpf_cgroup_from_id()
430        // finds a cgroup again where no op hands it over.
431        if let Ok((_, node_offset)) = member(&btf, cgroup, "kn")
432            && let Some(node) = btf.type_by_name::<Struct<'_>>("kernfs_node")
433            && let Ok((id, id_offset)) = member(&btf, node, "id")
434            && Int::try_from(id).is_ok_and(|id| id.size() == 8)
435        {
436            values[config::TG_KN] = node_offset;
437            values[config::KN_ID] = id_offset;
438        }
439    }
440    // The exit record ops.exit() copies, the dump context ops.dump_task()
441    // reads, and the task fields that say how long a task has waited.
442    let exit = btf
443        .type_by_name::<Struct<'_>>("scx_exit_info")
444        .context("find struct scx_exit_info")?;
445    values[config::EXIT_CPU] = member(&btf, exit, "exit_cpu").map_or(0, |(_, offset)| offset + 1);
446    for (index, name) in [
447        (config::EXIT_CODE, "exit_code"),
448        (config::EXIT_REASON, "reason"),
449        (config::EXIT_MSG, "msg"),
450        (config::EXIT_DUMP, "dump"),
451        (config::EXIT_KIND, "kind"),
452    ] {
453        values[index] = member(&btf, exit, name)?.1;
454    }
455    values[config::DUMP_LEN] = u64::from(dump_len);
456    let dump = btf
457        .type_by_name::<Struct<'_>>("scx_dump_ctx")
458        .context("find struct scx_dump_ctx")?;
459    values[config::DUMP_AT_JIFFIES] = member(&btf, dump, "at_jiffies")?.1;
460    values[config::DUMP_AT_NS] = member(&btf, dump, "at_ns")?.1;
461    values[config::RUNNABLE_AT] = entity_offset + member(&btf, entity, "runnable_at")?.1;
462    values[config::PID] = member(&btf, task, "pid")?.1;
463    let (se, se_offset) = member(&btf, task, "se")?;
464    let se = Struct::try_from(se).map_err(|_| anyhow::anyhow!("task_struct.se is not a struct"))?;
465    values[config::SUM_EXEC_RUNTIME] = se_offset + member(&btf, se, "sum_exec_runtime")?.1;
466    // The enqueue flags and re-enqueue reasons that say whether
467    // ops.select_cid() placed a task and why the kernel handed one back,
468    // and the lazy preemption flag, 0 where the kernel has none.
469    for (index, name) in [
470        (config::ENQ_CPU_SELECTED, "SCX_ENQ_CPU_SELECTED"),
471        (config::ENQ_IMMED, "SCX_ENQ_IMMED"),
472        (config::ENQ_REENQ, "SCX_ENQ_REENQ"),
473        (config::REENQ_MASK, "SCX_TASK_REENQ_REASON_MASK"),
474        (config::REENQ_IMMED, "SCX_TASK_REENQ_IMMED"),
475        (config::REENQ_PREEMPTED, "SCX_TASK_REENQ_PREEMPTED"),
476        (config::ENQ_LAST, "SCX_ENQ_LAST"),
477        (config::ENQ_PREEMPT_LAZY, "SCX_ENQ_PREEMPT_LAZY"),
478        (config::ENQ_BLOCKED, "SCX_ENQ_BLOCKED"),
479    ] {
480        values[index] = enumerator(&btf, name).unwrap_or(0);
481    }
482    // Whether a task is a proxy-execution donor, on a kernel that has it.
483    if let Ok((blocked, offset)) = member(&btf, task, "is_blocked")
484        && Int::try_from(blocked).is_ok_and(|blocked| blocked.size() == 1)
485    {
486        values[config::IS_BLOCKED] = offset;
487    }
488    // Whether the groups of a CPU's sd_llc are clusters, for the idle scan
489    // to search the target's cluster first: sched_domain.groups plus one,
490    // 0 where it cannot be read, sched_group.flags and SD_CLUSTER.
491    if let (Some(domain), Some(group), Some(cluster)) = (
492        btf.type_by_name::<Struct<'_>>("sched_domain"),
493        btf.type_by_name::<Struct<'_>>("sched_group"),
494        enumerator(&btf, "SD_CLUSTER"),
495    ) && let (Ok((groups, groups_offset)), Ok((flags, flags_offset))) =
496        (member(&btf, domain, "groups"), member(&btf, group, "flags"))
497        && Ptr::try_from(groups).is_ok()
498        && Int::try_from(flags).is_ok_and(|flags| flags.size() == 4)
499        && groups_offset % 8 == 0
500        && flags_offset % 4 == 0
501    {
502        values[config::SD_GROUPS] = groups_offset + 1;
503        values[config::SG_FLAGS] = flags_offset;
504        values[config::SD_CLUSTER] = cluster;
505    }
506    // The task's load weight, set_load_weight(): its nice level, or
507    // WEIGHT_IDLEPRIO for SCHED_IDLE.
508    for (index, name) in [
509        (config::STATIC_PRIO, "static_prio"),
510        (config::POLICY, "policy"),
511    ] {
512        let (field, offset) = member(&btf, task, name)?;
513        let integer = Int::try_from(field)
514            .map_err(|_| anyhow::anyhow!("task_struct.{name} is not an integer"))?;
515        ensure!(
516            integer.size() == 4
517                && integer.bits == 32
518                && integer.offset == 0
519                && offset % 4 == 0
520                && offset + 4 <= task.size() as u64,
521            "invalid task_struct.{name} field"
522        );
523        values[index] = offset;
524    }
525    Ok(values)
526}
527
528/// The arena's size in pages, as scheds/include/lib/arena_map.h declares it:
529/// address space only, backed by the pages actually allocated. An arena
530/// spans at most 4 GiB, which 2^16 pages keep to with 64 KiB pages on arm64.
531const ARENA_PAGES: u32 = if cfg!(target_arch = "aarch64") {
532    1 << 16
533} else {
534    1 << 20
535};
536
537/// Open the object `object` as `name`, with libbpf's open options that
538/// ObjectBuilder does not take, such as a BPF token path, as the C
539/// schedulers pass them.
540pub fn open(name: &str, object: &[u8], libbpf: &LibbpfOpts, verbose: bool) -> Result<OpenObject> {
541    // build.rs leaves the objects empty on a host without the Rust BPF
542    // toolchain, so that workspace builds do not need it.
543    ensure!(
544        !object.is_empty(),
545        "built without the Rust BPF toolchain (rust-src, bpf-linker and \
546         llvm-objcopy): install it and rebuild {name}"
547    );
548    let (object, adapted) = kfunc_protos::adapt(object).context("match kfunc prototypes")?;
549    let object = object.as_slice();
550    for kfunc in adapted {
551        info!("{kfunc}() returns differently on this kernel: prototype matched");
552    }
553    let mut builder = ObjectBuilder::default();
554    builder.name(name)?;
555    builder.debug(verbose);
556    let name = CString::new(name)?;
557    Ok(match libbpf.clone().into_bpf_open_opts() {
558        Some(mut opts) => {
559            opts.object_name = name.as_ptr();
560            let pointer = unsafe {
561                libbpf_rs::libbpf_sys::bpf_object__open_mem(
562                    object.as_ptr().cast(),
563                    object.len() as libbpf_rs::libbpf_sys::size_t,
564                    &opts,
565                )
566            };
567            let pointer = NonNull::new(pointer).ok_or_else(|| {
568                anyhow::anyhow!("open Rust BPF object: {}", std::io::Error::last_os_error())
569            })?;
570            unsafe { OpenObject::from_ptr(pointer) }
571        }
572        None => builder
573            .open_memory(object)
574            .context("open Rust BPF object")?,
575    })
576}
577
578/// Pick the struct_ops map of the object `name` the kernel can take: the one whose cgroup
579/// callbacks it knows, `cgroup_ops`, or `rust_ops` without the callbacks
580/// it lacks, which are zeroed in the map's initial value. The exit dump
581/// buffer is sized to `dump_len` and the map told so. Returns the map's
582/// name.
583pub fn select_ops(
584    name: &str,
585    open: &mut OpenObject,
586    object: &[u8],
587    cgroup_ops: Option<&'static str>,
588    dump_len: u32,
589) -> Result<&'static str> {
590    let ops = cgroup_ops.unwrap_or("rust_ops");
591    let ops_type = ops_type(ops);
592    let callbacks = missing_callbacks(name, object, ops_type)?;
593    let dump_length = ops_member(name, object, ops_type, "exit_dump_len")?;
594    // The buffer ops.exit() copies the debug dump into, as long as the
595    // dump the kernel is asked for.
596    open.maps_mut()
597        .find(|map| map.name() == OsStr::new(".data.uei_dump"))
598        .context("find the exit dump")?
599        .set_value_size(dump_len)?;
600    for mut map in open.maps_mut() {
601        let name = map.name().to_owned();
602        if name.as_encoded_bytes().starts_with(b"rust_ops") {
603            map.set_autocreate(name == OsStr::new(ops))?;
604            if name == OsStr::new(ops) {
605                let data = map
606                    .initial_value_mut()
607                    .context("initialize the struct_ops map")?;
608                for range in &callbacks {
609                    data.get_mut(range.clone())
610                        .context("cgroup callback beyond the struct_ops map")?
611                        .fill(0);
612                }
613                let length = dump_length.clone();
614                data.get_mut(length)
615                    .context("exit_dump_len beyond the struct_ops map")?
616                    .copy_from_slice(&dump_len.to_ne_bytes());
617            }
618        }
619    }
620    Ok(ops)
621}
622
623/// The BPF object's struct_ops type of the map `ops`.
624fn ops_type(ops: &str) -> &'static str {
625    if ops == "rust_ops" {
626        "sched_ext_ops_cid"
627    } else {
628        "sched_ext_ops_cid___cgroup"
629    }
630}
631
632/// The ops flags the struct_ops map `ops` asks the kernel for, as
633/// `#[scheduler]` declares them.
634pub fn ops_flags(name: &str, open: &mut OpenObject, object: &[u8], ops: &str) -> Result<u64> {
635    let range = ops_member(name, object, ops_type(ops), "flags")?;
636    let map = open
637        .maps_mut()
638        .find(|map| map.name() == OsStr::new(ops))
639        .context("find the struct_ops map")?;
640    let bytes = map
641        .initial_value()
642        .and_then(|data| data.get(range))
643        .context("flags beyond the struct_ops map")?;
644    Ok(u64::from_ne_bytes(bytes.try_into()?))
645}
646
647/// Load the struct_ops map `ops` with the ops `flags`.
648pub fn set_ops_flags(
649    name: &str,
650    open: &mut OpenObject,
651    object: &[u8],
652    ops: &str,
653    flags: u64,
654) -> Result<()> {
655    let range = ops_member(name, object, ops_type(ops), "flags")?;
656    let mut map = open
657        .maps_mut()
658        .find(|map| map.name() == OsStr::new(ops))
659        .context("find the struct_ops map")?;
660    map.initial_value_mut()
661        .and_then(|data| data.get_mut(range))
662        .context("flags beyond the struct_ops map")?
663        .copy_from_slice(&flags.to_ne_bytes());
664    Ok(())
665}
666
667/// The longest watchdog timeout the kernel takes, SCX_WATCHDOG_MAX_TIMEOUT,
668/// which is also what it applies when the timeout is 0.
669pub const WATCHDOG_MAX_TIMEOUT_MS: u32 = 30_000;
670
671/// Load the struct_ops map `ops` with the watchdog timeout `timeout_ms`, in
672/// place of `timeout_ms` of `#[scheduler]`.
673pub fn set_ops_timeout(
674    name: &str,
675    open: &mut OpenObject,
676    object: &[u8],
677    ops: &str,
678    timeout_ms: u32,
679) -> Result<()> {
680    // The kernel refuses a longer one with EINVAL, which says less.
681    ensure!(
682        timeout_ms <= WATCHDOG_MAX_TIMEOUT_MS,
683        "the watchdog timeout is at most {WATCHDOG_MAX_TIMEOUT_MS} ms, not {timeout_ms}"
684    );
685    let range = ops_member(name, object, ops_type(ops), "timeout_ms")?;
686    let mut map = open
687        .maps_mut()
688        .find(|map| map.name() == OsStr::new(ops))
689        .context("find the struct_ops map")?;
690    map.initial_value_mut()
691        .and_then(|data| data.get_mut(range))
692        .context("timeout_ms beyond the struct_ops map")?
693        .copy_from_slice(&timeout_ms.to_ne_bytes());
694    Ok(())
695}
696
697/// The length of the debug dump the scheduler's struct_ops map asks for,
698/// `dump_len` of `#[scheduler]`.
699pub fn declared_dump_len(name: &str, open: &mut OpenObject, object: &[u8]) -> Result<u32> {
700    let range = ops_member(name, object, "sched_ext_ops_cid", "exit_dump_len")?;
701    let mut map = open
702        .maps_mut()
703        .find(|map| map.name() == OsStr::new("rust_ops"))
704        .context("find the struct_ops map")?;
705    let bytes = map
706        .initial_value_mut()
707        .context("read the struct_ops map")?
708        .get(range)
709        .context("exit_dump_len beyond the struct_ops map")?;
710    // Zero asks the kernel for its default.
711    Ok(match u32::from_ne_bytes(bytes.try_into()?) {
712        0 => EXIT_DUMP_DEFAULT,
713        length => length,
714    })
715}
716
717/// Size the arena and, when the object has them, the hrtick timers, one
718/// per CID. CIDs are dense and never exceed nr_cpu_ids.
719pub fn size_maps(open: &mut OpenObject) -> Result<()> {
720    open.maps_mut()
721        .find(|map| map.name() == OsStr::new("arena"))
722        .context("find arena map")?
723        .set_max_entries(ARENA_PAGES)?;
724    for mut map in open
725        .maps_mut()
726        .filter(|map| map.name() == OsStr::new("hrticks"))
727    {
728        map.set_max_entries(libbpf_rs::num_possible_cpus()? as u32)?;
729    }
730    Ok(())
731}
732
733/// Fill the frozen configuration table with `config`.
734pub fn set_config(open: &mut OpenObject, config: &[u64]) -> Result<()> {
735    let mut map = open
736        .maps_mut()
737        .find(|map| map.name().as_encoded_bytes().ends_with(b".rodata.config"))
738        .context("find scheduler configuration")?;
739    let data = map
740        .initial_value_mut()
741        .context("initialize scheduler configuration")?;
742    ensure!(
743        data.len() == std::mem::size_of_val(config),
744        "unexpected configuration size"
745    );
746    for (destination, value) in data.as_chunks_mut::<8>().0.iter_mut().zip(config) {
747        *destination = value.to_ne_bytes();
748    }
749    Ok(())
750}
751
752/// Attach the scheduler through its struct_ops map `ops`.
753pub fn attach(object: &mut Object, ops: &str) -> Result<Link> {
754    object
755        .maps_mut()
756        .find(|map| map.name() == OsStr::new(ops))
757        .context("find the struct_ops map")?
758        .attach_struct_ops()
759        .context("attach sched_ext scheduler")
760}
761
762/// The arena statistics the BPF side keeps, see pool.rs: slots taken and
763/// released, validation errors, failed and successful page allocations,
764/// and idle-mask updates that ran out of retries.
765pub fn arena_stats(object: &Object) -> Result<[u64; 6]> {
766    let stats = object
767        .maps()
768        .find(|map| map.name() == OsStr::new(".data.astats"))
769        .context("find arena statistics")?;
770    let mut data = [0_u8; 48];
771    ensure!(
772        stats.lookup_into(&0_u32.to_ne_bytes(), &mut data, MapFlags::ANY)?,
773        "read arena statistics"
774    );
775    let mut counters = [0; 6];
776    for (counter, bytes) in counters.iter_mut().zip(data.as_chunks::<8>().0) {
777        *counter = u64::from_ne_bytes(*bytes);
778    }
779    Ok(counters)
780}
781
782/// What the statistics must say after a clean run.
783pub fn check_arena_stats(counters: &[u64; 6]) -> Result<()> {
784    ensure!(
785        counters[0] == counters[1],
786        "arena task contexts were not all released"
787    );
788    ensure!(counters[2] == 0, "arena task context validation failed");
789    ensure!(counters[5] == 0, "idle state CAS retries exhausted");
790    Ok(())
791}
792
793/// Each callback is its own BPF program and the kernel keeps a run count and
794/// a run time per program once the accounting is switched on. That costs two
795/// clock reads around every callback, so it is on only while a stats client
796/// is polling and goes away by itself once nobody has asked for a while.
797pub struct OpsProfiler {
798    /// Holding the descriptor keeps the accounting enabled.
799    pub enabled: Option<OwnedFd>,
800    last_read: Instant,
801    warned: bool,
802}
803
804impl Default for OpsProfiler {
805    fn default() -> Self {
806        Self::new()
807    }
808}
809
810impl OpsProfiler {
811    /// How long without a stats request before the accounting is dropped.
812    const IDLE: Duration = Duration::from_secs(5);
813
814    pub fn new() -> Self {
815        Self {
816            enabled: None,
817            last_read: Instant::now(),
818            warned: false,
819        }
820    }
821
822    fn enable(&mut self) {
823        if self.enabled.is_some() {
824            return;
825        }
826        let fd = unsafe {
827            libbpf_rs::libbpf_sys::bpf_enable_stats(libbpf_rs::libbpf_sys::BPF_STATS_RUN_TIME)
828        };
829        if fd < 0 {
830            if !self.warned {
831                eprintln!(
832                    "BPF run-time stats unavailable: {}",
833                    std::io::Error::last_os_error()
834                );
835                self.warned = true;
836            }
837            return;
838        }
839        self.enabled = Some(unsafe { OwnedFd::from_raw_fd(fd) });
840    }
841
842    /// Drop the accounting once no client has polled for a while.
843    pub fn expire(&mut self) {
844        if self.enabled.is_some() && self.last_read.elapsed() > Self::IDLE {
845            self.enabled = None;
846        }
847    }
848
849    /// Cumulative counts and times of every struct_ops program, keyed by the
850    /// callback name. The kernel truncates program names, so the names come
851    /// from the object.
852    pub fn sample(&mut self, object: &Object) -> BTreeMap<String, OpStats> {
853        self.last_read = Instant::now();
854        self.enable();
855        let mut ops = BTreeMap::new();
856        if self.enabled.is_none() {
857            return ops;
858        }
859        for program in object.progs() {
860            if program.prog_type() != ProgramType::StructOps {
861                continue;
862            }
863            let mut info: libbpf_rs::libbpf_sys::bpf_prog_info = unsafe { std::mem::zeroed() };
864            let mut len = std::mem::size_of_val(&info) as u32;
865            let result = unsafe {
866                libbpf_rs::libbpf_sys::bpf_prog_get_info_by_fd(
867                    program.as_fd().as_raw_fd(),
868                    (&raw mut info).cast(),
869                    &mut len,
870                )
871            };
872            if result != 0 {
873                continue;
874            }
875            let name = program.name().to_string_lossy();
876            // A callback of #[scheduler] is scx_NAME, a hand-written entry
877            // point rust_NAME.
878            let name = name
879                .strip_prefix("scx_")
880                .or_else(|| name.strip_prefix("rust_"))
881                .unwrap_or(&name)
882                .to_string();
883            ops.insert(
884                name,
885                OpStats {
886                    calls: info.run_cnt,
887                    ns: info.run_time_ns,
888                    misses: info.recursion_misses,
889                },
890            );
891        }
892        ops
893    }
894}
895
896/// What the arena holds, from the map's memlock accounting, which kernels
897/// from 7.3 keep for the allocated pages, and the size of the map.
898pub fn arena_usage(object: &Object) -> (Option<u64>, u64) {
899    let Some(map) = object.maps().find(|map| map.name() == OsStr::new("arena")) else {
900        return (None, 0);
901    };
902    let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }.max(1) as u64;
903    let max = u64::from(map.max_entries()) * page;
904    let fd = map.as_fd().as_raw_fd();
905    let memlock: u64 = std::fs::read_to_string(format!("/proc/self/fdinfo/{fd}"))
906        .ok()
907        .and_then(|info| {
908            info.lines()
909                .find_map(|line| line.strip_prefix("memlock:"))
910                .and_then(|value| value.trim().parse().ok())
911        })
912        .unwrap_or(0);
913    ((memlock > 0).then_some(memlock), max)
914}
915
916/// SCX_EXIT_UNREG_KERN, the last of the graceful exit kinds.
917const EXIT_UNREG_KERN: i32 = 66;
918
919/// SCX_EXIT_DUMP_DFL_LEN, the dump length the kernel uses for 0.
920pub const EXIT_DUMP_DEFAULT: u32 = 32768;
921/// Size of struct user_exit_info: kind, exit_cpu, exit_code, a 128-byte
922/// reason and a 1024-byte message.
923const UEI_BYTES: usize = 4 + 4 + 8 + 128 + 1024;
924
925/// The exit record ops.exit() keeps, reported as scx_utils reports it: the
926/// debug dump and the exit reason, as an error for an error exit.
927pub fn exit_report(object: &Object, dump_len: u32) -> Result<Option<UserExitInfo>> {
928    let uei = object
929        .maps()
930        .find(|map| map.name() == OsStr::new(".data.uei"))
931        .context("find the exit record")?;
932    let mut record = vec![0_u8; UEI_BYTES];
933    ensure!(
934        uei.lookup_into(&0_u32.to_ne_bytes(), &mut record, MapFlags::ANY)?,
935        "read the exit record"
936    );
937    if i32::from_ne_bytes(record[0..4].try_into()?) == 0 {
938        return Ok(None);
939    }
940    let dump = object
941        .maps()
942        .find(|map| map.name() == OsStr::new(".data.uei_dump"))
943        .context("find the exit dump")?;
944    let mut text = vec![0_u8; dump_len as usize + 1];
945    dump.lookup_into(
946        &0_u32.to_ne_bytes(),
947        &mut text[..dump_len as usize],
948        MapFlags::ANY,
949    )?;
950    record[UEI_BYTES - 1] = 0;
951    record[16 + 127] = 0;
952    Ok(Some(UserExitInfo::new(
953        record[0..4].as_ptr().cast(),
954        record[8..16].as_ptr().cast(),
955        record[4..8].as_ptr().cast(),
956        record[16..].as_ptr().cast(),
957        record[16 + 128..].as_ptr().cast(),
958        text.as_ptr().cast(),
959    )))
960}
961
962/// TICK_NSEC. The coarse clocks advance only from the timer interrupt, so
963/// their resolution is the tick period.
964fn tick_ns() -> u64 {
965    let mut time = libc::timespec {
966        tv_sec: 0,
967        tv_nsec: 0,
968    };
969    let result = unsafe { libc::clock_getres(libc::CLOCK_MONOTONIC_COARSE, &mut time) };
970    if result == 0 && time.tv_sec == 0 && time.tv_nsec > 0 {
971        time.tv_nsec as u64
972    } else {
973        1_000_000
974    }
975}
976
977/// The section of a tunable: `scx_rustsched::params!` puts each in a read-only
978/// section of its own, named PREFIX NAME "." TYPE.
979const PARAM_PREFIX: &str = ".rodata.param.";
980
981/// A tunable's name as the command line takes it.
982fn param_name(name: &str) -> String {
983    name.to_ascii_lowercase().replace('-', "_")
984}
985
986/// The name, in lower case, and the type of the tunable the map `section`
987/// holds, if it holds one.
988fn param_of(section: &OsStr) -> Option<(String, String)> {
989    let (name, ty) = section
990        .to_str()?
991        .strip_prefix(PARAM_PREFIX)?
992        .rsplit_once('.')?;
993    Some((param_name(name), ty.to_owned()))
994}
995
996/// The value `text` gives a tunable of type `ty` and `size` bytes: true or
997/// false for a bool, and otherwise an integer, decimal or 0x hexadecimal,
998/// in the range of the type. None if it is not one.
999fn param_parse(ty: &str, size: usize, text: &str) -> Option<u64> {
1000    let bits = 8 * u32::try_from(size).ok()?;
1001    if !(8..=64).contains(&bits) {
1002        return None;
1003    }
1004    match (ty, text) {
1005        ("bool", "true" | "1") => Some(1),
1006        ("bool", "false" | "0") => Some(0),
1007        ("bool", _) => None,
1008        _ if ty.starts_with('i') => text
1009            .parse::<i64>()
1010            .ok()
1011            .filter(|value| (value << (64 - bits)) >> (64 - bits) == *value)
1012            .map(|value| value as u64),
1013        _ => match text.strip_prefix("0x") {
1014            Some(digits) => u64::from_str_radix(digits, 16).ok(),
1015            None => text.parse::<u64>().ok(),
1016        }
1017        .filter(|value| bits == 64 || value >> bits == 0),
1018    }
1019}
1020
1021/// The element type of an array tunable, whose type is its element's in
1022/// brackets.
1023fn param_element(ty: &str) -> Option<&str> {
1024    ty.strip_prefix('[')?.strip_suffix(']')
1025}
1026
1027/// A value a tunable can hold: an integer or a bool, alone or as the
1028/// element of an array.
1029pub trait ParamValue: Copy {
1030    /// The type as `scx_rustsched::params!` names it.
1031    const TYPE: &'static str;
1032    /// Append the value as the object holds it.
1033    fn append(self, bytes: &mut Vec<u8>);
1034}
1035
1036macro_rules! param_values {
1037    ($($type:ident),*) => {
1038        $(impl ParamValue for $type {
1039            const TYPE: &'static str = stringify!($type);
1040            fn append(self, bytes: &mut Vec<u8>) {
1041                bytes.extend_from_slice(&self.to_ne_bytes());
1042            }
1043        })*
1044    };
1045}
1046param_values!(u8, u16, u32, u64, i8, i16, i32, i64);
1047
1048impl ParamValue for bool {
1049    const TYPE: &'static str = "bool";
1050    fn append(self, bytes: &mut Vec<u8>) {
1051        bytes.push(u8::from(self));
1052    }
1053}
1054
1055/// The tunables of an object that is open and not loaded yet, the ones it
1056/// declares with `scx_rustsched::params!`, to set before the load, and the ops
1057/// flags it is loaded with. It records which tunables were set: run()
1058/// refuses to start a scheduler that has one nobody set.
1059pub struct Params<'a> {
1060    open: &'a mut OpenObject,
1061    set: std::collections::BTreeSet<String>,
1062    flags: u64,
1063}
1064
1065impl<'a> Params<'a> {
1066    /// The tunables of `open`, which is loaded with the ops `flags`.
1067    pub fn new(open: &'a mut OpenObject, flags: u64) -> Self {
1068        Self {
1069            open,
1070            set: Default::default(),
1071            flags,
1072        }
1073    }
1074
1075    /// The ops flags the scheduler is loaded with: the ones `#[scheduler]`
1076    /// declares until changed here.
1077    pub fn flags(&self) -> u64 {
1078        self.flags
1079    }
1080
1081    /// Load the scheduler with the ops `flags` too, such as one the
1082    /// running kernel may lack, which `compat` reads as 0 there.
1083    pub fn add_flags(&mut self, flags: u64) {
1084        self.flags |= flags;
1085    }
1086
1087    /// Load the scheduler without the ops `flags`.
1088    pub fn clear_flags(&mut self, flags: u64) {
1089        self.flags &= !flags;
1090    }
1091
1092    /// The type of the tunable `name`, if the scheduler has it.
1093    pub fn type_of(&mut self, name: &str) -> Option<String> {
1094        let name = param_name(name);
1095        self.open
1096            .maps_mut()
1097            .filter_map(|map| param_of(map.name()))
1098            .find_map(|(found, ty)| (found == name).then_some(ty))
1099    }
1100
1101    /// Give the tunable `name`, of type `ty`, the first `data.len()` bytes
1102    /// `bytes(data.len())` returns.
1103    fn write(
1104        &mut self,
1105        name: &str,
1106        bytes: impl FnOnce(&str, usize) -> Result<Vec<u8>>,
1107    ) -> Result<()> {
1108        let name = param_name(name);
1109        let (mut map, ty) = self
1110            .open
1111            .maps_mut()
1112            .find_map(|map| match param_of(map.name()) {
1113                Some((found, ty)) if found == name => Some((map, ty)),
1114                _ => None,
1115            })
1116            .with_context(|| format!("the scheduler has no parameter {name}"))?;
1117        let data = map
1118            .initial_value_mut()
1119            .with_context(|| format!("parameter {name} has no value"))?;
1120        let bytes = bytes(&ty, data.len()).with_context(|| format!("parameter {name}"))?;
1121        data.copy_from_slice(&bytes);
1122        self.set.insert(name);
1123        Ok(())
1124    }
1125
1126    /// Set a tunable from the value of a command line option: true or
1127    /// false for a bool, and otherwise an integer, decimal or 0x
1128    /// hexadecimal, in the range of the tunable's type.
1129    pub fn set_text(&mut self, name: &str, text: &str) -> Result<()> {
1130        self.write(name, |ty, size| {
1131            let value = param_parse(ty, size, text)
1132                .with_context(|| format!("{text:?} is not a valid {ty}"))?;
1133            Ok(value.to_ne_bytes()[..size].to_vec())
1134        })
1135    }
1136
1137    /// Set a tunable from values of its own type: one for an integer or a
1138    /// bool, and for an array at most as many as it has elements, the
1139    /// rest of which become zero.
1140    pub fn set<T: ParamValue>(&mut self, name: &str, values: &[T]) -> Result<()> {
1141        self.write(name, |ty, size| {
1142            let array = param_element(ty);
1143            ensure!(
1144                array.unwrap_or(ty) == T::TYPE,
1145                "it is {ty}, not {}",
1146                T::TYPE
1147            );
1148            let mut bytes = Vec::new();
1149            values.iter().for_each(|value| value.append(&mut bytes));
1150            ensure!(
1151                bytes.len() == size || array.is_some() && bytes.len() < size,
1152                "{} values do not fit {ty} of {size} bytes",
1153                values.len()
1154            );
1155            bytes.resize(size, 0);
1156            Ok(bytes)
1157        })
1158    }
1159
1160    /// Set the tunables the options `A` name, from the command line as
1161    /// parsed: an option sets the tunable of its name, so `steal`, given
1162    /// as `--steal`, sets `STEAL`. A time in microseconds or milliseconds
1163    /// sets the tunable in nanoseconds: `slice_us`, given as `--slice-us`,
1164    /// sets `SLICE_NS` to a thousand times its value. An option's default
1165    /// is the one in `A`, which the scheduler starts with whatever default
1166    /// the tunable was declared with. An option that names no tunable, or
1167    /// an array, is left to the scheduler's own settings.
1168    pub fn set_options<A: clap::Args>(&mut self, matches: &clap::ArgMatches) -> Result<()> {
1169        for (name, value) in named::<A>(matches) {
1170            let scalar = |ty: String| param_element(&ty).is_none();
1171            if let Some(ty) = self.type_of(&name) {
1172                if scalar(ty) {
1173                    self.set_text(&name, &value)?;
1174                }
1175                continue;
1176            }
1177            let Some((tunable, factor)) = time_unit(&name) else {
1178                continue;
1179            };
1180            if self.type_of(&tunable).is_some_and(scalar) {
1181                let scaled = value
1182                    .parse::<i128>()
1183                    .ok()
1184                    .and_then(|value| value.checked_mul(factor))
1185                    .with_context(|| {
1186                        format!("--{}: {value:?} is not a time", name.replace('_', "-"))
1187                    })?;
1188                self.set_text(&tunable, &scaled.to_string())
1189                    .with_context(|| format!("--{} {value}", name.replace('_', "-")))?;
1190            }
1191        }
1192        Ok(())
1193    }
1194
1195    /// Fail if the scheduler has a tunable that was not set: an option
1196    /// whose name does not match the tunable it is meant for, or one left
1197    /// out of the loader's own settings.
1198    pub fn check(self) -> Result<()> {
1199        let unset: Vec<_> = self
1200            .open
1201            .maps_mut()
1202            .filter_map(|map| Some(param_of(map.name())?.0))
1203            .filter(|name| !self.set.contains(name))
1204            .collect();
1205        ensure!(
1206            unset.is_empty(),
1207            "no option sets the scheduler's parameters: {}",
1208            unset.join(", ")
1209        );
1210        Ok(())
1211    }
1212}
1213
1214/// How run() loads and runs a scheduler.
1215#[derive(Default)]
1216pub struct RunOptions {
1217    /// libbpf's open options, such as a BPF token path.
1218    pub libbpf: LibbpfOpts,
1219    /// Print libbpf's debug output.
1220    pub verbose: bool,
1221    /// Load the object and stop, without attaching it.
1222    pub verify: bool,
1223    /// Detach after this many seconds.
1224    pub duration: Option<u64>,
1225    /// Bytes of the kernel's debug dump to keep at exit, 0 for what the
1226    /// scheduler declares.
1227    pub exit_dump_len: u32,
1228    /// The watchdog timeout in milliseconds, 0 for what the scheduler
1229    /// declares.
1230    pub timeout_ms: u32,
1231    /// Print the scheduler's statistics at this interval, in seconds.
1232    pub stats: Option<f64>,
1233    /// The scheduler's version, for `Scheduler::attached()`.
1234    pub version: &'static str,
1235}
1236
1237/// How the CPUs' capacities and packing ranks are read for the scheduler,
1238/// see tiers.rs.
1239pub struct Capacity {
1240    pub mode: tiers::CapacityMode,
1241    /// Capacities within this many percent of each other are one tier.
1242    pub tolerance_pct: Option<u32>,
1243    /// Ignore the kernel's packing priorities.
1244    pub no_asym_packing: bool,
1245}
1246
1247impl Default for Capacity {
1248    fn default() -> Self {
1249        Self {
1250            mode: tiers::CapacityMode::Kernel,
1251            tolerance_pct: None,
1252            no_asym_packing: false,
1253        }
1254    }
1255}
1256
1257/// What a scheduler does while no stats client asks, see [`Stats::idle`].
1258type Idle = Option<Box<dyn FnMut(&Loaded)>>;
1259
1260/// A scheduler's statistics: what it serves to scx_stats clients, and how
1261/// `--stats` and `--monitor` print it. A scheduler has none unless its
1262/// [`Scheduler::stats`] returns this, built from its two callbacks.
1263pub struct Stats {
1264    describe: Box<dyn Fn() -> Result<()>>,
1265    #[allow(clippy::type_complexity)]
1266    serve: Box<dyn FnOnce(Idle) -> Result<Box<dyn FnMut(&Loaded, Duration) -> Result<()>>>>,
1267    idle: Idle,
1268    monitor: Box<dyn FnOnce(Duration, Arc<AtomicBool>) -> Result<()> + Send>,
1269}
1270
1271impl Stats {
1272    /// `data` describes the metrics `M` to the stats server, `metrics`
1273    /// reads them from the running scheduler each time a client asks, and
1274    /// `show` prints what a client received, once per interval.
1275    pub fn new<M>(
1276        data: fn() -> scx_stats::StatsServerData<(), M>,
1277        mut metrics: impl FnMut(&Loaded) -> M + 'static,
1278        mut show: impl FnMut(M, Duration) -> Result<()> + Send + 'static,
1279    ) -> Self
1280    where
1281        M: Send + 'static + for<'a> serde::Deserialize<'a>,
1282    {
1283        Self {
1284            describe: Box::new(move || data().describe_meta(&mut std::io::stdout(), None)),
1285            idle: None,
1286            serve: Box::new(move |mut idle: Idle| {
1287                let server = scx_stats::StatsServer::new(data()).launch()?;
1288                let (responses, requests) = server.channels();
1289                Ok(Box::new(move |object: &Loaded, timeout: Duration| {
1290                    // The server answers its clients for as long as it lives.
1291                    let _server = &server;
1292                    match requests.recv_timeout(timeout) {
1293                        Ok(()) => responses
1294                            .send(metrics(object))
1295                            .map_err(|_| anyhow::anyhow!("stats client went away"))?,
1296                        Err(error) if error.is_timeout() => {
1297                            if let Some(idle) = idle.as_mut() {
1298                                idle(object);
1299                            }
1300                        }
1301                        Err(error) => Err(error)?,
1302                    }
1303                    Ok(())
1304                }))
1305            }),
1306            monitor: Box::new(move |interval, shutdown| {
1307                scx_utils::monitor_stats::<M>(
1308                    &[],
1309                    interval,
1310                    || shutdown.load(Ordering::Relaxed),
1311                    |metrics| show(metrics, interval),
1312                )
1313            }),
1314        }
1315    }
1316}
1317
1318impl Stats {
1319    /// Call `idle` about ten times a second while no client asks, for
1320    /// what the scheduler keeps only for a client, such as the callback
1321    /// profiler's accounting, to be dropped once nobody has asked for a
1322    /// while.
1323    pub fn idle(mut self, idle: impl FnMut(&Loaded) + 'static) -> Self {
1324        self.idle = Some(Box::new(idle));
1325        self
1326    }
1327}
1328
1329/// What a scheduler adds to the loader, on its options. Every method has a
1330/// default, so `impl Scheduler for Opts {}` is a scheduler with nothing
1331/// but the tunables its options name.
1332pub trait Scheduler: clap::Args {
1333    /// Set the tunables no option sets by its name, see `main!`.
1334    fn settings(&self, _params: &mut Params) -> Result<()> {
1335        Ok(())
1336    }
1337
1338    /// What the scheduler asks of the runtime, a mask of
1339    /// `config::runtime`'s bits.
1340    fn runtime(&self) -> u64 {
1341        0
1342    }
1343
1344    /// How the CPUs' capacities and packing ranks are read.
1345    fn capacity(&self) -> Capacity {
1346        Capacity::default()
1347    }
1348
1349    /// The scheduler's statistics, for `--stats` and `--monitor`. None: it
1350    /// has no statistics.
1351    fn stats(&self) -> Option<Stats> {
1352        None
1353    }
1354
1355    /// Called once the scheduler is attached and its init has run, with
1356    /// what the loader knows about the run, for the scheduler to report as
1357    /// it likes.
1358    fn attached(&self, attached: &Attached<'_>) {
1359        info!("{} attached", attached.name());
1360    }
1361
1362    /// Called when the scheduler was unregistered without an error, `why`
1363    /// being the reason, and the message if any. An error exit is reported
1364    /// by the loader, with the kernel's debug dump.
1365    fn stopped(&self, name: &str, why: &str) {
1366        info!("{name} stopped: {why}");
1367    }
1368}
1369
1370/// The run of an attached scheduler, see `Scheduler::attached()`: one
1371/// method for each thing the loader knows about it.
1372pub struct Attached<'a> {
1373    name: &'a str,
1374    version: &'a str,
1375    object: &'a Object,
1376    flags: u64,
1377    runtime: u64,
1378    tiers: Option<&'a Tiers>,
1379    topology: std::cell::OnceCell<Option<Topology>>,
1380}
1381
1382/// What `Attached` reads off scx_utils' Topology, once asked.
1383struct Topology {
1384    nr_cpus: usize,
1385    nr_nodes: usize,
1386    smt: bool,
1387}
1388
1389impl Attached<'_> {
1390    /// The scheduler's name.
1391    pub fn name(&self) -> &str {
1392        self.name
1393    }
1394
1395    /// The scheduler's version.
1396    pub fn version(&self) -> &str {
1397        self.version
1398    }
1399
1400    /// The version with the build's git revision and target, what
1401    /// `--version` prints.
1402    pub fn build(&self) -> String {
1403        scx_utils::build_id::full_version(self.version)
1404    }
1405
1406    /// The loaded object, to read what the scheduler's init computed.
1407    pub fn object(&self) -> &Object {
1408        self.object
1409    }
1410
1411    /// The running kernel's release string, what uname -r prints.
1412    pub fn kernel_release(&self) -> String {
1413        let mut uts: libc::utsname = unsafe { std::mem::zeroed() };
1414        if unsafe { libc::uname(&mut uts) } != 0 {
1415            return "unknown".to_string();
1416        }
1417        unsafe { std::ffi::CStr::from_ptr(uts.release.as_ptr()) }
1418            .to_string_lossy()
1419            .into_owned()
1420    }
1421
1422    /// The tick period in nanoseconds, TICK_NSEC.
1423    pub fn tick_ns(&self) -> u64 {
1424        tick_ns()
1425    }
1426
1427    fn topology(&self) -> Option<&Topology> {
1428        self.topology
1429            .get_or_init(|| {
1430                let topology = scx_utils::Topology::new().ok()?;
1431                Some(Topology {
1432                    nr_cpus: topology.all_cpus.len(),
1433                    nr_nodes: topology
1434                        .nodes
1435                        .values()
1436                        .filter(|node| !node.all_cpus.is_empty())
1437                        .count(),
1438                    smt: topology.smt_enabled,
1439                })
1440            })
1441            .as_ref()
1442    }
1443
1444    /// The machine's CPUs. None when the topology cannot be read.
1445    pub fn nr_cpus(&self) -> Option<usize> {
1446        Some(self.topology()?.nr_cpus)
1447    }
1448
1449    /// The machine's NUMA nodes with CPUs. None when the topology cannot be
1450    /// read.
1451    pub fn nr_nodes(&self) -> Option<usize> {
1452        Some(self.topology()?.nr_nodes)
1453    }
1454
1455    /// Does the runtime treat the machine's NUMA nodes as one?
1456    pub fn numa_disabled(&self) -> bool {
1457        self.runtime & config::runtime::DISABLE_NUMA != 0
1458    }
1459
1460    /// Does the runtime use SMT: the machine has it, and the runtime is not
1461    /// told to treat every CPU as a core of its own. None when the topology
1462    /// cannot be read.
1463    pub fn smt(&self) -> Option<bool> {
1464        Some(self.topology()?.smt && self.runtime & config::runtime::DISABLE_SMT == 0)
1465    }
1466
1467    /// The ops flags the struct_ops map asks the kernel for.
1468    pub fn ops_flags(&self) -> u64 {
1469        self.flags
1470    }
1471
1472    /// The CPUs' capacity and packing tiers, if they could be read.
1473    pub fn tiers(&self) -> Option<&Tiers> {
1474        self.tiers
1475    }
1476
1477    /// Bytes the arena holds, when the kernel accounts them, and the
1478    /// arena's size, see `arena_usage()`.
1479    pub fn arena_usage(&self) -> (Option<u64>, u64) {
1480        arena_usage(self.object)
1481    }
1482}
1483
1484/// Read the CPUs' capacities and packing priorities and hand them to the
1485/// loaded object, see tiers.rs.
1486fn publish_tiers(object: &mut Object, capacity: &Capacity) -> Result<Tiers> {
1487    let mut tiers = tiers::Tiers::new(capacity.mode, capacity.tolerance_pct)?;
1488    tiers.read_priorities(object, capacity.no_asym_packing)?;
1489    tiers.publish(object)?;
1490    Ok(tiers)
1491}
1492
1493/// The stats client, in its own thread: beside the scheduler with
1494/// `--stats`, started once its server is up.
1495fn spawn_monitor(
1496    monitor: Box<dyn FnOnce(Duration, Arc<AtomicBool>) -> Result<()> + Send>,
1497    interval: f64,
1498    shutdown: Arc<AtomicBool>,
1499) -> std::thread::JoinHandle<()> {
1500    std::thread::spawn(move || {
1501        if let Err(error) = monitor(Duration::from_secs_f64(interval), shutdown) {
1502            eprintln!("stats monitor thread finished because of an error {error}");
1503        }
1504    })
1505}
1506
1507/// Load, attach and run the scheduler `name` until `shutdown` is set, the
1508/// duration is over or the kernel ejects it. `legacy` and `sized` are the
1509/// two objects scx_rustsched_build leaves in OUT_DIR, of which the running
1510/// kernel takes one. `matches` is the command line the options
1511/// `scheduler` were parsed from: its options set the tunables they name,
1512/// and the scheduler's own settings the rest.
1513pub fn run<S: Scheduler>(
1514    name: &str,
1515    legacy: &[u8],
1516    sized: &[u8],
1517    options: &RunOptions,
1518    shutdown: &Arc<AtomicBool>,
1519    matches: &clap::ArgMatches,
1520    scheduler: &S,
1521) -> Result<()> {
1522    let bytes = if sized_topology()? { sized } else { legacy };
1523    let mut open = open(name, bytes, &options.libbpf, options.verbose)?;
1524    let dump_len = match options.exit_dump_len {
1525        0 => declared_dump_len(name, &mut open, bytes)?,
1526        len => len,
1527    };
1528    let config = configuration(dump_len, scheduler.runtime())?;
1529    // A scheduler with cgroup callbacks has a second map for a kernel
1530    // that names them cgroup_*.
1531    let cgroup = if open
1532        .maps_mut()
1533        .any(|map| map.name() == OsStr::new("rust_ops_cgroup"))
1534    {
1535        cgroup_ops()?
1536    } else {
1537        None
1538    };
1539    let ops = select_ops(name, &mut open, bytes, cgroup, dump_len)?;
1540    if options.timeout_ms != 0 {
1541        set_ops_timeout(name, &mut open, bytes, ops, options.timeout_ms)?;
1542    }
1543    let declared = ops_flags(name, &mut open, bytes, ops)?;
1544    size_maps(&mut open)?;
1545    let mut config = config;
1546    let mut params = Params::new(&mut open, declared);
1547    params.set_options::<S>(matches)?;
1548    // A scheduler that counts for its statistics only when they are asked
1549    // for declares a `STATS` tunable: it is true with --stats.
1550    if params.type_of("stats").as_deref() == Some("bool") {
1551        params.set("stats", &[options.stats.is_some()])?;
1552    }
1553    scheduler.settings(&mut params)?;
1554    let flags = params.flags();
1555    params.check()?;
1556    set_ops_flags(name, &mut open, bytes, ops, flags)?;
1557    config[config::OPS_FLAGS] = flags;
1558    set_config(&mut open, &config)?;
1559    let mut object = open.load().context("load Rust BPF object")?;
1560    if options.verify {
1561        println!("Rust BPF object verified successfully");
1562        return Ok(());
1563    }
1564    // The CPUs' capacities and packing ranks, for a scheduler that reads
1565    // them at init. One that does not is not held back by a machine they
1566    // cannot be read on.
1567    let tiers = publish_tiers(&mut object, &scheduler.capacity())
1568        .inspect_err(|error| warn!("CPU capacities and ranks not read: {error:#}"))
1569        .ok();
1570    let link = attach(&mut object, ops)?;
1571    let object = Loaded(Arc::new(object));
1572    scheduler.attached(&Attached {
1573        name,
1574        version: options.version,
1575        object: &object,
1576        flags,
1577        runtime: scheduler.runtime(),
1578        tiers: tiers.as_ref(),
1579        topology: std::cell::OnceCell::new(),
1580    });
1581    let start = Instant::now();
1582    // The stats server answers clients from this loop, --stats among them.
1583    let (mut serve, mut monitor) = match scheduler.stats() {
1584        Some(stats) => {
1585            let serve = (stats.serve)(stats.idle)?;
1586            let monitor = options
1587                .stats
1588                .map(|interval| spawn_monitor(stats.monitor, interval, shutdown.clone()));
1589            (Some(serve), monitor)
1590        }
1591        None => (None, None),
1592    };
1593    while !shutdown.load(Ordering::Relaxed)
1594        && options
1595            .duration
1596            .is_none_or(|seconds| start.elapsed() < Duration::from_secs(seconds))
1597        && exit_report(&object, dump_len)?.is_none()
1598    {
1599        match serve.as_mut() {
1600            Some(serve) => serve(&object, Duration::from_millis(100))?,
1601            None => std::thread::sleep(Duration::from_millis(100)),
1602        }
1603    }
1604    shutdown.store(true, Ordering::Relaxed);
1605    if let Some(monitor) = monitor.take() {
1606        let _ = monitor.join();
1607    }
1608    drop(serve);
1609    drop(link);
1610    // The counters are printed with -v, and whenever they say the run was
1611    // not clean, which fails it.
1612    let counters = arena_stats(&object)?;
1613    let clean = check_arena_stats(&counters);
1614    if options.verbose || clean.is_err() {
1615        info!(
1616            "arena: allocated={} freed={} errors={} failures={} pages={}",
1617            counters[0], counters[1], counters[2], counters[3], counters[4]
1618        );
1619        info!("idle: cas_failures={}", counters[5]);
1620    }
1621    clean?;
1622    // A graceful unregister is one line; an error exit keeps the standard
1623    // report, with the debug dump.
1624    if let Some(why) = exit_summary(&object)? {
1625        scheduler.stopped(name, &why);
1626        return Ok(());
1627    }
1628    match exit_report(&object, dump_len)? {
1629        Some(info) => info.report(),
1630        None => Ok(()),
1631    }
1632}
1633
1634/// A graceful unregister as one line, "reason" or "reason (msg)", read off
1635/// the exit record. None when the scheduler has not exited, or was kicked
1636/// out by an error, which keeps the standard report and its debug dump.
1637fn exit_summary(object: &Object) -> Result<Option<String>> {
1638    let uei = object
1639        .maps()
1640        .find(|map| map.name() == OsStr::new(".data.uei"))
1641        .context("find the exit record")?;
1642    let mut record = vec![0_u8; UEI_BYTES];
1643    ensure!(
1644        uei.lookup_into(&0_u32.to_ne_bytes(), &mut record, MapFlags::ANY)?,
1645        "read the exit record"
1646    );
1647    let kind = i32::from_ne_bytes(record[0..4].try_into()?);
1648    if kind == 0 || kind > EXIT_UNREG_KERN {
1649        return Ok(None);
1650    }
1651    let text = |bytes: &[u8]| {
1652        let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
1653        String::from_utf8_lossy(&bytes[..end]).into_owned()
1654    };
1655    let reason = text(&record[16..16 + 128]);
1656    let msg = text(&record[16 + 128..]);
1657    Ok(Some(match (reason.is_empty(), msg.is_empty()) {
1658        (true, _) => "unknown reason".to_string(),
1659        (false, true) => reason,
1660        (false, false) => format!("{reason} ({msg})"),
1661    }))
1662}
1663
1664/// The options every scheduler takes, beside its own.
1665#[derive(clap::Parser)]
1666struct Cli {
1667    #[arg(short = 'v', long, help = "Enable libbpf debug output")]
1668    verbose: bool,
1669    #[arg(short = 'V', long, help = "Print the scheduler version and exit")]
1670    version: bool,
1671    #[arg(long, help = "Load the BPF object and exit")]
1672    verify: bool,
1673    #[arg(long, help = "Run for this many seconds")]
1674    duration: Option<u64>,
1675    #[arg(
1676        long,
1677        default_value_t = 0,
1678        help = "Exit debug dump buffer length. 0 indicates default"
1679    )]
1680    exit_dump_len: u32,
1681    #[arg(
1682        long,
1683        default_value_t = 0,
1684        help = "Watchdog timeout in milliseconds, at most 30000. 0 keeps the scheduler's own"
1685    )]
1686    timeout_ms: u32,
1687    #[arg(long, help = "Enable stats monitoring with the specified interval")]
1688    stats: Option<f64>,
1689    #[arg(
1690        long,
1691        help = "Run in stats monitoring mode with the specified interval. Scheduler is not launched"
1692    )]
1693    monitor: Option<f64>,
1694    #[arg(long, help = "Show descriptions for statistics")]
1695    help_stats: bool,
1696    #[clap(flatten, next_help_heading = "Libbpf Options")]
1697    libbpf: LibbpfOpts,
1698}
1699
1700/// The options of a scheduler with no tunables.
1701#[derive(clap::Args)]
1702pub struct NoOpts {}
1703
1704impl Scheduler for NoOpts {}
1705
1706/// The tunable in nanoseconds that an option in microseconds or
1707/// milliseconds sets, and the factor: `slice_us` sets `slice_ns`, 1000.
1708fn time_unit(option: &str) -> Option<(String, i128)> {
1709    [("_us", 1_000), ("_ms", 1_000_000)]
1710        .into_iter()
1711        .find_map(|(suffix, factor)| Some((format!("{}_ns", option.strip_suffix(suffix)?), factor)))
1712}
1713
1714/// The options of `A` with their values as given or defaulted, by name.
1715fn named<A: clap::Args>(matches: &clap::ArgMatches) -> Vec<(String, String)> {
1716    A::augment_args(clap::Command::new(""))
1717        .get_arguments()
1718        .filter_map(|arg| {
1719            let name = arg.get_id().as_str();
1720            let value = matches.get_raw(name)?.next()?;
1721            Some((name.to_owned(), value.to_string_lossy().into_owned()))
1722        })
1723        .collect()
1724}
1725
1726/// Log at info level to stderr, each line with the local time and its
1727/// level, as the C-form schedulers do.
1728fn init_logger() -> Result<()> {
1729    let mut config = simplelog::ConfigBuilder::new();
1730    // The local offset cannot be read safely once there is more than one
1731    // thread, and the time is then UTC.
1732    let _ = config.set_time_offset_to_local();
1733    config
1734        .set_time_level(simplelog::LevelFilter::Error)
1735        .set_location_level(simplelog::LevelFilter::Off)
1736        .set_target_level(simplelog::LevelFilter::Off)
1737        .set_thread_level(simplelog::LevelFilter::Off);
1738    simplelog::TermLogger::init(
1739        simplelog::LevelFilter::Info,
1740        config.build(),
1741        simplelog::TerminalMode::Stderr,
1742        simplelog::ColorChoice::Auto,
1743    )?;
1744    Ok(())
1745}
1746
1747/// The whole loader of a scheduler: parse the command line, then run()
1748/// until Ctrl-C. `main!` calls this with the scheduler's name, version and
1749/// objects, and with its options as `S`.
1750pub fn cli<S: Scheduler>(
1751    name: &'static str,
1752    version: &'static str,
1753    about: &'static str,
1754    legacy: &[u8],
1755    sized: &[u8],
1756) -> Result<()> {
1757    use clap::{Args, FromArgMatches};
1758
1759    let command = Cli::augment_args(S::augment_args(clap::Command::new(name)));
1760    let matches = command.about(about).get_matches();
1761    let options = Cli::from_arg_matches(&matches)?;
1762    let scheduler = S::from_arg_matches(&matches)?;
1763    if options.version {
1764        println!("{name} {}", scx_utils::build_id::full_version(version));
1765        return Ok(());
1766    }
1767    init_logger()?;
1768    let shutdown = Arc::new(AtomicBool::new(false));
1769    let signal = shutdown.clone();
1770    ctrlc::set_handler(move || signal.store(true, Ordering::Relaxed))?;
1771    if options.help_stats || options.monitor.is_some() || options.stats.is_some() {
1772        let stats = scheduler
1773            .stats()
1774            .with_context(|| format!("{name} has no statistics"))?;
1775        if options.help_stats {
1776            return (stats.describe)();
1777        }
1778        if let Some(interval) = options.monitor {
1779            return (stats.monitor)(Duration::from_secs_f64(interval), shutdown);
1780        }
1781    }
1782    run(
1783        name,
1784        legacy,
1785        sized,
1786        &RunOptions {
1787            libbpf: options.libbpf,
1788            verbose: options.verbose,
1789            verify: options.verify,
1790            duration: options.duration,
1791            exit_dump_len: options.exit_dump_len,
1792            timeout_ms: options.timeout_ms,
1793            stats: options.stats,
1794            version,
1795        },
1796        &shutdown,
1797        &matches,
1798        &scheduler,
1799    )
1800}
1801
1802/// The `main()` of a scheduler: the objects its build script left in
1803/// OUT_DIR, loaded and run by cli(). The scheduler's options are a
1804/// `clap::Args` structure, each field named as the tunable it sets, a
1805/// time in microseconds as the tunable in nanoseconds, and what else the scheduler adds to the loader is its `Scheduler`
1806/// implementation:
1807///
1808/// ```ignore
1809/// #[derive(clap::Args)]
1810/// struct Opts {
1811///     /// Scheduling slice duration in microseconds.
1812///     #[clap(short = 's', long, default_value = "5000")]
1813///     slice_us: u64,
1814/// }
1815///
1816/// impl scx_rustsched_loader::Scheduler for Opts {}
1817///
1818/// scx_rustsched_loader::main!("A minimal sched_ext scheduler written in Rust BPF", Opts);
1819/// ```
1820///
1821/// An option that is not the tunable's value as it stands, such as a
1822/// list of CPUs that the scheduler takes as a mask, has a name of its
1823/// own, and `Scheduler::settings()` sets the tunable from it:
1824///
1825/// ```ignore
1826/// impl scx_rustsched_loader::Scheduler for Opts {
1827///     fn settings(
1828///         &self,
1829///         params: &mut scx_rustsched_loader::Params,
1830///     ) -> scx_rustsched_loader::Result<()> {
1831///         params.set("primary_cpus", &mask_of(&self.primary_domain)?)
1832///     }
1833/// }
1834/// ```
1835///
1836/// Every tunable of the scheduler has to be set by one or the other.
1837#[macro_export]
1838macro_rules! main {
1839    ($about:expr) => {
1840        $crate::main!($about, $crate::NoOpts);
1841    };
1842    ($about:expr, $opts:ty) => {
1843        fn main() -> $crate::Result<()> {
1844            $crate::cli::<$opts>(
1845                env!("CARGO_PKG_NAME"),
1846                env!("CARGO_PKG_VERSION"),
1847                $about,
1848                include_bytes!(concat!(env!("OUT_DIR"), "/scheduler-legacy.bpf.o")),
1849                include_bytes!(concat!(env!("OUT_DIR"), "/scheduler-sized.bpf.o")),
1850            )
1851        }
1852    };
1853}
1854
1855#[cfg(test)]
1856mod tests {
1857    use super::*;
1858
1859    #[test]
1860    fn param_sections() {
1861        let of = |section: &str| param_of(OsStr::new(section));
1862        assert_eq!(
1863            of(".rodata.param.SLICE_NS.u64"),
1864            Some(("slice_ns".into(), "u64".into()))
1865        );
1866        assert_eq!(
1867            of(".rodata.param.STEAL.bool"),
1868            Some(("steal".into(), "bool".into()))
1869        );
1870        assert_eq!(of(".rodata.config"), None);
1871        assert_eq!(of(".rodata.param.NOTYPE"), None);
1872        assert_eq!(param_name("Slice-NS"), "slice_ns");
1873    }
1874
1875    #[test]
1876    fn params_from_options() {
1877        use clap::{Args, FromArgMatches};
1878
1879        #[derive(clap::Args)]
1880        struct Opts {
1881            #[clap(short = 's', long, default_value = "5000000")]
1882            slice_ns: u64,
1883            #[clap(long, action = clap::ArgAction::Set, default_value = "true")]
1884            steal: bool,
1885            #[clap(long)]
1886            fast: bool,
1887        }
1888
1889        let parse = |line: &[&str]| {
1890            let command = Cli::augment_args(Opts::augment_args(clap::Command::new("test")));
1891            let matches = command.get_matches_from(line);
1892            assert!(Cli::from_arg_matches(&matches).is_ok());
1893            named::<Opts>(&matches)
1894                .iter()
1895                .map(|(name, value)| format!("{name}={value}"))
1896                .collect::<Vec<_>>()
1897        };
1898        assert_eq!(
1899            parse(&["test"]),
1900            ["slice_ns=5000000", "steal=true", "fast=false"]
1901        );
1902        assert_eq!(
1903            parse(&["test", "-s", "1000", "--steal", "false", "--fast", "-v"]),
1904            ["slice_ns=1000", "steal=false", "fast=true"]
1905        );
1906    }
1907
1908    #[test]
1909    fn param_arrays() {
1910        assert_eq!(
1911            param_of(OsStr::new(".rodata.param.PRIMARY_CPUS.[u64]")),
1912            Some(("primary_cpus".into(), "[u64]".into()))
1913        );
1914        assert_eq!(param_element("[u64]"), Some("u64"));
1915        assert_eq!(param_element("u64"), None);
1916        let mut bytes = Vec::new();
1917        [1_u16, 0x0203]
1918            .iter()
1919            .for_each(|value| value.append(&mut bytes));
1920        true.append(&mut bytes);
1921        assert_eq!(bytes.len(), 5);
1922        assert_eq!(bytes[..2], 1_u16.to_ne_bytes());
1923        assert_eq!(bytes[4], 1);
1924        assert_eq!(<i32 as ParamValue>::TYPE, "i32");
1925    }
1926
1927    #[test]
1928    fn param_values() {
1929        assert_eq!(param_parse("u64", 8, "5000000"), Some(5_000_000));
1930        assert_eq!(param_parse("u64", 8, "0x10"), Some(16));
1931        assert_eq!(param_parse("u64", 8, "-1"), None);
1932        assert_eq!(
1933            param_parse("u32", 4, "4294967295"),
1934            Some(u64::from(u32::MAX))
1935        );
1936        assert_eq!(param_parse("u32", 4, "4294967296"), None);
1937        assert_eq!(param_parse("u8", 1, "256"), None);
1938        assert_eq!(param_parse("i32", 4, "-1"), Some(u64::MAX));
1939        assert_eq!(param_parse("i32", 4, "2147483648"), None);
1940        assert_eq!(param_parse("i8", 1, "-128"), Some(-128_i64 as u64));
1941        assert_eq!(param_parse("i8", 1, "-129"), None);
1942        assert_eq!(param_parse("bool", 1, "true"), Some(1));
1943        assert_eq!(param_parse("bool", 1, "0"), Some(0));
1944        assert_eq!(param_parse("bool", 1, "7"), None);
1945        assert_eq!(param_parse("u64", 16, "1"), None);
1946        assert_eq!(param_parse("u64", 8, "many"), None);
1947    }
1948
1949    #[test]
1950    fn time_units() {
1951        assert_eq!(time_unit("slice_us"), Some(("slice_ns".into(), 1_000)));
1952        assert_eq!(
1953            time_unit("timeout_ms"),
1954            Some(("timeout_ns".into(), 1_000_000))
1955        );
1956        assert_eq!(time_unit("slice_ns"), None);
1957        assert_eq!(time_unit("bonus"), None);
1958    }
1959}