Skip to main content

scx_utils/
topology.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.
5
6//! # SCX Topology
7//!
8//! A crate that allows schedulers to inspect and model the host's topology, in
9//! service of creating scheduling domains.
10//!
11//! A Topology is comprised of one or more Node objects, which themselves are
12//! comprised hierarchically of LLC -> Core -> Cpu objects respectively:
13//!```rust,ignore
14//!                                   Topology
15//!                                       |
16//! o--------------------------------o   ...   o----------------o---------------o
17//! |         Node                   |         |         Node                   |
18//! | ID      0                      |         | ID      1                      |
19//! | LLCs    <id, Llc>              |         | LLCs    <id, Llc>              |
20//! | Span    0x00000fffff00000fffff |         | Span    0xfffff00000fffff00000 |
21//! o--------------------------------o         o--------------------------------o
22//!                 \
23//!                  --------------------
24//!                                      \
25//! o--------------------------------o   ...   o--------------------------------o
26//! |             Llc                |         |             Llc                |
27//! | ID     0                       |         | ID     1                       |
28//! | Cores  <id, Core>              |         | Cores  <id, Core>              |
29//! | Span   0x00000ffc0000000ffc00  |         | Span   0x00000003ff00000003ff  |
30//! o--------------------------------o         o----------------o---------------o
31//!                                                             /
32//!                                        ---------------------
33//!                                       /
34//! o--------------------------------o   ...   o--------------------------------o
35//! |              Core              |         |              Core              |
36//! | ID     0                       |         | ID     9                       |
37//! | Cpus   <id, Cpu>               |         | Cpus   <id, Cpu>               |
38//! | Span   0x00000000010000000001  |         | Span   0x00000002000000000200  |
39//! o--------------------------------o         o----------------o---------------o
40//!                                                             /
41//!                                        ---------------------
42//!                                       /
43//! o--------------------------------o   ...   o---------------------------------o
44//! |              Cpu               |         |               Cpu               |
45//! | ID       9                     |         | ID       49                     |
46//! | online   1                     |         | online   1                      |
47//! | min_freq 400000                |         | min_freq 400000                 |
48//! | max_freq 5881000               |         | min_freq 5881000                |
49//! o--------------------------------o         o---------------------------------o
50//!```
51//! Every object contains a Cpumask that spans all CPUs in that point in the
52//! topological hierarchy.
53//!
54//! Creating Topology
55//! -----------------
56//!
57//! Topology objects are created using the static new function:
58//!
59//!```  
60//!     use scx_utils::Topology;
61//!     let top = Topology::new().unwrap();
62//!```
63//!
64//! Querying Topology
65//! -----------------
66//!
67//! With a created Topology, you can query the topological hierarchy using the
68//! set of accessor functions defined below. All objects in the topological
69//! hierarchy are entirely read-only. If the host topology were to change (due
70//! to e.g. hotplug), a new Topology object should be created.
71
72use crate::compat::ROOT_PREFIX;
73use crate::cpumask::read_cpulist;
74use crate::misc::find_best_split_size;
75use crate::misc::read_file_byte;
76use crate::misc::read_file_usize_vec;
77use crate::misc::read_from_file;
78use crate::Cpumask;
79use anyhow::bail;
80use anyhow::Result;
81use glob::glob;
82use log::info;
83use log::warn;
84use sscanf::sscanf;
85use std::cmp::min;
86use std::collections::BTreeMap;
87use std::io::Write;
88use std::path::Path;
89use std::sync::Arc;
90
91#[cfg(feature = "gpu-topology")]
92use crate::gpu::{create_gpus, Gpu, GpuIndex};
93
94lazy_static::lazy_static! {
95    /// The maximum possible number of CPU IDs in the system. As mentioned
96    /// above, this is different than the number of possible CPUs on the
97    /// system (though very seldom is). This number may differ from the
98    /// number of possible CPUs on the system when e.g. there are fully
99    /// disabled CPUs in the middle of the range of possible CPUs (i.e. CPUs
100    /// that may not be onlined).
101    pub static ref NR_CPU_IDS: usize = read_cpu_ids().unwrap().last().unwrap() + 1;
102
103    /// The number of possible CPUs that may be active on the system. Note
104    /// that this value is separate from the number of possible _CPU IDs_ in
105    /// the system, as there may be gaps in what CPUs are allowed to be
106    /// onlined. For example, some BIOS implementations may report spans of
107    /// disabled CPUs that may not be onlined, whose IDs are lower than the
108    /// IDs of other CPUs that may be onlined.
109    pub static ref NR_CPUS_POSSIBLE: usize = libbpf_rs::num_possible_cpus().unwrap();
110
111    /// The range to search for when finding the number of physical cores
112    /// assigned to a partition to split a large number of cores that share
113    /// an LLC domain. The suggested split for the cores isn't a function of
114    /// the underlying hardware's capability, but rather some sane number
115    /// to help determine the number of CPUs that share the same DSQ.
116    pub static ref NR_PARTITION_MIN_CORES: usize = 2;
117    pub static ref NR_PARTITION_MAX_CORES: usize = 8;
118}
119
120#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
121pub enum CoreType {
122    Big { turbo: bool },
123    Little,
124}
125
126#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
127pub enum Powermode {
128    Turbo,
129    Performance,
130    Powersave,
131    Any,
132}
133
134/// Return the list of CPU IDs matching the requested power mode.
135///
136/// Selects CPUs from the system topology based on [`Powermode`]:
137/// - [`Powermode::Turbo`]: only turbo-capable big cores
138/// - [`Powermode::Performance`]: all big cores
139/// - [`Powermode::Powersave`]: only little cores
140/// - [`Powermode::Any`]: all CPUs
141pub fn get_primary_cpus(mode: Powermode) -> std::io::Result<Vec<usize>> {
142    let topo = Topology::new().unwrap();
143
144    let cpus: Vec<usize> = topo
145        .all_cores
146        .values()
147        .flat_map(|core| &core.cpus)
148        .filter_map(|(cpu_id, cpu)| match (&mode, &cpu.core_type) {
149            (Powermode::Turbo, CoreType::Big { turbo: true })
150            | (Powermode::Performance, CoreType::Big { .. })
151            | (Powermode::Powersave, CoreType::Little) => Some(*cpu_id),
152            (Powermode::Any, ..) => Some(*cpu_id),
153            _ => None,
154        })
155        .collect();
156
157    Ok(cpus)
158}
159
160#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
161pub struct Cpu {
162    pub id: usize,
163    pub min_freq: usize,
164    pub max_freq: usize,
165    /// Base operational frqeuency. Only available on Intel Turbo Boost
166    /// CPUs. If not available, this will simply return maximum frequency.
167    pub base_freq: usize,
168    /// The best-effort guessing of cpu_capacity scaled to 1024.
169    pub cpu_capacity: usize,
170    pub smt_level: usize,
171    /// CPU idle resume latency
172    pub pm_qos_resume_latency_us: usize,
173    pub trans_lat_ns: usize,
174    pub l2_id: usize,
175    pub l3_id: usize,
176    /// Per-CPU cache size of all levels.
177    pub cache_size: usize,
178    pub core_type: CoreType,
179
180    /// Ancestor IDs.
181    pub core_id: usize,
182    pub llc_id: usize,
183    pub node_id: usize,
184    pub package_id: usize,
185    pub cluster_id: isize,
186}
187
188#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
189pub struct Core {
190    /// Monotonically increasing unique id
191    pub id: usize,
192    /// The sysfs value of core_id
193    pub kernel_id: usize,
194    pub cluster_id: isize,
195    pub cpus: BTreeMap<usize, Arc<Cpu>>,
196    /// Cpumask of all CPUs in this core.
197    pub span: Cpumask,
198    pub core_type: CoreType,
199
200    /// Ancestor IDs.
201    pub llc_id: usize,
202    pub node_id: usize,
203}
204
205#[derive(Debug, Clone)]
206pub struct Llc {
207    /// Monotonically increasing unique id
208    pub id: usize,
209    /// The kernel id of the llc
210    pub kernel_id: usize,
211    pub cores: BTreeMap<usize, Arc<Core>>,
212    /// Cpumask of all CPUs in this llc.
213    pub span: Cpumask,
214
215    /// Ancestor IDs.
216    pub node_id: usize,
217
218    /// Skip indices to access lower level members easily.
219    pub all_cpus: BTreeMap<usize, Arc<Cpu>>,
220}
221
222#[derive(Debug, Clone)]
223pub struct Node {
224    pub id: usize,
225    pub distance: Vec<usize>,
226    pub llcs: BTreeMap<usize, Arc<Llc>>,
227    /// Cpumask of all CPUs in this node.
228    pub span: Cpumask,
229
230    /// Skip indices to access lower level members easily.
231    pub all_cores: BTreeMap<usize, Arc<Core>>,
232    pub all_cpus: BTreeMap<usize, Arc<Cpu>>,
233
234    #[cfg(feature = "gpu-topology")]
235    pub gpus: BTreeMap<GpuIndex, Gpu>,
236}
237
238#[derive(Debug)]
239pub struct Topology {
240    pub nodes: BTreeMap<usize, Node>,
241    /// Cpumask all CPUs in the system.
242    pub span: Cpumask,
243    /// True if SMT is enabled in the system, false otherwise.
244    pub smt_enabled: bool,
245
246    /// Skip indices to access lower level members easily.
247    pub all_llcs: BTreeMap<usize, Arc<Llc>>,
248    pub all_cores: BTreeMap<usize, Arc<Core>>,
249    pub all_cpus: BTreeMap<usize, Arc<Cpu>>,
250}
251
252impl Topology {
253    fn instantiate(span: Cpumask, mut nodes: BTreeMap<usize, Node>) -> Result<Self> {
254        // Build skip indices prefixed with all_ for easy lookups. As Arc
255        // objects can only be modified while there's only one reference,
256        // skip indices must be built from bottom to top.
257        let mut topo_llcs = BTreeMap::new();
258        let mut topo_cores = BTreeMap::new();
259        let mut topo_cpus = BTreeMap::new();
260
261        for (_node_id, node) in nodes.iter_mut() {
262            let mut node_cores = BTreeMap::new();
263            let mut node_cpus = BTreeMap::new();
264
265            for (&llc_id, llc) in node.llcs.iter_mut() {
266                let llc_mut = Arc::get_mut(llc).unwrap();
267                let mut llc_cpus = BTreeMap::new();
268
269                for (&core_id, core) in llc_mut.cores.iter_mut() {
270                    let core_mut = Arc::get_mut(core).unwrap();
271                    let smt_level = core_mut.cpus.len();
272
273                    for (&cpu_id, cpu) in core_mut.cpus.iter_mut() {
274                        let cpu_mut = Arc::get_mut(cpu).unwrap();
275                        cpu_mut.smt_level = smt_level;
276
277                        if topo_cpus
278                            .insert(cpu_id, cpu.clone())
279                            .or(node_cpus.insert(cpu_id, cpu.clone()))
280                            .or(llc_cpus.insert(cpu_id, cpu.clone()))
281                            .is_some()
282                        {
283                            bail!("Duplicate CPU ID {}", cpu_id);
284                        }
285                    }
286
287                    // Note that in some weird architectures, core ids can be
288                    // duplicated in different LLC domains.
289                    topo_cores
290                        .insert(core_id, core.clone())
291                        .or(node_cores.insert(core_id, core.clone()));
292                }
293
294                llc_mut.all_cpus = llc_cpus;
295
296                if topo_llcs.insert(llc_id, llc.clone()).is_some() {
297                    bail!("Duplicate LLC ID {}", llc_id);
298                }
299            }
300
301            node.all_cores = node_cores;
302            node.all_cpus = node_cpus;
303        }
304
305        Ok(Topology {
306            nodes,
307            span,
308            smt_enabled: is_smt_active().unwrap_or(false),
309            all_llcs: topo_llcs,
310            all_cores: topo_cores,
311            all_cpus: topo_cpus,
312        })
313    }
314
315    /// Build a complete host Topology
316    pub fn new() -> Result<Topology> {
317        Self::with_virt_llcs(None)
318    }
319
320    pub fn with_virt_llcs(nr_cores_per_vllc: Option<(usize, usize)>) -> Result<Topology> {
321        let span = cpus_online()?;
322        let mut topo_ctx = TopoCtx::new();
323
324        // If the kernel is compiled with CONFIG_NUMA, then build a topology
325        // from the NUMA hierarchy in sysfs. Otherwise, just make a single
326        // default node of ID 0 which contains all cores.
327        let path = format!("{}/sys/devices/system/node", *ROOT_PREFIX);
328        let nodes = if Path::new(&path).exists() {
329            create_numa_nodes(&span, &mut topo_ctx, nr_cores_per_vllc)?
330        } else {
331            create_default_node(&span, &mut topo_ctx, false, nr_cores_per_vllc)?
332        };
333
334        Self::instantiate(span, nodes)
335    }
336
337    pub fn with_flattened_llc_node() -> Result<Topology> {
338        let span = cpus_online()?;
339        let mut topo_ctx = TopoCtx::new();
340        let nodes = create_default_node(&span, &mut topo_ctx, true, None)?;
341        Self::instantiate(span, nodes)
342    }
343
344    /// Build a topology with configuration from CLI arguments.
345    /// This method integrates with the TopologyArgs from the cli module to
346    /// create a topology based on command line parameters.
347    pub fn with_args(topology_args: &crate::cli::TopologyArgs) -> Result<Topology> {
348        // Validate the CLI arguments first
349        topology_args.validate()?;
350
351        // Get the virtual LLC configuration
352        let nr_cores_per_vllc = topology_args.get_nr_cores_per_vllc();
353
354        // Build topology with the specified configuration
355        Self::with_virt_llcs(nr_cores_per_vllc)
356    }
357
358    /// Get a vec of all GPUs on the hosts.
359    #[cfg(feature = "gpu-topology")]
360    pub fn gpus(&self) -> BTreeMap<GpuIndex, &Gpu> {
361        let mut gpus = BTreeMap::new();
362        for node in self.nodes.values() {
363            for (idx, gpu) in &node.gpus {
364                gpus.insert(*idx, gpu);
365            }
366        }
367        gpus
368    }
369
370    /// Returns whether the Topology has a hybrid architecture of big and little cores.
371    pub fn has_little_cores(&self) -> bool {
372        self.all_cores
373            .values()
374            .any(|c| c.core_type == CoreType::Little)
375    }
376
377    /// Returns a vector that maps the index of each logical CPU to the
378    /// sibling CPU. This represents the "next sibling" CPU within a package
379    /// in systems that support SMT. The sibling CPU is the other logical
380    /// CPU that shares the physical resources of the same physical core.
381    ///
382    /// Assuming each core holds exactly at most two cpus.
383    pub fn sibling_cpus(&self) -> Vec<i32> {
384        let mut sibling_cpu = vec![-1i32; *NR_CPUS_POSSIBLE];
385        for core in self.all_cores.values() {
386            let mut first = -1i32;
387            for &cpu in core.cpus.keys() {
388                if first < 0 {
389                    first = cpu as i32;
390                } else {
391                    sibling_cpu[first as usize] = cpu as i32;
392                    sibling_cpu[cpu] = first;
393                    break;
394                }
395            }
396        }
397        sibling_cpu
398    }
399
400    /// Count how many physical cores have at least one CPU set in the cpumask.
401    pub fn cpumask_nr_cores(&self, cpumask: &Cpumask) -> usize {
402        let mut count = 0;
403        for core in self.all_cores.values() {
404            if core.cpus.keys().any(|&cpu_id| cpumask.test_cpu(cpu_id)) {
405                count += 1;
406            }
407        }
408        count
409    }
410
411    /// Format a cpumask as a topology-aware visual grid.
412    ///
413    /// Each physical core is represented by a single character:
414    /// - `░` = no CPUs set
415    /// - `▀` = first HT only (top half)
416    /// - `▄` = second HT only (bottom half)
417    /// - `█` = both HTs (or all HTs for >2-way SMT)
418    ///
419    /// Cores within an LLC are split into evenly-sized groups of at
420    /// most 8 with spaces. LLCs are separated by `|`. Wrapping
421    /// happens at LLC boundaries. One line per NUMA node (may wrap).
422    pub fn format_cpumask_grid<W: Write>(
423        &self,
424        w: &mut W,
425        cpumask: &Cpumask,
426        indent: &str,
427        max_width: usize,
428    ) -> Result<()> {
429        for node in self.nodes.values() {
430            // Build the core characters for each LLC in this node.
431            // Within each LLC, cores are grouped by 4 with spaces.
432            let mut llc_segments: Vec<(usize, String)> = Vec::new();
433
434            for llc in node.llcs.values() {
435                let mut seg = String::new();
436                let nr_cores = llc.cores.len();
437                let nr_groups = (nr_cores + 7) / 8;
438                let base = nr_cores / nr_groups;
439                let rem = nr_cores % nr_groups;
440                // First `rem` groups get base+1, rest get base
441                let mut next_break = if rem > 0 { base + 1 } else { base };
442                let mut group_idx = 0;
443                for (i, core) in llc.cores.values().enumerate() {
444                    if i > 0 && i == next_break {
445                        seg.push(' ');
446                        group_idx += 1;
447                        next_break += if group_idx < rem { base + 1 } else { base };
448                    }
449                    let nr_cpus = core.cpus.len();
450                    let cpu_ids: Vec<usize> = core.cpus.keys().copied().collect();
451                    let nr_set: usize = cpu_ids.iter().filter(|&&c| cpumask.test_cpu(c)).count();
452
453                    let ch = if nr_cpus == 1 {
454                        if nr_set > 0 {
455                            '█'
456                        } else {
457                            '░'
458                        }
459                    } else if nr_cpus == 2 {
460                        let first_set = cpumask.test_cpu(cpu_ids[0]);
461                        let second_set = cpumask.test_cpu(cpu_ids[1]);
462                        match (first_set, second_set) {
463                            (false, false) => '░',
464                            (true, false) => '▀',
465                            (false, true) => '▄',
466                            (true, true) => '█',
467                        }
468                    } else {
469                        // >2 HTs (e.g. 4-way SMT)
470                        if nr_set == 0 {
471                            '░'
472                        } else if nr_set == nr_cpus {
473                            '█'
474                        } else {
475                            '▄'
476                        }
477                    };
478                    seg.push(ch);
479                }
480                llc_segments.push((llc.id, seg));
481            }
482
483            if llc_segments.is_empty() {
484                continue;
485            }
486
487            // Build prefix: "N{id} L{first_llc}: "
488            let first_llc_id = llc_segments[0].0;
489            let prefix = format!("{}N{} L{:02}: ", indent, node.id, first_llc_id);
490            let prefix_width = prefix.chars().count();
491            let cont_indent = format!(
492                "{}{}",
493                indent,
494                " ".repeat(prefix_width - indent.chars().count())
495            );
496
497            // Join LLCs with "|", wrapping at LLC boundaries
498            let mut line = prefix.clone();
499            let mut first_llc = true;
500
501            for (_, seg) in &llc_segments {
502                let seg_width = seg.chars().count();
503                let separator = if first_llc { "" } else { "|" };
504                let sep_width = separator.chars().count();
505                let current_line_width = line.chars().count();
506
507                if !first_llc && current_line_width + sep_width + seg_width > max_width {
508                    writeln!(w, "{}", line)?;
509                    line = format!("{}{}", cont_indent, seg);
510                } else {
511                    line = format!("{}{}{}", line, separator, seg);
512                }
513                first_llc = false;
514            }
515            writeln!(w, "{}", line)?;
516        }
517        Ok(())
518    }
519
520    /// Format a cpumask header line with cpu count, core count, and range.
521    pub fn format_cpumask_header(&self, cpumask: &Cpumask, min_cpus: u32, max_cpus: u32) -> String {
522        let nr_cpus = cpumask.weight();
523        let nr_cores = self.cpumask_nr_cores(cpumask);
524        format!(
525            "cpus={:3}({:3}c) [{:3},{:3}]",
526            nr_cpus, nr_cores, min_cpus, max_cpus
527        )
528    }
529}
530
531/******************************************************
532 * Helper structs/functions for creating the Topology *
533 ******************************************************/
534/// TopoCtx is a helper struct used to build a topology.
535struct TopoCtx {
536    /// Mapping of NUMA node core ids
537    node_core_kernel_ids: BTreeMap<(usize, usize, usize), usize>,
538    /// Mapping of NUMA node LLC ids
539    node_llc_kernel_ids: BTreeMap<(usize, usize, usize), usize>,
540    /// Mapping of L2 ids
541    l2_ids: BTreeMap<String, usize>,
542    /// Mapping of L3 ids
543    l3_ids: BTreeMap<String, usize>,
544}
545
546impl TopoCtx {
547    fn new() -> TopoCtx {
548        let core_kernel_ids = BTreeMap::new();
549        let llc_kernel_ids = BTreeMap::new();
550        let l2_ids = BTreeMap::new();
551        let l3_ids = BTreeMap::new();
552        TopoCtx {
553            node_core_kernel_ids: core_kernel_ids,
554            node_llc_kernel_ids: llc_kernel_ids,
555            l2_ids,
556            l3_ids,
557        }
558    }
559}
560
561fn cpus_online() -> Result<Cpumask> {
562    let path = format!("{}/sys/devices/system/cpu/online", *ROOT_PREFIX);
563    let online = std::fs::read_to_string(path)?;
564    Cpumask::from_cpulist(&online)
565}
566
567fn get_cache_id(topo_ctx: &mut TopoCtx, cache_level_path: &Path, cache_level: usize) -> usize {
568    // Check if the cache id is already cached
569    let id_map = match cache_level {
570        2 => &mut topo_ctx.l2_ids,
571        3 => &mut topo_ctx.l3_ids,
572        _ => return usize::MAX,
573    };
574
575    let path = &cache_level_path.join("shared_cpu_list");
576    let key = match std::fs::read_to_string(path) {
577        Ok(key) => key,
578        Err(_) => return usize::MAX,
579    };
580
581    let id = *id_map.get(&key).unwrap_or(&usize::MAX);
582    if id != usize::MAX {
583        return id;
584    }
585
586    // In case of a cache miss, try to get the id from the sysfs first.
587    let id = read_from_file(&cache_level_path.join("id")).unwrap_or(usize::MAX);
588    if id != usize::MAX {
589        // Keep the id in the map
590        id_map.insert(key, id);
591        return id;
592    }
593
594    // If the id file does not exist, assign an id and keep it in the map.
595    let id = id_map.len();
596    id_map.insert(key, id);
597
598    id
599}
600
601fn get_per_cpu_cache_size(cache_path: &Path) -> Result<usize> {
602    let path_str = cache_path.to_str().unwrap();
603    let paths = glob(&(path_str.to_owned() + "/index[0-9]*"))?;
604    let mut tot_size = 0;
605
606    for index in paths.filter_map(Result::ok) {
607        // If there is no size information under sysfs (e.g., many ARM SoCs),
608        // give 1024 as a default value. 1024 is small enough compared to the
609        // real cache size of the CPU, but it is large enough to give a penalty
610        // when multiple CPUs share the cache.
611        let size = read_file_byte(&index.join("size")).unwrap_or(1024_usize);
612        let cpulist: String = read_from_file(&index.join("shared_cpu_list"))?;
613        let num_cpus = read_cpulist(&cpulist)?.len();
614        tot_size += size / num_cpus;
615    }
616
617    Ok(tot_size)
618}
619
620#[allow(clippy::too_many_arguments)]
621fn create_insert_cpu(
622    id: usize,
623    node: &mut Node,
624    online_mask: &Cpumask,
625    topo_ctx: &mut TopoCtx,
626    cs: &CapacitySource,
627    flatten_llc: bool,
628) -> Result<()> {
629    // CPU is offline. The Topology hierarchy is read-only, and assumes
630    // that hotplug will cause the scheduler to restart. Thus, we can
631    // just skip this CPU altogether.
632    if !online_mask.test_cpu(id) {
633        return Ok(());
634    }
635
636    let cpu_str = format!("{}/sys/devices/system/cpu/cpu{}", *ROOT_PREFIX, id);
637    let cpu_path = Path::new(&cpu_str);
638
639    // Physical core ID
640    let top_path = cpu_path.join("topology");
641    let core_kernel_id = read_from_file(&top_path.join("core_id"))?;
642    let package_id = read_from_file(&top_path.join("physical_package_id"))?;
643    let cluster_id = read_from_file(&top_path.join("cluster_id"))?;
644
645    // Evaluate L2, L3 and LLC cache IDs.
646    //
647    // Use ID 0 if we fail to detect the cache hierarchy. This seems to happen on certain SKUs, so
648    // if there's no cache information then we have no option but to assume a single unified cache
649    // per node.
650    let cache_path = cpu_path.join("cache");
651    let l2_id = get_cache_id(topo_ctx, &cache_path.join(format!("index{}", 2)), 2);
652    let l3_id = get_cache_id(topo_ctx, &cache_path.join(format!("index{}", 3)), 3);
653    let llc_kernel_id = if flatten_llc {
654        0
655    } else if l3_id == usize::MAX {
656        l2_id
657    } else {
658        l3_id
659    };
660
661    // Per-CPU cache size
662    let cache_size = get_per_cpu_cache_size(&cache_path).unwrap_or(0_usize);
663
664    // Min and max frequencies. If the kernel is not compiled with
665    // CONFIG_CPU_FREQ, just assume 0 for both frequencies.
666    let freq_path = cpu_path.join("cpufreq");
667    let min_freq = read_from_file(&freq_path.join("scaling_min_freq")).unwrap_or(0_usize);
668    let max_freq = read_from_file(&freq_path.join("scaling_max_freq")).unwrap_or(0_usize);
669    let base_freq = read_from_file(&freq_path.join("base_frequency")).unwrap_or(max_freq);
670    let trans_lat_ns =
671        read_from_file(&freq_path.join("cpuinfo_transition_latency")).unwrap_or(0_usize);
672
673    // Cpu capacity
674    let cap_path = cpu_path.join(cs.suffix.clone());
675    let rcap = read_from_file(&cap_path).unwrap_or(cs.max_rcap);
676    let cpu_capacity = (rcap * 1024) / cs.max_rcap;
677
678    // Power management
679    let power_path = cpu_path.join("power");
680    let pm_qos_resume_latency_us =
681        read_from_file(&power_path.join("pm_qos_resume_latency_us")).unwrap_or(0_usize);
682
683    let num_llcs = topo_ctx.node_llc_kernel_ids.len();
684    let llc_id = topo_ctx
685        .node_llc_kernel_ids
686        .entry((node.id, package_id, llc_kernel_id))
687        .or_insert(num_llcs);
688
689    let llc = node.llcs.entry(*llc_id).or_insert(Arc::new(Llc {
690        id: *llc_id,
691        cores: BTreeMap::new(),
692        span: Cpumask::new(),
693        all_cpus: BTreeMap::new(),
694
695        node_id: node.id,
696        kernel_id: llc_kernel_id,
697    }));
698    let llc_mut = Arc::get_mut(llc).unwrap();
699
700    let core_type = if cs.avg_rcap < cs.max_rcap && rcap == cs.max_rcap {
701        CoreType::Big { turbo: true }
702    } else if !cs.has_biglittle || rcap >= cs.avg_rcap {
703        CoreType::Big { turbo: false }
704    } else {
705        CoreType::Little
706    };
707
708    let num_cores = topo_ctx.node_core_kernel_ids.len();
709    let core_id = topo_ctx
710        .node_core_kernel_ids
711        .entry((node.id, package_id, core_kernel_id))
712        .or_insert(num_cores);
713
714    let core = llc_mut.cores.entry(*core_id).or_insert(Arc::new(Core {
715        id: *core_id,
716        cpus: BTreeMap::new(),
717        span: Cpumask::new(),
718        core_type: core_type.clone(),
719
720        llc_id: *llc_id,
721        node_id: node.id,
722        kernel_id: core_kernel_id,
723        cluster_id,
724    }));
725    let core_mut = Arc::get_mut(core).unwrap();
726
727    core_mut.cpus.insert(
728        id,
729        Arc::new(Cpu {
730            id,
731            min_freq,
732            max_freq,
733            base_freq,
734            cpu_capacity,
735            smt_level: 0, // Will be initialized at instantiate().
736            pm_qos_resume_latency_us,
737            trans_lat_ns,
738            l2_id,
739            l3_id,
740            cache_size,
741            core_type: core_type.clone(),
742
743            core_id: *core_id,
744            llc_id: *llc_id,
745            node_id: node.id,
746            package_id,
747            cluster_id,
748        }),
749    );
750
751    if node.span.test_cpu(id) {
752        bail!("Node {} already had CPU {}", node.id, id);
753    }
754
755    // Update all of the devices' spans to include this CPU.
756    core_mut.span.set_cpu(id)?;
757    llc_mut.span.set_cpu(id)?;
758    node.span.set_cpu(id)?;
759
760    Ok(())
761}
762
763fn read_cpu_ids() -> Result<Vec<usize>> {
764    let mut cpu_ids = vec![];
765    let path = format!("{}/sys/devices/system/cpu/cpu[0-9]*", *ROOT_PREFIX);
766    let cpu_paths = glob(&path)?;
767    for cpu_path in cpu_paths.filter_map(Result::ok) {
768        let cpu_str = cpu_path.to_str().unwrap().trim();
769        if ROOT_PREFIX.is_empty() {
770            match sscanf!(cpu_str, "/sys/devices/system/cpu/cpu{usize}") {
771                Some(val) => cpu_ids.push(val),
772                None => {
773                    bail!("Failed to parse cpu ID {}", cpu_str);
774                }
775            }
776        } else {
777            match sscanf!(cpu_str, "{str}/sys/devices/system/cpu/cpu{usize}") {
778                Some((_, val)) => cpu_ids.push(val),
779                None => {
780                    bail!("Failed to parse cpu ID {}", cpu_str);
781                }
782            }
783        }
784    }
785    cpu_ids.sort();
786    Ok(cpu_ids)
787}
788
789struct CapacitySource {
790    /// Path suffix after /sys/devices/system/cpu/cpuX
791    suffix: String,
792    /// Average raw capacity value
793    avg_rcap: usize,
794    /// Maximum raw capacity value
795    max_rcap: usize,
796    /// Does a system have little cores?
797    has_biglittle: bool,
798}
799
800fn get_capacity_source() -> Option<CapacitySource> {
801    // Sources for guessing cpu_capacity under /sys/devices/system/cpu/cpuX.
802    // They should be ordered from the most precise to the least precise.
803    let sources = [
804        "cpufreq/amd_pstate_prefcore_ranking",
805        "cpufreq/amd_pstate_highest_perf",
806        "acpi_cppc/highest_perf",
807        "cpu_capacity",
808        "cpufreq/cpuinfo_max_freq",
809    ];
810
811    // Find the most precise source for cpu_capacity estimation.
812    let prefix = format!("{}/sys/devices/system/cpu/cpu0", *ROOT_PREFIX);
813    let mut raw_capacity;
814    let mut suffix = sources[sources.len() - 1];
815    'outer: for src in sources {
816        let path_str = [prefix.clone(), src.to_string()].join("/");
817        let path = Path::new(&path_str);
818        raw_capacity = read_from_file(&path).unwrap_or(0_usize);
819        if raw_capacity > 0 {
820            // It would be an okay source...
821            suffix = src;
822            // But double-check if the source has meaningful information.
823            let path = format!("{}/sys/devices/system/cpu/cpu[0-9]*", *ROOT_PREFIX);
824            let cpu_paths = glob(&path).ok()?;
825            for cpu_path in cpu_paths.filter_map(Result::ok) {
826                let raw_capacity2 = read_from_file(&cpu_path.join(suffix)).unwrap_or(0_usize);
827                if raw_capacity != raw_capacity2 {
828                    break 'outer;
829                }
830            }
831            // The source exists, but it tells that all CPUs have the same
832            // capacity. Let's search more if there is any source that can
833            // tell the capacity differences among CPUs. This can happen when
834            // a buggy driver lies (e.g., "acpi_cppc/highest_perf").
835        }
836    }
837
838    // Find the max raw_capacity value for scaling to 1024.
839    let mut max_rcap = 0;
840    let mut min_rcap = usize::MAX;
841    let mut avg_rcap = 0;
842    let mut nr_cpus = 0;
843    let mut has_biglittle = false;
844    let path = format!("{}/sys/devices/system/cpu/cpu[0-9]*", *ROOT_PREFIX);
845    let cpu_paths = glob(&path).ok()?;
846    for cpu_path in cpu_paths.filter_map(Result::ok) {
847        let rcap = read_from_file(&cpu_path.join(suffix)).unwrap_or(0_usize);
848        if max_rcap < rcap {
849            max_rcap = rcap;
850        }
851        if min_rcap > rcap {
852            min_rcap = rcap;
853        }
854        avg_rcap += rcap;
855        nr_cpus += 1;
856    }
857
858    if nr_cpus == 0 || max_rcap == 0 {
859        suffix = "";
860        avg_rcap = 1024;
861        max_rcap = 1024;
862        warn!("CPU capacity information is not available under sysfs.");
863    } else {
864        avg_rcap /= nr_cpus;
865        // We consider a system to have a heterogeneous CPU architecture only
866        // when there is a significant capacity gap (e.g., 1.3x). CPU capacities
867        // can still vary in a homogeneous architecture—for instance, due to
868        // chip binning or when only a subset of CPUs supports turbo boost.
869        //
870        // Note that we need a more systematic approach to accurately detect
871        // big/LITTLE architectures across various SoC designs. The current
872        // approach, with a significant capacity difference, is somewhat ad-hoc.
873        has_biglittle = max_rcap as f32 >= (1.3 * min_rcap as f32);
874    }
875
876    Some(CapacitySource {
877        suffix: suffix.to_string(),
878        avg_rcap,
879        max_rcap,
880        has_biglittle,
881    })
882}
883
884fn is_smt_active() -> Option<bool> {
885    let path = format!("{}/sys/devices/system/cpu/smt/active", *ROOT_PREFIX);
886    let smt_on: u8 = read_from_file(Path::new(&path)).ok()?;
887    Some(smt_on == 1)
888}
889
890fn replace_with_virt_llcs(
891    node: &mut Node,
892    min_cores: usize,
893    max_cores: usize,
894    start_id: usize,
895) -> Result<usize> {
896    let mut next_id = start_id;
897    let mut core_to_partition: BTreeMap<usize, usize> = BTreeMap::new();
898    let mut partition_to_kernel_id: BTreeMap<usize, usize> = BTreeMap::new();
899    let num_orig_llcs = node.llcs.len();
900
901    // First pass: determine core to partition mapping, partition to
902    // kernel_id mapping, and total partitions needed
903    for (_llc_id, llc) in node.llcs.iter() {
904        // Group cores by type (big/little) to partition separately
905        let mut cores_by_type: BTreeMap<bool, Vec<usize>> = BTreeMap::new();
906
907        for (core_id, core) in llc.cores.iter() {
908            let core_type = core.core_type == CoreType::Little;
909            cores_by_type
910                .entry(core_type)
911                .or_insert(Vec::new())
912                .push(*core_id);
913        }
914
915        for (_core_type, core_ids) in cores_by_type.iter() {
916            let num_cores_in_bucket = core_ids.len();
917
918            // Find optimal partition size within specified range
919            let best_split = find_best_split_size(num_cores_in_bucket, min_cores, max_cores);
920            let num_partitions = num_cores_in_bucket / best_split;
921
922            // Assign cores to partitions within a group type
923            for (bucket_idx, &core_id) in core_ids.iter().enumerate() {
924                let partition_idx = min(bucket_idx / best_split, num_partitions - 1);
925                let current_partition_id = next_id + partition_idx;
926                core_to_partition.insert(core_id, current_partition_id);
927                partition_to_kernel_id.insert(current_partition_id, llc.kernel_id);
928            }
929
930            next_id += num_partitions;
931        }
932    }
933
934    // Create new virtual LLC structures based on partitioning found above
935    let mut virt_llcs: BTreeMap<usize, Arc<Llc>> = BTreeMap::new();
936
937    for vllc_id in start_id..next_id {
938        let kernel_id = partition_to_kernel_id.get(&vllc_id).copied().unwrap();
939        virt_llcs.insert(
940            vllc_id,
941            Arc::new(Llc {
942                id: vllc_id,
943                kernel_id,
944                cores: BTreeMap::new(),
945                span: Cpumask::new(),
946                node_id: node.id,
947                all_cpus: BTreeMap::new(),
948            }),
949        );
950    }
951
952    // Second pass: move cores to the appropriate new LLC based on partition
953    for (_llc_id, llc) in node.llcs.iter_mut() {
954        for (core_id, core) in llc.cores.iter() {
955            if let Some(&target_partition_id) = core_to_partition.get(core_id) {
956                if let Some(target_llc) = virt_llcs.get_mut(&target_partition_id) {
957                    let target_llc_mut = Arc::get_mut(target_llc).unwrap();
958
959                    // Clone core and update its LLC ID to match new partition
960                    let mut new_core = (**core).clone();
961                    new_core.llc_id = target_partition_id;
962
963                    // Update all CPUs within this core to reference new LLC ID
964                    let mut updated_cpus = BTreeMap::new();
965                    for (cpu_id, cpu) in new_core.cpus.iter() {
966                        let mut new_cpu = (**cpu).clone();
967                        new_cpu.llc_id = target_partition_id;
968
969                        // Add CPU to the virtual LLC's span
970                        target_llc_mut.span.set_cpu(*cpu_id)?;
971
972                        updated_cpus.insert(*cpu_id, Arc::new(new_cpu));
973                    }
974                    new_core.cpus = updated_cpus;
975
976                    // Add the updated core to the virtual LLC
977                    target_llc_mut.cores.insert(*core_id, Arc::new(new_core));
978                }
979            }
980        }
981    }
982
983    // Replace original LLCs with virtual LLCs
984    node.llcs = virt_llcs;
985
986    let num_virt_llcs = next_id - start_id;
987    let vllc_sizes: Vec<usize> = node.llcs.values().map(|llc| llc.cores.len()).collect();
988
989    if vllc_sizes.is_empty() {
990        return Ok(next_id);
991    }
992
993    // Most vLLCs should have the same size, only the last one might differ
994    let common_size = vllc_sizes[0];
995    let last_size = *vllc_sizes.last().unwrap();
996
997    if common_size == last_size {
998        info!(
999            "Node {}: split {} LLC(s) into {} virtual LLCs with {} cores each",
1000            node.id, num_orig_llcs, num_virt_llcs, common_size
1001        );
1002    } else {
1003        info!(
1004            "Node {}: split {} LLC(s) into {} virtual LLCs with {} cores each (last with {})",
1005            node.id, num_orig_llcs, num_virt_llcs, common_size, last_size
1006        );
1007    }
1008
1009    Ok(next_id)
1010}
1011
1012fn create_default_node(
1013    online_mask: &Cpumask,
1014    topo_ctx: &mut TopoCtx,
1015    flatten_llc: bool,
1016    nr_cores_per_vllc: Option<(usize, usize)>,
1017) -> Result<BTreeMap<usize, Node>> {
1018    let mut nodes = BTreeMap::<usize, Node>::new();
1019
1020    let mut node = Node {
1021        id: 0,
1022        distance: vec![],
1023        llcs: BTreeMap::new(),
1024        span: Cpumask::new(),
1025        #[cfg(feature = "gpu-topology")]
1026        gpus: BTreeMap::new(),
1027        all_cores: BTreeMap::new(),
1028        all_cpus: BTreeMap::new(),
1029    };
1030
1031    #[cfg(feature = "gpu-topology")]
1032    {
1033        let system_gpus = create_gpus();
1034        if let Some(gpus) = system_gpus.get(&0) {
1035            for gpu in gpus {
1036                node.gpus.insert(gpu.index, gpu.clone());
1037            }
1038        }
1039    }
1040
1041    let path = format!("{}/sys/devices/system/cpu", *ROOT_PREFIX);
1042    if !Path::new(&path).exists() {
1043        bail!("/sys/devices/system/cpu sysfs node not found");
1044    }
1045
1046    let cs = get_capacity_source().unwrap();
1047    let cpu_ids = read_cpu_ids()?;
1048    for cpu_id in cpu_ids.iter() {
1049        create_insert_cpu(*cpu_id, &mut node, online_mask, topo_ctx, &cs, flatten_llc)?;
1050    }
1051
1052    if let Some((min_cores_val, max_cores_val)) = nr_cores_per_vllc {
1053        replace_with_virt_llcs(&mut node, min_cores_val, max_cores_val, 0)?;
1054    }
1055
1056    nodes.insert(node.id, node);
1057
1058    Ok(nodes)
1059}
1060
1061fn create_numa_nodes(
1062    online_mask: &Cpumask,
1063    topo_ctx: &mut TopoCtx,
1064    nr_cores_per_vllc: Option<(usize, usize)>,
1065) -> Result<BTreeMap<usize, Node>> {
1066    let mut nodes = BTreeMap::<usize, Node>::new();
1067    let mut next_virt_llc_id = 0;
1068
1069    #[cfg(feature = "gpu-topology")]
1070    let system_gpus = create_gpus();
1071
1072    let path = format!("{}/sys/devices/system/node/node*", *ROOT_PREFIX);
1073    let numa_paths = glob(&path)?;
1074    for numa_path in numa_paths.filter_map(Result::ok) {
1075        let numa_str = numa_path.to_str().unwrap().trim();
1076        let node_id = if ROOT_PREFIX.is_empty() {
1077            match sscanf!(numa_str, "/sys/devices/system/node/node{usize}") {
1078                Some(val) => val,
1079                None => {
1080                    bail!("Failed to parse NUMA node ID {}", numa_str);
1081                }
1082            }
1083        } else {
1084            match sscanf!(numa_str, "{str}/sys/devices/system/node/node{usize}") {
1085                Some((_, val)) => val,
1086                None => {
1087                    bail!("Failed to parse NUMA node ID {}", numa_str);
1088                }
1089            }
1090        };
1091
1092        let distance = read_file_usize_vec(
1093            Path::new(&format!(
1094                "{}/sys/devices/system/node/node{}/distance",
1095                *ROOT_PREFIX, node_id
1096            )),
1097            ' ',
1098        )?;
1099        let mut node = Node {
1100            id: node_id,
1101            distance,
1102            llcs: BTreeMap::new(),
1103            span: Cpumask::new(),
1104
1105            all_cores: BTreeMap::new(),
1106            all_cpus: BTreeMap::new(),
1107
1108            #[cfg(feature = "gpu-topology")]
1109            gpus: BTreeMap::new(),
1110        };
1111
1112        #[cfg(feature = "gpu-topology")]
1113        {
1114            if let Some(gpus) = system_gpus.get(&node_id) {
1115                for gpu in gpus {
1116                    node.gpus.insert(gpu.index, gpu.clone());
1117                }
1118            }
1119        }
1120
1121        let cpu_pattern = numa_path.join("cpu[0-9]*");
1122        let cpu_paths = glob(cpu_pattern.to_string_lossy().as_ref())?;
1123        let cs = get_capacity_source().unwrap();
1124        let mut cpu_ids = vec![];
1125        for cpu_path in cpu_paths.filter_map(Result::ok) {
1126            let cpu_str = cpu_path.to_str().unwrap().trim();
1127            let cpu_id = if ROOT_PREFIX.is_empty() {
1128                match sscanf!(cpu_str, "/sys/devices/system/node/node{usize}/cpu{usize}") {
1129                    Some((_, val)) => val,
1130                    None => {
1131                        bail!("Failed to parse cpu ID {}", cpu_str);
1132                    }
1133                }
1134            } else {
1135                match sscanf!(
1136                    cpu_str,
1137                    "{str}/sys/devices/system/node/node{usize}/cpu{usize}"
1138                ) {
1139                    Some((_, _, val)) => val,
1140                    None => {
1141                        bail!("Failed to parse cpu ID {}", cpu_str);
1142                    }
1143                }
1144            };
1145            cpu_ids.push(cpu_id);
1146        }
1147        cpu_ids.sort();
1148
1149        for cpu_id in cpu_ids {
1150            create_insert_cpu(cpu_id, &mut node, online_mask, topo_ctx, &cs, false)?;
1151        }
1152
1153        if let Some((min_cores_val, max_cores_val)) = nr_cores_per_vllc {
1154            next_virt_llc_id =
1155                replace_with_virt_llcs(&mut node, min_cores_val, max_cores_val, next_virt_llc_id)?;
1156        }
1157
1158        nodes.insert(node.id, node);
1159    }
1160    Ok(nodes)
1161}
1162
1163/// Test topology construction helpers.
1164///
1165/// Provides [`make_test_topo()`] for building synthetic [`Topology`] instances
1166/// with configurable node/LLC/core/HT counts, and [`mask_from_bits()`] for
1167/// building [`Cpumask`] values from a list of CPU IDs. Enable via the
1168/// `testutils` feature of `scx_utils`.
1169#[cfg(any(test, feature = "testutils"))]
1170pub mod testutils {
1171    use super::*;
1172    use crate::set_cpumask_test_width;
1173
1174    /// Create a [`Cpu`] with the given IDs and default frequencies/capacity.
1175    pub fn test_cpu(id: usize, core_id: usize, llc_id: usize, node_id: usize) -> Cpu {
1176        Cpu {
1177            id,
1178            core_id,
1179            llc_id,
1180            node_id,
1181            min_freq: 0,
1182            max_freq: 0,
1183            base_freq: 0,
1184            cpu_capacity: 1024,
1185            smt_level: 0, // filled by instantiate()
1186            pm_qos_resume_latency_us: 0,
1187            trans_lat_ns: 0,
1188            l2_id: 0,
1189            l3_id: llc_id,
1190            cache_size: 0,
1191            core_type: CoreType::Big { turbo: false },
1192            package_id: node_id,
1193            cluster_id: 0,
1194        }
1195    }
1196
1197    /// Create a [`Core`] from a set of CPUs with the given IDs.
1198    pub fn test_core(
1199        id: usize,
1200        cpus: BTreeMap<usize, Arc<Cpu>>,
1201        llc_id: usize,
1202        node_id: usize,
1203    ) -> Core {
1204        let mut span = Cpumask::new();
1205        for &cpu_id in cpus.keys() {
1206            span.set_cpu(cpu_id).unwrap();
1207        }
1208        Core {
1209            id,
1210            kernel_id: id,
1211            cluster_id: 0,
1212            cpus,
1213            span,
1214            core_type: CoreType::Big { turbo: false },
1215            llc_id,
1216            node_id,
1217        }
1218    }
1219
1220    /// Create an [`Llc`] from a set of cores with the given IDs.
1221    pub fn test_llc(id: usize, cores: BTreeMap<usize, Arc<Core>>, node_id: usize) -> Llc {
1222        let mut span = Cpumask::new();
1223        for core in cores.values() {
1224            for &cpu_id in core.cpus.keys() {
1225                span.set_cpu(cpu_id).unwrap();
1226            }
1227        }
1228        Llc {
1229            id,
1230            kernel_id: id,
1231            cores,
1232            span,
1233            node_id,
1234            all_cpus: BTreeMap::new(), // filled by instantiate()
1235        }
1236    }
1237
1238    /// Create a [`Node`] from a set of LLCs with the given IDs.
1239    pub fn test_node(id: usize, llcs: BTreeMap<usize, Arc<Llc>>, nr_nodes: usize) -> Node {
1240        let mut span = Cpumask::new();
1241        for llc in llcs.values() {
1242            for core in llc.cores.values() {
1243                for &cpu_id in core.cpus.keys() {
1244                    span.set_cpu(cpu_id).unwrap();
1245                }
1246            }
1247        }
1248        Node {
1249            id,
1250            distance: vec![10; nr_nodes],
1251            llcs,
1252            span,
1253            all_cores: BTreeMap::new(), // filled by instantiate()
1254            all_cpus: BTreeMap::new(),  // filled by instantiate()
1255            #[cfg(feature = "gpu-topology")]
1256            gpus: BTreeMap::new(),
1257        }
1258    }
1259
1260    /// Build a synthetic [`Topology`] with the specified dimensions.
1261    ///
1262    /// Returns `(topology, total_cpu_count)`. CPU IDs are assigned
1263    /// sequentially starting from 0: node 0's LLCs get the lowest IDs,
1264    /// then node 1, etc.
1265    ///
1266    /// Sets the Cpumask test width override to `total_cpus` so that all
1267    /// masks created during the test have consistent width.
1268    pub fn make_test_topo(
1269        nr_nodes: usize,
1270        llcs_per_node: usize,
1271        cores_per_llc: usize,
1272        hts_per_core: usize,
1273    ) -> (Topology, usize) {
1274        let total_cpus = nr_nodes * llcs_per_node * cores_per_llc * hts_per_core;
1275        set_cpumask_test_width(total_cpus);
1276
1277        let mut cpu_id = 0usize;
1278        let mut core_id = 0usize;
1279        let mut llc_id = 0usize;
1280        let mut nodes = BTreeMap::new();
1281
1282        for node_idx in 0..nr_nodes {
1283            let mut llcs = BTreeMap::new();
1284            for _ in 0..llcs_per_node {
1285                let mut cores = BTreeMap::new();
1286                for _ in 0..cores_per_llc {
1287                    let mut cpus = BTreeMap::new();
1288                    for _ in 0..hts_per_core {
1289                        cpus.insert(
1290                            cpu_id,
1291                            Arc::new(test_cpu(cpu_id, core_id, llc_id, node_idx)),
1292                        );
1293                        cpu_id += 1;
1294                    }
1295                    cores.insert(
1296                        core_id,
1297                        Arc::new(test_core(core_id, cpus, llc_id, node_idx)),
1298                    );
1299                    core_id += 1;
1300                }
1301                llcs.insert(llc_id, Arc::new(test_llc(llc_id, cores, node_idx)));
1302                llc_id += 1;
1303            }
1304            nodes.insert(node_idx, test_node(node_idx, llcs, nr_nodes));
1305        }
1306
1307        let mut span = Cpumask::new();
1308        for i in 0..total_cpus {
1309            span.set_cpu(i).unwrap();
1310        }
1311
1312        (Topology::instantiate(span, nodes).unwrap(), total_cpus)
1313    }
1314
1315    /// Create a [`Cpumask`] from a list of set CPU IDs.
1316    pub fn mask_from_bits(_total: usize, bits: &[usize]) -> Cpumask {
1317        let mut mask = Cpumask::new();
1318        for &b in bits {
1319            mask.set_cpu(b).unwrap();
1320        }
1321        mask
1322    }
1323}
1324
1325#[cfg(test)]
1326mod tests {
1327    use super::testutils::*;
1328    use super::*;
1329
1330    fn grid_output(topo: &Topology, cpumask: &Cpumask) -> String {
1331        let mut buf = Vec::new();
1332        topo.format_cpumask_grid(&mut buf, cpumask, "    ", 80)
1333            .unwrap();
1334        String::from_utf8(buf).unwrap()
1335    }
1336
1337    #[test]
1338    fn test_grid_2node_2llc_3core_2ht() {
1339        // 2 nodes, 2 LLCs/node, 3 cores/LLC, 2 HTs/core = 24 CPUs
1340        let (topo, total) = make_test_topo(2, 2, 3, 2);
1341        assert_eq!(total, 24);
1342
1343        // Set some specific CPUs:
1344        // Node0 LLC0: core0(cpu0,1) core1(cpu2,3) core2(cpu4,5)
1345        // Node0 LLC1: core3(cpu6,7) core4(cpu8,9) core5(cpu10,11)
1346        // Node1 LLC2: core6(cpu12,13) core7(cpu14,15) core8(cpu16,17)
1347        // Node1 LLC3: core9(cpu18,19) core10(cpu20,21) core11(cpu22,23)
1348        //
1349        // Set: cpu1(core0 2nd HT), cpu2+3(core1 both), cpu12(core6 1st HT)
1350        let cpumask = mask_from_bits(total, &[1, 2, 3, 12]);
1351
1352        let output = grid_output(&topo, &cpumask);
1353        // Node0: LLC0=[▄ █ ░] LLC1=[░ ░ ░]
1354        // Node1: LLC2=[▀ ░ ░] LLC3=[░ ░ ░]
1355        assert!(output.contains("N0 L00:"));
1356        assert!(output.contains("N1 L02:"));
1357        // LLC0=▄█░, LLC1=░░░ separated by |
1358        assert!(output.contains("▄█░|░░░"));
1359        // LLC2=▀░░, LLC3=░░░ separated by |
1360        assert!(output.contains("▀░░|░░░"));
1361
1362        // Core count: cores 0,1,6 have at least one CPU set = 3
1363        assert_eq!(topo.cpumask_nr_cores(&cpumask), 3);
1364    }
1365
1366    #[test]
1367    fn test_grid_empty_cpumask() {
1368        let (topo, total) = make_test_topo(1, 2, 3, 2);
1369        let cpumask = mask_from_bits(total, &[]);
1370        let output = grid_output(&topo, &cpumask);
1371        // All chars should be ░
1372        assert!(!output.contains('█'));
1373        assert!(!output.contains('▀'));
1374        assert!(!output.contains('▄'));
1375        assert!(output.contains('░'));
1376        assert_eq!(topo.cpumask_nr_cores(&cpumask), 0);
1377    }
1378
1379    #[test]
1380    fn test_grid_full_cpumask() {
1381        let (topo, total) = make_test_topo(1, 2, 3, 2);
1382        let cpumask = mask_from_bits(total, &(0..total).collect::<Vec<_>>());
1383        let output = grid_output(&topo, &cpumask);
1384        // All chars should be █
1385        assert!(!output.contains('░'));
1386        assert!(!output.contains('▀'));
1387        assert!(!output.contains('▄'));
1388        assert!(output.contains('█'));
1389        assert_eq!(topo.cpumask_nr_cores(&cpumask), 6);
1390    }
1391
1392    #[test]
1393    fn test_grid_mixed_ht() {
1394        // 1 node, 1 LLC, 4 cores, 2 HTs = 8 CPUs
1395        let (topo, total) = make_test_topo(1, 1, 4, 2);
1396        // core0: cpu0,1  core1: cpu2,3  core2: cpu4,5  core3: cpu6,7
1397        // Set: cpu0 only (▀), cpu3 only (▄), cpu4+5 (█), none on core3 (░)
1398        let cpumask = mask_from_bits(total, &[0, 3, 4, 5]);
1399        let output = grid_output(&topo, &cpumask);
1400        assert!(output.contains('▀'));
1401        assert!(output.contains('▄'));
1402        assert!(output.contains('█'));
1403        assert!(output.contains('░'));
1404    }
1405
1406    #[test]
1407    fn test_grid_single_node() {
1408        let (topo, total) = make_test_topo(1, 1, 2, 2);
1409        let cpumask = mask_from_bits(total, &[0, 1]);
1410        let output = grid_output(&topo, &cpumask);
1411        assert!(output.contains("N0 L00:"));
1412        assert!(!output.contains("N1"));
1413    }
1414
1415    #[test]
1416    fn test_grid_overflow_wrap() {
1417        // 1 node, 12 LLCs, 4 cores each, 2 HTs = many characters
1418        // 12 LLCs grouped by 4 = 3 groups per line, should wrap
1419        let (topo, total) = make_test_topo(1, 12, 4, 2);
1420        let cpumask = mask_from_bits(total, &[0]);
1421        let mut buf = Vec::new();
1422        topo.format_cpumask_grid(&mut buf, &cpumask, "    ", 60)
1423            .unwrap();
1424        let output = String::from_utf8(buf).unwrap();
1425        // Should have multiple lines for node 0 due to wrapping
1426        let lines: Vec<&str> = output.lines().collect();
1427        assert!(
1428            lines.len() > 1,
1429            "Expected wrapping with narrow width, got {} lines",
1430            lines.len()
1431        );
1432    }
1433
1434    #[test]
1435    fn test_grid_smt_off() {
1436        // 1 node, 1 LLC, 4 cores, 1 HT = no SMT
1437        let (topo, total) = make_test_topo(1, 1, 4, 1);
1438        // core0: cpu0, core1: cpu1, core2: cpu2, core3: cpu3
1439        let cpumask = mask_from_bits(total, &[0, 2]);
1440        let output = grid_output(&topo, &cpumask);
1441        // Only █ and ░ should appear
1442        assert!(output.contains('█'));
1443        assert!(output.contains('░'));
1444        assert!(!output.contains('▀'));
1445        assert!(!output.contains('▄'));
1446    }
1447
1448    #[test]
1449    fn test_grid_4way_smt() {
1450        // 1 node, 1 LLC, 2 cores, 4 HTs = 8 CPUs
1451        let (topo, total) = make_test_topo(1, 1, 2, 4);
1452        // core0: cpu0-3, core1: cpu4-7
1453        // Set all of core0 → █, set 2 of core1 → ▄ (partial)
1454        let cpumask = mask_from_bits(total, &[0, 1, 2, 3, 4, 5]);
1455        let output = grid_output(&topo, &cpumask);
1456        assert!(output.contains('█')); // core0: all 4 set
1457        assert!(output.contains('▄')); // core1: partial (2 of 4)
1458    }
1459
1460    #[test]
1461    fn test_cpumask_header() {
1462        let (topo, total) = make_test_topo(1, 1, 4, 2);
1463        // 4 cores, 8 CPUs. Set cpu0,1,2 (2 cores touched)
1464        let cpumask = mask_from_bits(total, &[0, 1, 2]);
1465        let header = topo.format_cpumask_header(&cpumask, 5, 10);
1466        assert!(header.contains("cpus=  3(  2c)"));
1467        assert!(header.contains("[  5, 10]"));
1468    }
1469}