axiolid_backend_cpu/
topology.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct CacheLevel {
33 pub level: u8,
35 pub bytes: usize,
37 pub line_bytes: usize,
39 pub shared_by: usize,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Default)]
52pub struct CpuTopology {
53 pub logical_cpus: Option<usize>,
55 pub caches: Vec<CacheLevel>,
57 pub heterogeneous_cores: Option<bool>,
63}
64
65impl CpuTopology {
66 pub fn detect() -> Self {
68 Self {
69 logical_cpus: detect_logical_cpus(),
70 caches: detect_caches(),
71 heterogeneous_cores: detect_heterogeneous(),
72 }
73 }
74
75 pub fn cache_bytes(&self, level: u8) -> Option<usize> {
77 self.caches
78 .iter()
79 .find(|cache| cache.level == level)
80 .map(|cache| cache.bytes)
81 }
82
83 pub fn last_level_bytes(&self) -> Option<usize> {
85 self.caches.iter().map(|cache| cache.bytes).max()
86 }
87
88 pub fn line_bytes(&self) -> Option<usize> {
90 self.caches.first().map(|cache| cache.line_bytes)
91 }
92
93 pub fn fits_in_cache(&self, bytes: usize, level: u8) -> Option<bool> {
98 self.cache_bytes(level).map(|capacity| bytes <= capacity)
99 }
100}
101
102fn detect_logical_cpus() -> Option<usize> {
108 std::thread::available_parallelism().ok().map(Into::into)
109}
110
111fn parse_size(text: &str) -> Option<usize> {
113 let text = text.trim();
114 let (digits, multiplier) = match text.as_bytes().last()? {
115 b'K' => (&text[..text.len() - 1], 1024),
116 b'M' => (&text[..text.len() - 1], 1024 * 1024),
117 b'G' => (&text[..text.len() - 1], 1024 * 1024 * 1024),
118 _ => (text, 1),
119 };
120 digits.parse::<usize>().ok()?.checked_mul(multiplier)
121}
122
123fn parse_cpu_list(text: &str) -> Option<usize> {
125 let mut total = 0usize;
126 for part in text.trim().split(',') {
127 if part.is_empty() {
128 continue;
129 }
130 match part.split_once('-') {
131 Some((low, high)) => {
132 let low: usize = low.trim().parse().ok()?;
133 let high: usize = high.trim().parse().ok()?;
134 total += high.checked_sub(low)?.checked_add(1)?;
135 }
136 None => total += 1,
137 }
138 }
139 (total > 0).then_some(total)
140}
141
142#[cfg(target_os = "linux")]
147fn detect_caches() -> Vec<CacheLevel> {
148 use std::fs::read_to_string;
149
150 let mut caches = Vec::new();
151 for index in 0..16 {
152 let base = format!("/sys/devices/system/cpu/cpu0/cache/index{index}");
153 let Ok(kind) = read_to_string(format!("{base}/type")) else {
154 break;
155 };
156 let kind = kind.trim();
157 if kind != "Data" && kind != "Unified" {
158 continue;
159 }
160 let level = read_to_string(format!("{base}/level"))
161 .ok()
162 .and_then(|text| text.trim().parse::<u8>().ok());
163 let bytes = read_to_string(format!("{base}/size"))
164 .ok()
165 .and_then(|text| parse_size(&text));
166 let line_bytes = read_to_string(format!("{base}/coherency_line_size"))
167 .ok()
168 .and_then(|text| text.trim().parse::<usize>().ok());
169 let shared_by = read_to_string(format!("{base}/shared_cpu_list"))
170 .ok()
171 .and_then(|text| parse_cpu_list(&text));
172 if let (Some(level), Some(bytes), Some(line_bytes), Some(shared_by)) =
175 (level, bytes, line_bytes, shared_by)
176 {
177 caches.push(CacheLevel {
178 level,
179 bytes,
180 line_bytes,
181 shared_by,
182 });
183 }
184 }
185 caches.sort_unstable_by_key(|cache| cache.level);
186 caches.dedup_by_key(|cache| cache.level);
187 caches
188}
189
190#[cfg(not(target_os = "linux"))]
192fn detect_caches() -> Vec<CacheLevel> {
193 Vec::new()
194}
195
196#[cfg(target_os = "linux")]
207fn detect_heterogeneous() -> Option<bool> {
208 use std::fs::read_to_string;
209
210 if std::path::Path::new("/sys/devices/system/cpu/types").is_dir() {
211 return Some(true);
212 }
213
214 let mut capacities = Vec::new();
215 for cpu in 0..256 {
216 let path = format!("/sys/devices/system/cpu/cpu{cpu}/cpu_capacity");
217 if !std::path::Path::new(&path).exists() {
218 if cpu == 0 {
220 return None;
221 }
222 break;
223 }
224 if let Some(value) = read_to_string(&path)
225 .ok()
226 .and_then(|text| text.trim().parse::<u32>().ok())
227 {
228 capacities.push(value);
229 }
230 }
231 if capacities.is_empty() {
232 return None;
233 }
234 let first = capacities[0];
235 Some(capacities.iter().any(|value| *value != first))
236}
237
238#[cfg(not(target_os = "linux"))]
240fn detect_heterogeneous() -> Option<bool> {
241 None
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn sizes_parse_with_and_without_suffixes() {
250 assert_eq!(parse_size("32K"), Some(32 * 1024));
251 assert_eq!(parse_size("4096K"), Some(4096 * 1024));
252 assert_eq!(parse_size("16M"), Some(16 * 1024 * 1024));
253 assert_eq!(parse_size("512"), Some(512));
254 assert_eq!(parse_size(""), None);
255 assert_eq!(parse_size("garbage"), None);
256 }
257
258 #[test]
259 fn cpu_lists_count_ranges_and_singletons() {
260 assert_eq!(parse_cpu_list("0-19"), Some(20));
261 assert_eq!(parse_cpu_list("0"), Some(1));
262 assert_eq!(parse_cpu_list("0,4,8"), Some(3));
263 assert_eq!(parse_cpu_list("0-3,8-11"), Some(8));
264 assert_eq!(parse_cpu_list(""), None);
265 }
266
267 #[test]
269 fn a_malformed_range_refuses_rather_than_wrapping() {
270 assert_eq!(parse_cpu_list("19-0"), None);
271 }
272
273 #[test]
275 fn an_unmeasured_cache_level_answers_none() {
276 let topology = CpuTopology::default();
277 assert_eq!(topology.cache_bytes(1), None);
278 assert_eq!(topology.last_level_bytes(), None);
279 assert_eq!(topology.fits_in_cache(1024, 1), None);
280 }
281
282 #[test]
283 fn fit_queries_compare_against_the_measured_capacity() {
284 let topology = CpuTopology {
285 logical_cpus: Some(8),
286 caches: vec![CacheLevel {
287 level: 1,
288 bytes: 32 * 1024,
289 line_bytes: 64,
290 shared_by: 1,
291 }],
292 heterogeneous_cores: Some(false),
293 };
294 assert_eq!(topology.fits_in_cache(32 * 1024, 1), Some(true));
295 assert_eq!(topology.fits_in_cache(32 * 1024 + 1, 1), Some(false));
296 assert_eq!(topology.fits_in_cache(1024, 2), None);
297 }
298
299 #[test]
304 fn detection_is_self_consistent_on_this_host() {
305 let topology = CpuTopology::detect();
306 if let Some(cpus) = topology.logical_cpus {
307 assert!(cpus >= 1, "a usable machine has at least one CPU");
308 }
309 let mut previous = 0usize;
310 for cache in &topology.caches {
311 assert!(cache.bytes > 0, "a reported cache has a size");
312 assert!(cache.line_bytes > 0, "a reported cache has a line size");
313 assert!(cache.shared_by >= 1, "a cache is shared by >= 1 CPU");
314 assert!(
315 cache.bytes >= previous,
316 "cache levels grow with level: L{} is {} bytes after {previous}",
317 cache.level,
318 cache.bytes,
319 );
320 previous = cache.bytes;
321 }
322 }
323}