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::Cpumask;
73use crate::cpumask::read_cpulist;
74use crate::misc::read_file_byte;
75use crate::misc::read_file_usize_vec;
76use crate::misc::read_from_file;
77use anyhow::Result;
78use anyhow::bail;
79use glob::glob;
80use sscanf::sscanf;
81use std::collections::BTreeMap;
82use std::path::Path;
83use std::path::PathBuf;
84use std::sync::Arc;
85
86#[cfg(feature = "gpu-topology")]
87use crate::gpu::{Gpu, GpuIndex, create_gpus};
88
89lazy_static::lazy_static! {
90    /// The maximum possible number of CPU IDs in the system. As mentioned
91    /// above, this is different than the number of possible CPUs on the
92    /// system (though very seldom is). This number may differ from the
93    /// number of possible CPUs on the system when e.g. there are fully
94    /// disabled CPUs in the middle of the range of possible CPUs (i.e. CPUs
95    /// that may not be onlined).
96    pub static ref NR_CPU_IDS: usize = read_cpu_ids().unwrap().last().unwrap() + 1;
97
98    /// The number of possible CPUs that may be active on the system. Note
99    /// that this value is separate from the number of possible _CPU IDs_ in
100    /// the system, as there may be gaps in what CPUs are allowed to be
101    /// onlined. For example, some BIOS implementations may report spans of
102    /// disabled CPUs that may not be onlined, whose IDs are lower than the
103    /// IDs of other CPUs that may be onlined.
104    pub static ref NR_CPUS_POSSIBLE: usize = libbpf_rs::num_possible_cpus().unwrap();
105}
106
107#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
108pub enum CoreType {
109    Big { turbo: bool },
110    Little,
111}
112
113#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
114pub struct Cpu {
115    pub id: usize,
116    pub min_freq: usize,
117    pub max_freq: usize,
118    /// Base operational frqeuency. Only available on Intel Turbo Boost
119    /// CPUs. If not available, this will simply return maximum frequency.
120    pub base_freq: usize,
121    /// The best-effort guessing of cpu_capacity scaled to 1024.
122    pub cpu_capacity: usize,
123    pub smt_level: usize,
124    /// CPU idle resume latency
125    pub pm_qos_resume_latency_us: usize,
126    pub trans_lat_ns: usize,
127    pub l2_id: usize,
128    pub l3_id: usize,
129    /// Per-CPU cache size of all levels.
130    pub cache_size: usize,
131    pub core_type: CoreType,
132
133    /// Ancestor IDs.
134    pub core_id: usize,
135    pub llc_id: usize,
136    pub node_id: usize,
137    pub package_id: usize,
138    pub cluster_id: isize,
139}
140
141#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
142pub struct Core {
143    /// Monotonically increasing unique id
144    pub id: usize,
145    /// The sysfs value of core_id
146    pub kernel_id: usize,
147    pub cluster_id: isize,
148    pub cpus: BTreeMap<usize, Arc<Cpu>>,
149    /// Cpumask of all CPUs in this core.
150    pub span: Cpumask,
151    pub core_type: CoreType,
152
153    /// Ancestor IDs.
154    pub llc_id: usize,
155    pub node_id: usize,
156}
157
158#[derive(Debug, Clone)]
159pub struct Llc {
160    /// Monotonically increasing unique id
161    pub id: usize,
162    /// The kernel id of the llc
163    pub kernel_id: usize,
164    pub cores: BTreeMap<usize, Arc<Core>>,
165    /// Cpumask of all CPUs in this llc.
166    pub span: Cpumask,
167
168    /// Ancestor IDs.
169    pub node_id: usize,
170
171    /// Skip indices to access lower level members easily.
172    pub all_cpus: BTreeMap<usize, Arc<Cpu>>,
173}
174
175#[derive(Debug, Clone)]
176pub struct Node {
177    pub id: usize,
178    pub distance: Vec<usize>,
179    pub llcs: BTreeMap<usize, Arc<Llc>>,
180    /// Cpumask of all CPUs in this node.
181    pub span: Cpumask,
182
183    /// Skip indices to access lower level members easily.
184    pub all_cores: BTreeMap<usize, Arc<Core>>,
185    pub all_cpus: BTreeMap<usize, Arc<Cpu>>,
186
187    #[cfg(feature = "gpu-topology")]
188    pub gpus: BTreeMap<GpuIndex, Gpu>,
189}
190
191#[derive(Debug)]
192pub struct Topology {
193    pub nodes: BTreeMap<usize, Node>,
194    /// Cpumask all CPUs in the system.
195    pub span: Cpumask,
196    /// True if SMT is enabled in the system, false otherwise.
197    pub smt_enabled: bool,
198
199    /// Skip indices to access lower level members easily.
200    pub all_llcs: BTreeMap<usize, Arc<Llc>>,
201    pub all_cores: BTreeMap<usize, Arc<Core>>,
202    pub all_cpus: BTreeMap<usize, Arc<Cpu>>,
203}
204
205impl Topology {
206    fn instantiate(span: Cpumask, mut nodes: BTreeMap<usize, Node>) -> Result<Self> {
207        // Build skip indices prefixed with all_ for easy lookups. As Arc
208        // objects can only be modified while there's only one reference,
209        // skip indices must be built from bottom to top.
210        let mut topo_llcs = BTreeMap::new();
211        let mut topo_cores = BTreeMap::new();
212        let mut topo_cpus = BTreeMap::new();
213
214        for (_node_id, node) in nodes.iter_mut() {
215            let mut node_cores = BTreeMap::new();
216            let mut node_cpus = BTreeMap::new();
217
218            for (&llc_id, llc) in node.llcs.iter_mut() {
219                let llc_mut = Arc::get_mut(llc).unwrap();
220                let mut llc_cpus = BTreeMap::new();
221
222                for (&core_id, core) in llc_mut.cores.iter_mut() {
223                    let core_mut = Arc::get_mut(core).unwrap();
224                    let smt_level = core_mut.cpus.len();
225
226                    for (&cpu_id, cpu) in core_mut.cpus.iter_mut() {
227                        let cpu_mut = Arc::get_mut(cpu).unwrap();
228                        cpu_mut.smt_level = smt_level;
229
230                        if topo_cpus
231                            .insert(cpu_id, cpu.clone())
232                            .or(node_cpus.insert(cpu_id, cpu.clone()))
233                            .or(llc_cpus.insert(cpu_id, cpu.clone()))
234                            .is_some()
235                        {
236                            bail!("Duplicate CPU ID {}", cpu_id);
237                        }
238                    }
239
240                    // Note that in some weird architectures, core ids can be
241                    // duplicated in different LLC domains.
242                    topo_cores
243                        .insert(core_id, core.clone())
244                        .or(node_cores.insert(core_id, core.clone()));
245                }
246
247                llc_mut.all_cpus = llc_cpus;
248
249                if topo_llcs.insert(llc_id, llc.clone()).is_some() {
250                    bail!("Duplicate LLC ID {}", llc_id);
251                }
252            }
253
254            node.all_cores = node_cores;
255            node.all_cpus = node_cpus;
256        }
257
258        Ok(Topology {
259            nodes,
260            span,
261            smt_enabled: is_smt_active().unwrap_or(false),
262            all_llcs: topo_llcs,
263            all_cores: topo_cores,
264            all_cpus: topo_cpus,
265        })
266    }
267
268    /// Build a complete host Topology
269    pub fn new() -> Result<Topology> {
270        let span = cpus_online()?;
271        let mut topo_ctx = TopoCtx::new();
272        // If the kernel is compiled with CONFIG_NUMA, then build a topology
273        // from the NUMA hierarchy in sysfs. Otherwise, just make a single
274        // default node of ID 0 which contains all cores.
275        let nodes = if Path::new("/sys/devices/system/node").exists() {
276            create_numa_nodes(&span, &mut topo_ctx)?
277        } else {
278            create_default_node(&span, &mut topo_ctx, false)?
279        };
280
281        Self::instantiate(span, nodes)
282    }
283
284    pub fn with_flattened_llc_node() -> Result<Topology> {
285        let span = cpus_online()?;
286        let mut topo_ctx = TopoCtx::new();
287        let nodes = create_default_node(&span, &mut topo_ctx, true)?;
288        Self::instantiate(span, nodes)
289    }
290
291    /// Get a vec of all GPUs on the hosts.
292    #[cfg(feature = "gpu-topology")]
293    pub fn gpus(&self) -> BTreeMap<GpuIndex, &Gpu> {
294        let mut gpus = BTreeMap::new();
295        for node in self.nodes.values() {
296            for (idx, gpu) in &node.gpus {
297                gpus.insert(*idx, gpu);
298            }
299        }
300        gpus
301    }
302
303    /// Returns whether the Topology has a hybrid architecture of big and little cores.
304    pub fn has_little_cores(&self) -> bool {
305        self.all_cores
306            .values()
307            .any(|c| c.core_type == CoreType::Little)
308    }
309
310    /// Returns a vector that maps the index of each logical CPU to the
311    /// sibling CPU. This represents the "next sibling" CPU within a package
312    /// in systems that support SMT. The sibling CPU is the other logical
313    /// CPU that shares the physical resources of the same physical core.
314    ///
315    /// Assuming each core holds exactly at most two cpus.
316    pub fn sibling_cpus(&self) -> Vec<i32> {
317        let mut sibling_cpu = vec![-1i32; *NR_CPUS_POSSIBLE];
318        for core in self.all_cores.values() {
319            let mut first = -1i32;
320            for &cpu in core.cpus.keys() {
321                if first < 0 {
322                    first = cpu as i32;
323                } else {
324                    sibling_cpu[first as usize] = cpu as i32;
325                    sibling_cpu[cpu] = first;
326                    break;
327                }
328            }
329        }
330        sibling_cpu
331    }
332}
333
334/******************************************************
335 * Helper structs/functions for creating the Topology *
336 ******************************************************/
337/// TopoCtx is a helper struct used to build a topology.
338struct TopoCtx {
339    /// Mapping of NUMA node core ids
340    node_core_kernel_ids: BTreeMap<(usize, usize, usize), usize>,
341    /// Mapping of NUMA node LLC ids
342    node_llc_kernel_ids: BTreeMap<(usize, usize, usize), usize>,
343    /// Mapping of L2 ids
344    l2_ids: BTreeMap<String, usize>,
345    /// Mapping of L3 ids
346    l3_ids: BTreeMap<String, usize>,
347}
348
349impl TopoCtx {
350    fn new() -> TopoCtx {
351        let core_kernel_ids = BTreeMap::new();
352        let llc_kernel_ids = BTreeMap::new();
353        let l2_ids = BTreeMap::new();
354        let l3_ids = BTreeMap::new();
355        TopoCtx {
356            node_core_kernel_ids: core_kernel_ids,
357            node_llc_kernel_ids: llc_kernel_ids,
358            l2_ids,
359            l3_ids,
360        }
361    }
362}
363
364fn cpus_online() -> Result<Cpumask> {
365    let path = "/sys/devices/system/cpu/online";
366    let online = std::fs::read_to_string(path)?;
367    Cpumask::from_cpulist(&online)
368}
369
370fn get_cache_id(topo_ctx: &mut TopoCtx, cache_level_path: &PathBuf, cache_level: usize) -> usize {
371    // Check if the cache id is already cached
372    let id_map = match cache_level {
373        2 => &mut topo_ctx.l2_ids,
374        3 => &mut topo_ctx.l3_ids,
375        _ => return usize::MAX,
376    };
377
378    let path = &cache_level_path.join("shared_cpu_list");
379    let key = match std::fs::read_to_string(path) {
380        Ok(key) => key,
381        Err(_) => return usize::MAX,
382    };
383
384    let id = *id_map.get(&key).unwrap_or(&usize::MAX);
385    if id != usize::MAX {
386        return id;
387    }
388
389    // In case of a cache miss, try to get the id from the sysfs first.
390    let id = read_from_file(&cache_level_path.join("id")).unwrap_or(usize::MAX);
391    if id != usize::MAX {
392        // Keep the id in the map
393        id_map.insert(key, id);
394        return id;
395    }
396
397    // If the id file does not exist, assign an id and keep it in the map.
398    let id = id_map.len();
399    id_map.insert(key, id);
400
401    id
402}
403
404fn get_per_cpu_cache_size(cache_path: &PathBuf) -> Result<usize> {
405    let path_str = cache_path.to_str().unwrap();
406    let paths = glob(&(path_str.to_owned() + "/index[0-9]*"))?;
407    let mut tot_size = 0;
408
409    for index in paths.filter_map(Result::ok) {
410        // If there is no size information under sysfs (e.g., many ARM SoCs),
411        // give 1024 as a default value. 1024 is small enough compared to the
412        // real cache size of the CPU, but it is large enough to give a penalty
413        // when multiple CPUs share the cache.
414        let size = read_file_byte(&index.join("size")).unwrap_or(1024_usize);
415        let cpulist: String = read_from_file(&index.join("shared_cpu_list"))?;
416        let num_cpus = read_cpulist(&cpulist)?.len();
417        tot_size += size / num_cpus;
418    }
419
420    Ok(tot_size)
421}
422
423#[allow(clippy::too_many_arguments)]
424fn create_insert_cpu(
425    id: usize,
426    node: &mut Node,
427    online_mask: &Cpumask,
428    topo_ctx: &mut TopoCtx,
429    big_little: bool,
430    avg_cpu_freq: Option<(usize, usize)>,
431    capacity_src: Option<(String, usize, usize)>,
432    flatten_llc: bool,
433) -> Result<()> {
434    // CPU is offline. The Topology hierarchy is read-only, and assumes
435    // that hotplug will cause the scheduler to restart. Thus, we can
436    // just skip this CPU altogether.
437    if !online_mask.test_cpu(id) {
438        return Ok(());
439    }
440
441    let cpu_str = format!("/sys/devices/system/cpu/cpu{}", id);
442    let cpu_path = Path::new(&cpu_str);
443
444    // Physical core ID
445    let top_path = cpu_path.join("topology");
446    let core_kernel_id = read_from_file(&top_path.join("core_id"))?;
447    let package_id = read_from_file(&top_path.join("physical_package_id"))?;
448    let cluster_id = read_from_file(&top_path.join("cluster_id"))?;
449
450    // Evaluate L2, L3 and LLC cache IDs.
451    //
452    // Use ID 0 if we fail to detect the cache hierarchy. This seems to happen on certain SKUs, so
453    // if there's no cache information then we have no option but to assume a single unified cache
454    // per node.
455    let cache_path = cpu_path.join("cache");
456    let l2_id = get_cache_id(topo_ctx, &cache_path.join(format!("index{}", 2)), 2);
457    let l3_id = get_cache_id(topo_ctx, &cache_path.join(format!("index{}", 3)), 3);
458    let llc_kernel_id = if flatten_llc {
459        0
460    } else if l3_id == usize::MAX {
461        l2_id
462    } else {
463        l3_id
464    };
465
466    // Per-CPU cache size
467    let cache_size = get_per_cpu_cache_size(&cache_path).unwrap_or(0_usize);
468
469    // Min and max frequencies. If the kernel is not compiled with
470    // CONFIG_CPU_FREQ, just assume 0 for both frequencies.
471    let freq_path = cpu_path.join("cpufreq");
472    let min_freq = read_from_file(&freq_path.join("scaling_min_freq")).unwrap_or(0_usize);
473    let max_freq = read_from_file(&freq_path.join("scaling_max_freq")).unwrap_or(0_usize);
474    let base_freq = read_from_file(&freq_path.join("base_frequency")).unwrap_or(max_freq);
475    let trans_lat_ns =
476        read_from_file(&freq_path.join("cpuinfo_transition_latency")).unwrap_or(0_usize);
477
478    // Cpu capacity
479    let (cap_suffix, _avg_rcap, max_rcap) = capacity_src.unwrap_or(("".to_string(), 1024, 1024));
480    let cap_path = cpu_path.join(cap_suffix);
481    let rcap = read_from_file(&cap_path).unwrap_or(max_rcap);
482    let cpu_capacity = (rcap * 1024) / max_rcap;
483
484    // Power management
485    let power_path = cpu_path.join("power");
486    let pm_qos_resume_latency_us =
487        read_from_file(&power_path.join("pm_qos_resume_latency_us")).unwrap_or(0_usize);
488
489    let num_llcs = topo_ctx.node_llc_kernel_ids.len();
490    let llc_id = topo_ctx
491        .node_llc_kernel_ids
492        .entry((node.id, package_id, llc_kernel_id))
493        .or_insert(num_llcs);
494
495    let llc = node.llcs.entry(*llc_id).or_insert(Arc::new(Llc {
496        id: *llc_id,
497        cores: BTreeMap::new(),
498        span: Cpumask::new(),
499        all_cpus: BTreeMap::new(),
500
501        node_id: node.id,
502        kernel_id: llc_kernel_id,
503    }));
504    let llc_mut = Arc::get_mut(llc).unwrap();
505
506    let core_type = if !big_little {
507        CoreType::Big { turbo: false }
508    } else {
509        match avg_cpu_freq {
510            Some((avg_base_freq, top_max_freq)) => {
511                if max_freq == top_max_freq {
512                    CoreType::Big { turbo: true }
513                } else if base_freq >= avg_base_freq {
514                    CoreType::Big { turbo: false }
515                } else {
516                    CoreType::Little
517                }
518            }
519            None => CoreType::Big { turbo: false },
520        }
521    };
522
523    let num_cores = topo_ctx.node_core_kernel_ids.len();
524    let core_id = topo_ctx
525        .node_core_kernel_ids
526        .entry((node.id, package_id, core_kernel_id))
527        .or_insert(num_cores);
528
529    let core = llc_mut.cores.entry(*core_id).or_insert(Arc::new(Core {
530        id: *core_id,
531        cpus: BTreeMap::new(),
532        span: Cpumask::new(),
533        core_type: core_type.clone(),
534
535        llc_id: *llc_id,
536        node_id: node.id,
537        kernel_id: core_kernel_id,
538        cluster_id: cluster_id,
539    }));
540    let core_mut = Arc::get_mut(core).unwrap();
541
542    core_mut.cpus.insert(
543        id,
544        Arc::new(Cpu {
545            id,
546            min_freq,
547            max_freq,
548            base_freq,
549            cpu_capacity,
550            smt_level: 0, // Will be initialized at instantiate().
551            pm_qos_resume_latency_us,
552            trans_lat_ns,
553            l2_id,
554            l3_id,
555            cache_size,
556            core_type: core_type.clone(),
557
558            core_id: *core_id,
559            llc_id: *llc_id,
560            node_id: node.id,
561            package_id,
562            cluster_id,
563        }),
564    );
565
566    if node.span.test_cpu(id) {
567        bail!("Node {} already had CPU {}", node.id, id);
568    }
569
570    // Update all of the devices' spans to include this CPU.
571    core_mut.span.set_cpu(id)?;
572    llc_mut.span.set_cpu(id)?;
573    node.span.set_cpu(id)?;
574
575    Ok(())
576}
577
578fn read_cpu_ids() -> Result<Vec<usize>> {
579    let mut cpu_ids = vec![];
580    let cpu_paths = glob("/sys/devices/system/cpu/cpu[0-9]*")?;
581    for cpu_path in cpu_paths.filter_map(Result::ok) {
582        let cpu_str = cpu_path.to_str().unwrap().trim();
583        match sscanf!(cpu_str, "/sys/devices/system/cpu/cpu{usize}") {
584            Ok(val) => cpu_ids.push(val),
585            Err(_) => {
586                bail!("Failed to parse cpu ID {}", cpu_str);
587            }
588        }
589    }
590    cpu_ids.sort();
591    Ok(cpu_ids)
592}
593
594fn cpu_capacity_source() -> Option<(String, usize, usize)> {
595    // Sources for guessing cpu_capacity under /sys/devices/system/cpu/cpuX.
596    // They should be ordered from the most precise to the least precise.
597    let sources = [
598        "cpufreq/amd_pstate_highest_perf",
599        "acpi_cppc/highest_perf",
600        "cpu_capacity",
601        "cpufreq/cpuinfo_max_freq",
602    ];
603
604    // Find the most precise source for cpu_capacity estimation.
605    let prefix = "/sys/devices/system/cpu/cpu0";
606    let mut raw_capacity = 0;
607    let mut suffix = sources[sources.len() - 1];
608    for src in sources {
609        let path_str = [prefix, src].join("/");
610        let path = Path::new(&path_str);
611        raw_capacity = read_from_file(&path).unwrap_or(0_usize);
612        if raw_capacity > 0 {
613            suffix = src;
614            break;
615        }
616    }
617    if raw_capacity == 0 {
618        return None;
619    }
620
621    // Find the max raw_capacity value for scaling to 1024.
622    let mut max_raw_capacity = 0;
623    let mut avg_raw_capacity = 0;
624    let mut nr_cpus = 0;
625    let cpu_paths = glob("/sys/devices/system/cpu/cpu[0-9]*").ok()?;
626    for cpu_path in cpu_paths.filter_map(Result::ok) {
627        let raw_capacity = read_from_file(&cpu_path.join(suffix)).unwrap_or(0_usize);
628        if max_raw_capacity < raw_capacity {
629            max_raw_capacity = raw_capacity;
630        }
631        avg_raw_capacity += raw_capacity;
632        nr_cpus += 1;
633    }
634    if max_raw_capacity == 0 {
635        return None;
636    }
637
638    Some((
639        suffix.to_string(),
640        avg_raw_capacity / nr_cpus,
641        max_raw_capacity,
642    ))
643}
644
645// Return the average base frequency across all CPUs and the highest maximum frequency.
646fn avg_cpu_freq() -> Option<(usize, usize)> {
647    let mut top_max_freq = 0;
648    let mut avg_base_freq = 0;
649    let mut nr_cpus = 0;
650    let cpu_paths = glob("/sys/devices/system/cpu/cpu[0-9]*").ok()?;
651    for cpu_path in cpu_paths.filter_map(Result::ok) {
652        let freq_path = cpu_path.join("cpufreq");
653        let max_freq = read_from_file(&freq_path.join("scaling_max_freq")).unwrap_or(0_usize);
654        let base_freq = read_from_file(&freq_path.join("base_frequency")).unwrap_or(max_freq);
655        if base_freq > 0 {
656            if max_freq > top_max_freq {
657                top_max_freq = max_freq;
658            }
659            avg_base_freq += base_freq;
660            nr_cpus += 1;
661        }
662    }
663    if avg_base_freq == 0 {
664        return None;
665    }
666    Some((avg_base_freq / nr_cpus, top_max_freq))
667}
668
669fn has_big_little() -> Option<bool> {
670    let mut clusters = std::collections::HashSet::new();
671
672    let cpu_paths = glob("/sys/devices/system/cpu/cpu[0-9]*").ok()?;
673    for cpu_path in cpu_paths.filter_map(Result::ok) {
674        let top_path = cpu_path.join("topology");
675        let cluster_id = read_from_file(&top_path.join("cluster_id")).unwrap_or(-1);
676        clusters.insert(cluster_id);
677    }
678
679    Some(clusters.len() > 1)
680}
681
682fn is_smt_active() -> Option<bool> {
683    let smt_on: u8 = read_from_file(Path::new("/sys/devices/system/cpu/smt/active")).ok()?;
684    Some(smt_on == 1)
685}
686
687fn create_default_node(
688    online_mask: &Cpumask,
689    topo_ctx: &mut TopoCtx,
690    flatten_llc: bool,
691) -> Result<BTreeMap<usize, Node>> {
692    let mut nodes = BTreeMap::<usize, Node>::new();
693
694    let mut node = Node {
695        id: 0,
696        distance: vec![],
697        llcs: BTreeMap::new(),
698        span: Cpumask::new(),
699        #[cfg(feature = "gpu-topology")]
700        gpus: BTreeMap::new(),
701        all_cores: BTreeMap::new(),
702        all_cpus: BTreeMap::new(),
703    };
704
705    #[cfg(feature = "gpu-topology")]
706    {
707        let system_gpus = create_gpus();
708        if let Some(gpus) = system_gpus.get(&0) {
709            for gpu in gpus {
710                node.gpus.insert(gpu.index, gpu.clone());
711            }
712        }
713    }
714
715    if !Path::new("/sys/devices/system/cpu").exists() {
716        bail!("/sys/devices/system/cpu sysfs node not found");
717    }
718
719    let capacity_src = cpu_capacity_source();
720    let avg_cpu_freq = avg_cpu_freq();
721    let big_little = has_big_little().unwrap_or(false);
722    let cpu_ids = read_cpu_ids()?;
723    for cpu_id in cpu_ids.iter() {
724        create_insert_cpu(
725            *cpu_id,
726            &mut node,
727            online_mask,
728            topo_ctx,
729            big_little,
730            avg_cpu_freq,
731            capacity_src.clone(),
732            flatten_llc,
733        )?;
734    }
735
736    nodes.insert(node.id, node);
737
738    Ok(nodes)
739}
740
741fn create_numa_nodes(
742    online_mask: &Cpumask,
743    topo_ctx: &mut TopoCtx,
744) -> Result<BTreeMap<usize, Node>> {
745    let mut nodes = BTreeMap::<usize, Node>::new();
746
747    #[cfg(feature = "gpu-topology")]
748    let system_gpus = create_gpus();
749
750    let numa_paths = glob("/sys/devices/system/node/node*")?;
751    for numa_path in numa_paths.filter_map(Result::ok) {
752        let numa_str = numa_path.to_str().unwrap().trim();
753        let node_id = match sscanf!(numa_str, "/sys/devices/system/node/node{usize}") {
754            Ok(val) => val,
755            Err(_) => {
756                bail!("Failed to parse NUMA node ID {}", numa_str);
757            }
758        };
759        let distance = read_file_usize_vec(
760            Path::new(&format!(
761                "/sys/devices/system/node/node{}/distance",
762                node_id
763            )),
764            ' ',
765        )?;
766        let mut node = Node {
767            id: node_id,
768            distance,
769            llcs: BTreeMap::new(),
770            span: Cpumask::new(),
771
772            all_cores: BTreeMap::new(),
773            all_cpus: BTreeMap::new(),
774
775            #[cfg(feature = "gpu-topology")]
776            gpus: BTreeMap::new(),
777        };
778
779        #[cfg(feature = "gpu-topology")]
780        {
781            if let Some(gpus) = system_gpus.get(&node_id) {
782                for gpu in gpus {
783                    node.gpus.insert(gpu.index, gpu.clone());
784                }
785            }
786        }
787
788        let cpu_pattern = numa_path.join("cpu[0-9]*");
789        let cpu_paths = glob(cpu_pattern.to_string_lossy().as_ref())?;
790        let big_little = has_big_little().unwrap_or(false);
791        let capacity_src = cpu_capacity_source();
792        let avg_cpu_freq = avg_cpu_freq();
793        let mut cpu_ids = vec![];
794        for cpu_path in cpu_paths.filter_map(Result::ok) {
795            let cpu_str = cpu_path.to_str().unwrap().trim();
796            let cpu_id = match sscanf!(cpu_str, "/sys/devices/system/node/node{usize}/cpu{usize}") {
797                Ok((_, val)) => val,
798                Err(_) => {
799                    bail!("Failed to parse cpu ID {}", cpu_str);
800                }
801            };
802            cpu_ids.push(cpu_id);
803        }
804        cpu_ids.sort();
805
806        for cpu_id in cpu_ids {
807            create_insert_cpu(
808                cpu_id,
809                &mut node,
810                online_mask,
811                topo_ctx,
812                big_little,
813                avg_cpu_freq,
814                capacity_src.clone(),
815                false,
816            )?;
817        }
818
819        nodes.insert(node.id, node);
820    }
821    Ok(nodes)
822}