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    };
112
113    let input = ProgramInput {
114        context_in: Some(unsafe {
115            std::slice::from_raw_parts_mut(
116                &mut args as *mut _ as *mut u8,
117                std::mem::size_of_val(&args),
118            )
119        }),
120        ..Default::default()
121    };
122
123    let output = skel.progs.arena_init.test_run(input)?;
124    if output.return_value != 0 {
125        bail!(
126            "Could not initialize arenas, arena_init returned {}",
127            output.return_value as i32
128        );
129    }
130
131    Ok(())
132}
133
134fn setup_topology_node(skel: &mut BpfSkel<'_>, mask: &[u64]) -> Result<()> {
135    let mut args = types::arena_alloc_mask_args {
136        bitmap: 0 as c_ulong,
137    };
138
139    let input = ProgramInput {
140        context_in: Some(unsafe {
141            std::slice::from_raw_parts_mut(
142                &mut args as *mut _ as *mut u8,
143                std::mem::size_of_val(&args),
144            )
145        }),
146        ..Default::default()
147    };
148
149    let output = skel.progs.arena_alloc_mask.test_run(input)?;
150    if output.return_value != 0 {
151        bail!(
152            "Could not initialize arenas, setup_topology_node returned {}",
153            output.return_value as i32
154        );
155    }
156
157    let ptr = unsafe {
158        &mut *std::ptr::with_exposed_provenance_mut::<[u64; 10]>(args.bitmap.try_into().unwrap())
159    };
160
161    let (valid_mask, _) = ptr.split_at_mut(mask.len());
162    valid_mask.clone_from_slice(mask);
163
164    let mut args = types::arena_topology_node_init_args {
165        bitmap: args.bitmap as c_ulong,
166        data_size: 0 as c_ulong,
167        id: 0 as c_ulong,
168    };
169
170    let input = ProgramInput {
171        context_in: Some(unsafe {
172            std::slice::from_raw_parts_mut(
173                &mut args as *mut _ as *mut u8,
174                std::mem::size_of_val(&args),
175            )
176        }),
177        ..Default::default()
178    };
179
180    let output = skel.progs.arena_topology_node_init.test_run(input)?;
181    if output.return_value != 0 {
182        bail!(
183            "arena_topology_node_init returned {}",
184            output.return_value as i32
185        );
186    }
187
188    Ok(())
189}
190
191fn setup_topology(skel: &mut BpfSkel<'_>) -> Result<()> {
192    let topo = Topology::new().expect("Failed to build host topology");
193
194    setup_topology_node(skel, topo.span.as_raw_slice())?;
195
196    for (_, node) in topo.nodes {
197        setup_topology_node(skel, node.span.as_raw_slice())?;
198    }
199
200    for (_, llc) in topo.all_llcs {
201        setup_topology_node(
202            skel,
203            Arc::<Llc>::into_inner(llc)
204                .expect("missing llc")
205                .span
206                .as_raw_slice(),
207        )?;
208    }
209
210    for (_, core) in topo.all_cores {
211        setup_topology_node(
212            skel,
213            Arc::<Core>::into_inner(core)
214                .expect("missing core")
215                .span
216                .as_raw_slice(),
217        )?;
218    }
219    for (_, cpu) in topo.all_cpus {
220        let mut mask = [0; 9];
221        mask[cpu.id.checked_shr(64).unwrap_or(0)] |= 1 << (cpu.id % 64);
222        setup_topology_node(skel, &mask)?;
223    }
224
225    Ok(())
226}
227
228fn print_stream(skel: &mut BpfSkel<'_>, stream_id: u32) -> () {
229    let prog_fd = skel.progs.arena_selftest.as_fd().as_raw_fd();
230    let mut buf = vec![0u8; 4096];
231    let name = if stream_id == 1 { "OUTPUT" } else { "ERROR" };
232    let mut started = false;
233
234    loop {
235        let ret = unsafe {
236            libbpf_sys::bpf_prog_stream_read(
237                prog_fd,
238                stream_id,
239                buf.as_mut_ptr() as *mut c_void,
240                buf.len() as u32,
241                std::ptr::null_mut(),
242            )
243        };
244        if ret < 0 {
245            eprintln!("STREAM {} UNAVAILABLE (REQUIRES >= v6.17)", name);
246            return;
247        }
248
249        if !started {
250            println!("===BEGIN STREAM {}===", name);
251            started = true;
252        }
253
254        if ret == 0 {
255            break;
256        }
257
258        print!("{}", String::from_utf8_lossy(&buf[..ret as usize]));
259    }
260
261    println!("\n====END STREAM  {}====", name);
262}
263
264// Run the named test by setting selftest_run_id in the BPF bss and calling
265// arena_selftest. The ID comes from enum scx_selftest_id in selftest.h.
266fn run_test_by_name(skel: &mut BpfSkel<'_>, name: &str) -> Result<i32> {
267    let id = TEST_CASES
268        .iter()
269        .find(|(n, _)| *n == name)
270        .map(|(_, id)| *id)
271        .ok_or_else(|| {
272            anyhow::anyhow!(
273                "Unknown test: '{}'. Use --list to see available tests.",
274                name
275            )
276        })?;
277
278    skel.maps.bss_data.as_mut().unwrap().selftest_run_id = id;
279
280    let input = ProgramInput {
281        ..Default::default()
282    };
283    let output = skel.progs.arena_selftest.test_run(input)?;
284
285    Ok(output.return_value as i32)
286}
287
288fn main() {
289    TermLogger::init(
290        simplelog::LevelFilter::Info,
291        SimplelogConfig::default(),
292        TerminalMode::Mixed,
293        ColorChoice::Auto,
294    )
295    .unwrap();
296
297    let opts = Opts::parse();
298
299    if opts.list {
300        println!("Available test cases:\n{}", available_tests());
301        return;
302    }
303
304    // Validate test names before loading BPF.
305    for name in &opts.tests {
306        if !TEST_CASES.iter().any(|(n, _)| *n == name.as_str()) {
307            eprintln!(
308                "Unknown test: '{}'.\nAvailable tests:\n{}",
309                name,
310                available_tests()
311            );
312            std::process::exit(1);
313        }
314    }
315
316    let mut open_object = MaybeUninit::uninit();
317    let mut builder = BpfSkelBuilder::default();
318
319    builder.obj_builder.debug(true);
320    init_libbpf_logging(Some(PrintLevel::Debug));
321
322    let mut skel = builder
323        .open(&mut open_object)
324        .context("Failed to open BPF program")
325        .unwrap();
326
327    skel.maps.rodata_data.as_mut().unwrap().nr_cpu_ids = *NR_CPU_IDS as u32;
328
329    let mut skel = skel.load().context("Failed to load BPF program").unwrap();
330
331    setup_arenas(&mut skel).unwrap();
332    setup_topology(&mut skel).unwrap();
333
334    let to_run: Vec<&str> = if opts.tests.is_empty() {
335        TEST_CASES.iter().map(|(n, _)| *n).collect()
336    } else {
337        opts.tests.iter().map(String::as_str).collect()
338    };
339
340    let stdout_tty = std::io::stdout().is_terminal();
341    let stderr_tty = std::io::stderr().is_terminal();
342    let pass_label = colorize("[ PASS ]", COLOR_BRIGHT_GREEN, stdout_tty);
343    let fail_label = colorize("[ FAIL ]", COLOR_BRIGHT_RED, stderr_tty);
344
345    let mut any_failed = false;
346    for &name in &to_run {
347        match run_test_by_name(&mut skel, name) {
348            Ok(0) => println!("{} {}", pass_label, name),
349            Ok(ret) => {
350                eprintln!("{} {} (returned {})", fail_label, name, ret);
351                any_failed = true;
352            }
353            Err(e) => {
354                eprintln!("{} {} (error: {})", fail_label, name, e);
355                any_failed = true;
356            }
357        }
358
359        print_stream(&mut skel, BPF_STDOUT);
360        print_stream(&mut skel, BPF_STDERR);
361    }
362
363    if any_failed {
364        eprintln!(
365            "{}",
366            colorize(
367                "One or more selftests failed.",
368                COLOR_BRIGHT_RED,
369                stderr_tty
370            )
371        );
372        std::process::exit(1);
373    } else {
374        println!(
375            "{}",
376            colorize("All selftests passed.", COLOR_BRIGHT_GREEN, stdout_tty)
377        );
378    }
379}