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