Skip to main content

scx_arena_selftests/
main.rs

1// Copyright (c) Meta Platforms, Inc. and affiliates.
2
3// This software may be used and distributed according to the terms of the
4// GNU General Public License version 2.
5mod bpf_skel;
6pub use bpf_skel::*;
7
8use std::mem::MaybeUninit;
9
10use anyhow::bail;
11use anyhow::Context;
12use anyhow::Result;
13
14use std::ffi::c_ulong;
15use std::ffi::c_void;
16use std::io::IsTerminal;
17
18use std::os::fd::AsFd;
19use std::os::fd::AsRawFd;
20use std::sync::Arc;
21
22use clap::Parser;
23
24use scx_utils::init_libbpf_logging;
25use scx_utils::Core;
26use scx_utils::Llc;
27use scx_utils::Topology;
28use scx_utils::NR_CPU_IDS;
29
30use simplelog::{ColorChoice, Config as SimplelogConfig, TermLogger, TerminalMode};
31
32use libbpf_rs::libbpf_sys;
33
34use libbpf_rs::skel::OpenSkel;
35use libbpf_rs::skel::SkelBuilder;
36use libbpf_rs::PrintLevel;
37use libbpf_rs::ProgramInput;
38
39const BPF_STDOUT: u32 = 1;
40const BPF_STDERR: u32 = 2;
41
42const COLOR_BRIGHT_GREEN: &str = "\x1b[92m";
43const COLOR_BRIGHT_RED: &str = "\x1b[91m";
44const COLOR_RESET: &str = "\x1b[0m";
45
46fn colorize(text: &str, color: &str, is_tty: bool) -> String {
47    if is_tty {
48        format!("{}{}{}", color, text, COLOR_RESET)
49    } else {
50        text.to_string()
51    }
52}
53
54// Mirrors enum scx_selftest_id in lib/selftests/selftest.h.
55// SCX_SELFTEST_ID_ALL (0) is reserved for "run all" and is not listed in
56// TEST_CASES; only the named per-test IDs appear there.
57#[repr(u32)]
58#[allow(non_camel_case_types)]
59enum SelfTestId {
60    #[allow(dead_code)]
61    SCX_SELFTEST_ID_ALL = 0,
62    SCX_SELFTEST_ID_ATQ = 1,
63    SCX_SELFTEST_ID_BTREE = 2,
64    SCX_SELFTEST_ID_LVQUEUE = 3,
65    SCX_SELFTEST_ID_MINHEAP = 4,
66    SCX_SELFTEST_ID_RBTREE = 5,
67    SCX_SELFTEST_ID_TOPOLOGY = 6,
68}
69
70fn available_tests() -> String {
71    TEST_CASES
72        .iter()
73        .map(|(name, _)| format!("  {}", name))
74        .collect::<Vec<_>>()
75        .join("\n")
76}
77
78const TEST_CASES: &[(&str, u32)] = &[
79    ("atq", SelfTestId::SCX_SELFTEST_ID_ATQ as u32),
80    ("btree", SelfTestId::SCX_SELFTEST_ID_BTREE as u32),
81    ("lvqueue", SelfTestId::SCX_SELFTEST_ID_LVQUEUE as u32),
82    ("minheap", SelfTestId::SCX_SELFTEST_ID_MINHEAP as u32),
83    ("rbtree", SelfTestId::SCX_SELFTEST_ID_RBTREE as u32),
84    ("topology", SelfTestId::SCX_SELFTEST_ID_TOPOLOGY as u32),
85];
86
87#[derive(Debug, Parser)]
88#[clap(about = "scx_arena library selftests")]
89struct Opts {
90    /// List all available test cases and exit.
91    #[clap(long)]
92    list: bool,
93
94    /// Run one or more specific test cases. Multiple names can be given after a
95    /// single --test flag (e.g. --test rbtree atq), or the flag can be repeated.
96    /// If not specified, all tests are run.
97    #[clap(long = "test", value_name = "NAME", num_args(1..))]
98    tests: Vec<String>,
99}
100
101fn setup_arenas(skel: &mut BpfSkel<'_>) -> Result<()> {
102    const STATIC_ALLOC_PAGES_GRANULARITY: c_ulong = 512;
103    const TASK_SIZE: c_ulong = 42;
104
105    // Allocate the arena memory from the BPF side so userspace initializes it before starting
106    // the scheduler. Despite the function call's name this is neither a test nor a test run,
107    // it's the recommended way of executing SEC("syscall") probes.
108    let mut args = types::arena_init_args {
109        static_pages: STATIC_ALLOC_PAGES_GRANULARITY,
110        task_ctx_size: TASK_SIZE,
111        task_ctx_align: 0,
112    };
113
114    let input = ProgramInput {
115        context_in: Some(unsafe {
116            std::slice::from_raw_parts_mut(
117                &mut args as *mut _ as *mut u8,
118                std::mem::size_of_val(&args),
119            )
120        }),
121        ..Default::default()
122    };
123
124    let output = skel.progs.arena_init.test_run(input)?;
125    if output.return_value != 0 {
126        bail!(
127            "Could not initialize arenas, arena_init returned {}",
128            output.return_value as i32
129        );
130    }
131
132    Ok(())
133}
134
135fn setup_topology_node(skel: &mut BpfSkel<'_>, mask: &[u64]) -> Result<()> {
136    let mut args = types::arena_alloc_mask_args {
137        bitmap: 0 as c_ulong,
138    };
139
140    let input = ProgramInput {
141        context_in: Some(unsafe {
142            std::slice::from_raw_parts_mut(
143                &mut args as *mut _ as *mut u8,
144                std::mem::size_of_val(&args),
145            )
146        }),
147        ..Default::default()
148    };
149
150    let output = skel.progs.arena_alloc_mask.test_run(input)?;
151    if output.return_value != 0 {
152        bail!(
153            "Could not initialize arenas, setup_topology_node returned {}",
154            output.return_value as i32
155        );
156    }
157
158    let ptr = unsafe {
159        &mut *std::ptr::with_exposed_provenance_mut::<[u64; 10]>(args.bitmap.try_into().unwrap())
160    };
161
162    let (valid_mask, _) = ptr.split_at_mut(mask.len());
163    valid_mask.clone_from_slice(mask);
164
165    let mut args = types::arena_topology_node_init_args {
166        bitmap: args.bitmap as c_ulong,
167        data_size: 0 as c_ulong,
168        id: 0 as c_ulong,
169    };
170
171    let input = ProgramInput {
172        context_in: Some(unsafe {
173            std::slice::from_raw_parts_mut(
174                &mut args as *mut _ as *mut u8,
175                std::mem::size_of_val(&args),
176            )
177        }),
178        ..Default::default()
179    };
180
181    let output = skel.progs.arena_topology_node_init.test_run(input)?;
182    if output.return_value != 0 {
183        bail!(
184            "arena_topology_node_init returned {}",
185            output.return_value as i32
186        );
187    }
188
189    Ok(())
190}
191
192fn setup_topology(skel: &mut BpfSkel<'_>) -> Result<()> {
193    let topo = Topology::new().expect("Failed to build host topology");
194
195    // Set per-level max children before registering any topology nodes.
196    // NOTE: rust/scx_arena/scx_arena/src/arenalib.rs::setup_topology_max_children()
197    // contains equivalent logic and must be kept in sync with this block.
198    let max_children: [u32; 5] = [
199        topo.nodes.len() as u32,
200        topo.nodes.values().map(|n| n.llcs.len()).max().unwrap_or(0) as u32,
201        topo.all_llcs
202            .values()
203            .map(|l| l.cores.len())
204            .max()
205            .unwrap_or(0) as u32,
206        topo.all_cores
207            .values()
208            .map(|c| c.cpus.len())
209            .max()
210            .unwrap_or(0) as u32,
211        0,
212    ];
213    let mut init_args = types::arena_topology_init_args { max_children };
214    let init_input = ProgramInput {
215        context_in: Some(unsafe {
216            std::slice::from_raw_parts_mut(
217                &mut init_args as *mut _ as *mut u8,
218                std::mem::size_of_val(&init_args),
219            )
220        }),
221        ..Default::default()
222    };
223    let output = skel.progs.arena_topology_init.test_run(init_input)?;
224    if output.return_value != 0 {
225        bail!(
226            "arena_topology_init returned {}",
227            output.return_value as i32
228        );
229    }
230
231    setup_topology_node(skel, topo.span.as_raw_slice())?;
232
233    for (_, node) in topo.nodes {
234        setup_topology_node(skel, node.span.as_raw_slice())?;
235    }
236
237    for (_, llc) in topo.all_llcs {
238        setup_topology_node(
239            skel,
240            Arc::<Llc>::into_inner(llc)
241                .expect("missing llc")
242                .span
243                .as_raw_slice(),
244        )?;
245    }
246
247    for (_, core) in topo.all_cores {
248        setup_topology_node(
249            skel,
250            Arc::<Core>::into_inner(core)
251                .expect("missing core")
252                .span
253                .as_raw_slice(),
254        )?;
255    }
256    for (_, cpu) in topo.all_cpus {
257        let mut mask = [0; 9];
258        mask[cpu.id / 64] |= 1 << (cpu.id % 64);
259        setup_topology_node(skel, &mask)?;
260    }
261
262    Ok(())
263}
264
265fn print_stream(skel: &mut BpfSkel<'_>, stream_id: u32) -> () {
266    let prog_fd = skel.progs.arena_selftest.as_fd().as_raw_fd();
267    let mut buf = vec![0u8; 4096];
268    let name = if stream_id == 1 { "OUTPUT" } else { "ERROR" };
269    let mut started = false;
270
271    loop {
272        let ret = unsafe {
273            libbpf_sys::bpf_prog_stream_read(
274                prog_fd,
275                stream_id,
276                buf.as_mut_ptr() as *mut c_void,
277                buf.len() as u32,
278                std::ptr::null_mut(),
279            )
280        };
281        if ret < 0 {
282            eprintln!("STREAM {} UNAVAILABLE (REQUIRES >= v6.17)", name);
283            return;
284        }
285
286        if !started {
287            println!("===BEGIN STREAM {}===", name);
288            started = true;
289        }
290
291        if ret == 0 {
292            break;
293        }
294
295        print!("{}", String::from_utf8_lossy(&buf[..ret as usize]));
296    }
297
298    println!("\n====END STREAM  {}====", name);
299}
300
301// Run the named test by setting selftest_run_id in the BPF bss and calling
302// arena_selftest. The ID comes from enum scx_selftest_id in selftest.h.
303fn run_test_by_name(skel: &mut BpfSkel<'_>, name: &str) -> Result<i32> {
304    let id = TEST_CASES
305        .iter()
306        .find(|(n, _)| *n == name)
307        .map(|(_, id)| *id)
308        .ok_or_else(|| {
309            anyhow::anyhow!(
310                "Unknown test: '{}'. Use --list to see available tests.",
311                name
312            )
313        })?;
314
315    skel.maps.bss_data.as_mut().unwrap().selftest_run_id = id;
316
317    let input = ProgramInput {
318        ..Default::default()
319    };
320    let output = skel.progs.arena_selftest.test_run(input)?;
321
322    Ok(output.return_value as i32)
323}
324
325fn main() {
326    TermLogger::init(
327        simplelog::LevelFilter::Info,
328        SimplelogConfig::default(),
329        TerminalMode::Mixed,
330        ColorChoice::Auto,
331    )
332    .unwrap();
333
334    let opts = Opts::parse();
335
336    if opts.list {
337        println!("Available test cases:\n{}", available_tests());
338        return;
339    }
340
341    // Validate test names before loading BPF.
342    for name in &opts.tests {
343        if !TEST_CASES.iter().any(|(n, _)| *n == name.as_str()) {
344            eprintln!(
345                "Unknown test: '{}'.\nAvailable tests:\n{}",
346                name,
347                available_tests()
348            );
349            std::process::exit(1);
350        }
351    }
352
353    let mut open_object = MaybeUninit::uninit();
354    let mut builder = BpfSkelBuilder::default();
355
356    builder.obj_builder.debug(true);
357    init_libbpf_logging(Some(PrintLevel::Debug));
358
359    let mut skel = builder
360        .open(&mut open_object)
361        .context("Failed to open BPF program")
362        .unwrap();
363
364    skel.maps.rodata_data.as_mut().unwrap().nr_cpu_ids = *NR_CPU_IDS as u32;
365
366    let mut skel = skel.load().context("Failed to load BPF program").unwrap();
367
368    setup_arenas(&mut skel).unwrap();
369    setup_topology(&mut skel).unwrap();
370
371    let to_run: Vec<&str> = if opts.tests.is_empty() {
372        TEST_CASES.iter().map(|(n, _)| *n).collect()
373    } else {
374        opts.tests.iter().map(String::as_str).collect()
375    };
376
377    let stdout_tty = std::io::stdout().is_terminal();
378    let stderr_tty = std::io::stderr().is_terminal();
379    let pass_label = colorize("[ PASS ]", COLOR_BRIGHT_GREEN, stdout_tty);
380    let fail_label = colorize("[ FAIL ]", COLOR_BRIGHT_RED, stderr_tty);
381
382    let mut any_failed = false;
383    for &name in &to_run {
384        match run_test_by_name(&mut skel, name) {
385            Ok(0) => println!("{} {}", pass_label, name),
386            Ok(ret) => {
387                eprintln!("{} {} (returned {})", fail_label, name, ret);
388                any_failed = true;
389            }
390            Err(e) => {
391                eprintln!("{} {} (error: {})", fail_label, name, e);
392                any_failed = true;
393            }
394        }
395
396        print_stream(&mut skel, BPF_STDOUT);
397        print_stream(&mut skel, BPF_STDERR);
398    }
399
400    if any_failed {
401        eprintln!(
402            "{}",
403            colorize(
404                "One or more selftests failed.",
405                COLOR_BRIGHT_RED,
406                stderr_tty
407            )
408        );
409        std::process::exit(1);
410    } else {
411        println!(
412            "{}",
413            colorize("All selftests passed.", COLOR_BRIGHT_GREEN, stdout_tty)
414        );
415    }
416}