Skip to main content

scx_arena/
scx_libarena.rs

1// SPDX-License-Identifier: GPL-2.0
2//
3// Copyright (c) 2025 Meta Platforms
4// Author: Emil Tsalapatis <etsal@meta.com>
5
6// This software may be used and distributed according to the terms of the
7// GNU General Public License version 2.
8
9use scx_utils::Topology;
10use scx_utils::{Core, Llc};
11
12use std::ffi::CString;
13use std::os::raw::c_ulong;
14use std::sync::Arc;
15
16use anyhow::Result;
17use anyhow::bail;
18
19use libbpf_rs::AsRawLibbpf;
20use libbpf_rs::Object;
21use libbpf_rs::ProgramInput;
22use libbpf_rs::ProgramMut;
23use libbpf_rs::libbpf_sys;
24
25mod types {
26    use std::os::raw::c_ulong;
27
28    #[repr(C)]
29    pub(super) struct ArenaInitArgs {
30        pub task_ctx_size: c_ulong,
31        pub task_ctx_align: c_ulong,
32    }
33
34    #[repr(C)]
35    pub(super) struct ArenaAllocMaskArgs {
36        pub bitmap: c_ulong,
37    }
38
39    #[repr(C)]
40    pub(super) struct ArenaTopologyNodeInitArgs {
41        pub bitmap: c_ulong,
42        pub data_size: c_ulong,
43        pub id: c_ulong,
44    }
45
46    #[repr(C)]
47    pub(super) struct ArenaTopologyInitArgs {
48        pub max_children: [u32; 5],
49    }
50}
51
52/// Live BPF arena library state. Returned by setup() and must be kept alive
53/// for as long as the scheduler instance uses the arena: dropping it stops
54/// and joins the library's background threads.
55#[must_use]
56#[derive(Debug)]
57pub struct ScxLibArena {
58    _watcher: crate::Daemon,
59    _urcu: Option<crate::Daemon>,
60}
61
62impl ScxLibArena {
63    /// Number of u64 words needed to hold a mask of @nr_cpus bits.
64    fn nr_cpumask_words(nr_cpus: usize) -> usize {
65        nr_cpus.div_ceil(64)
66    }
67
68    fn run_prog_by_name(obj: &Object, name: &str, input: ProgramInput) -> Result<i32> {
69        let c_name = CString::new(name)?;
70        let ptr = unsafe {
71            libbpf_sys::bpf_object__find_program_by_name(
72                obj.as_libbpf_object().as_ptr(),
73                c_name.as_ptr(),
74            )
75        };
76        if ptr as u64 == 0_u64 {
77            bail!("No program with name {} found in object", name);
78        }
79
80        let bpfprog = unsafe { &mut *ptr };
81        let prog = ProgramMut::new_mut(bpfprog);
82
83        let output = prog.test_run(input)?;
84
85        // Reach into the object and get the fd of the program
86        // Get the fd of the test program to run
87
88        Ok(output.return_value as i32)
89    }
90
91    /// Set up basic library state.
92    fn setup_arena(obj: &Object, task_size: usize, task_align: usize) -> Result<()> {
93        let input = ProgramInput {
94            context_in: None,
95            ..Default::default()
96        };
97        let ret = Self::run_prog_by_name(obj, "arena_buddy_reset", input)?;
98        if ret != 0 {
99            bail!("Could not initialize libarena buddy allocator: {}", ret);
100        }
101
102        // Allocate the arena memory from the BPF side so userspace initializes it before starting
103        // the scheduler. Despite the function call's name this is neither a test nor a test run,
104        // it's the recommended way of executing SEC("syscall") probes.
105        let mut args = types::ArenaInitArgs {
106            task_ctx_size: task_size as c_ulong,
107            task_ctx_align: task_align as c_ulong,
108        };
109
110        let input = ProgramInput {
111            context_in: Some(unsafe {
112                std::slice::from_raw_parts_mut(
113                    &mut args as *mut _ as *mut u8,
114                    std::mem::size_of_val(&args),
115                )
116            }),
117            ..Default::default()
118        };
119
120        let ret = Self::run_prog_by_name(obj, "arena_init", input)?;
121        if ret != 0 {
122            bail!("Could not initialize arenas, setup_arenas returned {}", ret);
123        }
124
125        Ok(())
126    }
127
128    fn setup_topology_node(obj: &Object, nr_cpus: usize, mask: &[u64], id: usize) -> Result<()> {
129        let nr_words = Self::nr_cpumask_words(nr_cpus);
130        if mask.len() < nr_words {
131            bail!(
132                "CPU mask has {} words, expected at least {}",
133                mask.len(),
134                nr_words
135            );
136        }
137        let mask = &mask[..nr_words];
138
139        let mut args = types::ArenaAllocMaskArgs {
140            bitmap: 0 as c_ulong,
141        };
142
143        // Exclude memory-only NUMA nodes
144        if mask.iter().all(|&b| b == 0) {
145            return Ok(());
146        }
147
148        let input = ProgramInput {
149            context_in: Some(unsafe {
150                std::slice::from_raw_parts_mut(
151                    &mut args as *mut _ as *mut u8,
152                    std::mem::size_of_val(&args),
153                )
154            }),
155            ..Default::default()
156        };
157
158        let ret = Self::run_prog_by_name(obj, "arena_alloc_mask", input)?;
159
160        if ret != 0 {
161            bail!(
162                "Could not initialize arenas, setup_topology_node returned {}",
163                ret
164            );
165        }
166
167        let valid_mask = unsafe {
168            std::slice::from_raw_parts_mut(
169                std::ptr::with_exposed_provenance_mut::<u64>(args.bitmap.try_into().unwrap()),
170                nr_words,
171            )
172        };
173        valid_mask.copy_from_slice(mask);
174
175        let mut args = types::ArenaTopologyNodeInitArgs {
176            bitmap: args.bitmap as c_ulong,
177            data_size: 0 as c_ulong,
178            id: id as c_ulong,
179        };
180
181        let input = ProgramInput {
182            context_in: Some(unsafe {
183                std::slice::from_raw_parts_mut(
184                    &mut args as *mut _ as *mut u8,
185                    std::mem::size_of_val(&args),
186                )
187            }),
188            ..Default::default()
189        };
190
191        let ret = Self::run_prog_by_name(obj, "arena_topology_node_init", input)?;
192        if ret != 0 {
193            bail!("arena_topology_node_init returned {}", ret);
194        }
195
196        Ok(())
197    }
198
199    /// Set the per-level maximum number of children before registering topology
200    /// nodes. Each topology node at level L is allocated with
201    /// topo_max_children[L] child pointer slots, so these values must be set
202    /// before any arena_topology_node_init() calls. The sizes are derived from
203    /// the actual host topology to keep per-node allocation as small as
204    /// possible.
205    ///
206    /// NOTE: rust/scx_arena/selftests/src/main.rs::setup_topology() contains
207    /// equivalent logic and must be kept in sync with this function.
208    fn setup_topology_max_children(obj: &Object, topo: &Topology) -> Result<()> {
209        // Compute the maximum number of children at each topology level.
210        // TOPO_TOP  (0): children are NUMA nodes
211        // TOPO_NODE (1): children are LLCs
212        // TOPO_LLC  (2): children are cores
213        // TOPO_CORE (3): children are CPUs (SMT threads)
214        // TOPO_CPU  (4): leaf nodes, no children
215        let max_children: [u32; 5] = [
216            topo.nodes.len() as u32,
217            topo.nodes.values().map(|n| n.llcs.len()).max().unwrap_or(0) as u32,
218            topo.all_llcs
219                .values()
220                .map(|l| l.cores.len())
221                .max()
222                .unwrap_or(0) as u32,
223            topo.all_cores
224                .values()
225                .map(|c| c.cpus.len())
226                .max()
227                .unwrap_or(0) as u32,
228            0,
229        ];
230
231        let mut args = types::ArenaTopologyInitArgs { max_children };
232
233        let input = ProgramInput {
234            context_in: Some(unsafe {
235                std::slice::from_raw_parts_mut(
236                    &mut args as *mut _ as *mut u8,
237                    std::mem::size_of_val(&args),
238                )
239            }),
240            ..Default::default()
241        };
242
243        let ret = Self::run_prog_by_name(obj, "arena_topology_init", input)?;
244        if ret != 0 {
245            bail!("arena_topology_init returned {}", ret);
246        }
247
248        Ok(())
249    }
250
251    fn setup_topology(obj: &Object, nr_cpus: usize) -> Result<()> {
252        let topo = Topology::new().expect("Failed to build host topology");
253
254        Self::setup_topology_max_children(obj, &topo)?;
255
256        // Top level - ID 0 is fine as there's only one top-level node
257        Self::setup_topology_node(obj, nr_cpus, topo.span.as_raw_slice(), 0)?;
258
259        for (node_id, node) in topo.nodes {
260            Self::setup_topology_node(obj, nr_cpus, node.span.as_raw_slice(), node_id)?;
261        }
262
263        // LLCs need to use their actual LLC ID for proper indexing in topo_nodes
264        for (llc_id, llc) in topo.all_llcs {
265            Self::setup_topology_node(
266                obj,
267                nr_cpus,
268                Arc::<Llc>::into_inner(llc)
269                    .expect("missing llc")
270                    .span
271                    .as_raw_slice(),
272                llc_id,
273            )?;
274        }
275
276        for (core_id, core) in topo.all_cores {
277            Self::setup_topology_node(
278                obj,
279                nr_cpus,
280                Arc::<Core>::into_inner(core)
281                    .expect("missing core")
282                    .span
283                    .as_raw_slice(),
284                core_id,
285            )?;
286        }
287        for (_, cpu) in topo.all_cpus {
288            let mut mask = vec![0; Self::nr_cpumask_words(nr_cpus)];
289            mask[cpu.id / 64] |= 1 << (cpu.id % 64);
290            Self::setup_topology_node(obj, nr_cpus, &mask, cpu.id)?;
291        }
292
293        Ok(())
294    }
295
296    /// Set up the BPF arena library state and, when the object carries the
297    /// scx_urcu doorbell, spawn the reclaim daemon. The returned ScxLibArena
298    /// owns the library's background threads.
299    /// @task_align: task ctx element alignment, 0 for word alignment.
300    pub fn setup(
301        obj: &Object,
302        task_size: usize,
303        task_align: usize,
304        nr_cpus: usize,
305    ) -> Result<ScxLibArena> {
306        Self::setup_arena(obj, task_size, task_align)?;
307        Self::setup_topology(obj, nr_cpus)?;
308
309        Self::start(obj)
310    }
311
312    /// Start the userspace services for BPF arena state initialized by the
313    /// caller. The returned ScxLibArena must be kept alive for as long as the BPF
314    /// object uses the arena.
315    ///
316    /// Use this instead of setup() when a scheduler has its own BPF-side arena
317    /// initialization and does not use the generic arena topology.
318    pub fn start(obj: &Object) -> Result<ScxLibArena> {
319        Ok(ScxLibArena {
320            _watcher: crate::stream_watcher_spawn(obj)?,
321            _urcu: crate::urcu_spawn(obj)?,
322        })
323    }
324}