Skip to main content

themis/topology/cpu/
mod.rs

1//! CPU topology snapshot and accessors.
2
3mod cache;
4#[cfg(all(feature = "std", target_os = "linux"))]
5mod cpulist;
6mod detect;
7mod tables;
8
9use super::types::{CacheLevel, NumaNode};
10use crate::law::{MemoryTier, NumaNodeId, TopologyEpoch};
11
12#[cfg(not(feature = "std"))]
13extern crate alloc;
14
15#[cfg(not(feature = "std"))]
16use alloc::boxed::Box;
17
18#[cfg(not(feature = "std"))]
19use alloc::vec;
20
21pub(crate) use cache::detect_cache_levels;
22#[cfg(all(feature = "std", target_os = "linux"))]
23pub(crate) use cpulist::parse_cpu_list;
24pub use tables::{build_adjacent_nodes, build_node_to_index};
25#[cfg(any(test, feature = "std"))]
26pub use tables::{build_default_distance_row, build_processor_to_node};
27pub use tables::{LOCAL_DISTANCE, REMOTE_DISTANCE};
28
29/// CPU topology snapshot.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct CpuTopology {
32    /// Snapshot epoch.
33    pub(crate) epoch: TopologyEpoch,
34    pub(crate) numa_nodes: Box<[NumaNode]>,
35    pub(crate) processor_to_node: Box<[NumaNodeId]>,
36    pub(crate) node_to_index: Box<[usize]>,
37    pub(crate) adjacent_nodes: Box<[NumaNodeId]>,
38    pub(crate) logical_processors: usize,
39    pub(crate) cache_levels: Option<Box<[CacheLevel]>>,
40}
41
42impl CpuTopology {
43    /// Creates a single-node topology.
44    #[must_use]
45    pub fn single_node(logical_processors: usize) -> Self {
46        let logical_processors = logical_processors.max(1);
47        let processors: Box<[u32]> = (0..logical_processors as u32).collect();
48        let node_id = NumaNodeId::ZERO;
49        let numa_nodes: Box<[NumaNode]> = Box::new([NumaNode {
50            id: node_id,
51            processors,
52            distances: Box::new([LOCAL_DISTANCE]),
53            memory_tier: MemoryTier::Dram,
54        }]);
55
56        Self {
57            epoch: TopologyEpoch::INITIAL,
58            processor_to_node: vec![node_id; logical_processors].into_boxed_slice(),
59            node_to_index: build_node_to_index(&numa_nodes),
60            adjacent_nodes: build_adjacent_nodes(&numa_nodes),
61            numa_nodes,
62            logical_processors,
63            cache_levels: None,
64        }
65    }
66
67    /// Construct a topology from primary fields for testing.
68    ///
69    /// `node_to_index` and `adjacent_nodes` are derived from `numa_nodes`.
70    ///
71    /// Gated on `feature = "testing"` (not just `cfg(test)`) because integration
72    /// tests in `tests/` consume the lib as a regular dependency; `cfg(test)` only
73    /// activates when the lib itself is the test target, not when it is depended on.
74    #[cfg(any(test, feature = "testing"))]
75    #[must_use]
76    pub fn new_for_test(
77        epoch: TopologyEpoch,
78        numa_nodes: Box<[NumaNode]>,
79        processor_to_node: Box<[NumaNodeId]>,
80        logical_processors: usize,
81        cache_levels: Option<Box<[CacheLevel]>>,
82    ) -> Self {
83        let node_to_index = build_node_to_index(&numa_nodes);
84        let adjacent_nodes = build_adjacent_nodes(&numa_nodes);
85        Self {
86            epoch,
87            numa_nodes,
88            processor_to_node,
89            node_to_index,
90            adjacent_nodes,
91            logical_processors,
92            cache_levels,
93        }
94    }
95
96    /// Returns the snapshot epoch.
97    #[must_use]
98    pub const fn epoch(&self) -> TopologyEpoch {
99        self.epoch
100    }
101
102    /// Returns the NUMA node table.
103    #[must_use]
104    pub fn numa_nodes(&self) -> &[NumaNode] {
105        &self.numa_nodes
106    }
107
108    /// Returns the platform-reported cache hierarchy table.
109    ///
110    /// # Provenance
111    ///
112    /// `None` means the platform did not report a complete cache hierarchy.
113    /// The single-node constructor never fabricates cache values. Linux reads
114    /// cache-index records from sysfs, and Windows reads
115    /// `GetLogicalProcessorInformationEx`; malformed or unavailable platform
116    /// data remains typed absence. Consumers that tile on cache size must
117    /// preserve that absence instead of substituting a machine-independent
118    /// guess.
119    #[must_use]
120    pub fn cache_levels(&self) -> Option<&[CacheLevel]> {
121        self.cache_levels.as_deref()
122    }
123
124    /// Returns the logical processor count.
125    #[must_use]
126    pub const fn logical_processors(&self) -> usize {
127        self.logical_processors
128    }
129
130    /// Returns the NUMA node for a processor.
131    #[must_use]
132    pub fn processor_to_numa_node(&self, processor: u32) -> Option<NumaNodeId> {
133        self.processor_to_node
134            .get(processor as usize)
135            .copied()
136            .filter(|&node_id| node_id != NumaNodeId::INVALID)
137    }
138
139    /// Iterates over known processor-to-node mappings.
140    #[must_use = "iterators are lazy; consume the returned mapping iterator"]
141    pub fn processor_node_pairs(&self) -> impl Iterator<Item = (u32, NumaNodeId)> + '_ {
142        self.processor_to_node
143            .iter()
144            .enumerate()
145            .filter_map(|(processor, &node)| {
146                if node != NumaNodeId::INVALID {
147                    Some((processor as u32, node))
148                } else {
149                    None
150                }
151            })
152    }
153
154    /// Returns node distance (ACPI SLIT convention: `10` = local, higher =
155    /// farther).
156    ///
157    /// # Provenance
158    ///
159    /// Only the **Linux** backend reads real inter-node distances (from
160    /// `/sys/devices/system/node/nodeN/distance`), falling back to the
161    /// synthetic `10`/`20` matrix on read failure. The **Windows** backend has
162    /// no distance API without `GetLogicalProcessorInformationEx` relative-
163    /// distance parsing, so it always returns the synthetic `10` (local) /
164    /// `20` (remote) — uniform regardless of true inter-node latency. Consumers
165    /// that weight placement by distance must treat a Windows result as a
166    /// two-tier local/remote hint, not a measured latency.
167    #[must_use]
168    pub fn distance(&self, from: NumaNodeId, to: NumaNodeId) -> u32 {
169        match (self.node_index(from), self.node_index(to)) {
170            (Some(from_index), Some(to_index)) => self
171                .numa_nodes
172                .get(from_index)
173                .and_then(|node| {
174                    let max_node_id = self.node_to_index.len().saturating_sub(1);
175                    let idx = if node.distances.len() > max_node_id {
176                        to.index()
177                    } else {
178                        to_index
179                    };
180                    node.distances.get(idx).copied()
181                })
182                .unwrap_or_else(|| tables::default_distance(from_index, to_index)),
183            _ => {
184                if from == to {
185                    LOCAL_DISTANCE
186                } else {
187                    REMOTE_DISTANCE
188                }
189            }
190        }
191    }
192
193    /// Returns the compact topology index for a NUMA node ID.
194    #[must_use]
195    pub fn node_index(&self, node_id: NumaNodeId) -> Option<usize> {
196        self.node_to_index
197            .get(node_id.index())
198            .copied()
199            .filter(|&index| index != usize::MAX)
200    }
201
202    /// Returns adjacent nodes sorted by distance.
203    #[must_use]
204    pub fn adjacent_nodes(&self, node_id: NumaNodeId) -> &[NumaNodeId] {
205        if let Some(index) = self.node_index(node_id) {
206            let node_count = self.numa_nodes.len();
207            if node_count <= 1 {
208                return &[];
209            }
210            let stride = node_count - 1;
211            let start = index * stride;
212            let end = start + stride;
213            self.adjacent_nodes.get(start..end).unwrap_or(&[])
214        } else {
215            &[]
216        }
217    }
218}
219
220fn logical_processor_count() -> usize {
221    #[cfg(feature = "std")]
222    {
223        std::thread::available_parallelism()
224            .map(usize::from)
225            .unwrap_or(1)
226    }
227
228    #[cfg(not(feature = "std"))]
229    {
230        1
231    }
232}