1use std::sync::OnceLock;
18
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
21pub struct CacheInfo {
22 pub l1_data: usize,
24 pub l2: usize,
26 pub l3: usize,
28 pub l2_sharers: usize,
34}
35
36impl CacheInfo {
37 pub fn l1_data_or_default(&self) -> usize {
40 if self.l1_data > 0 {
41 self.l1_data
42 } else if cfg!(target_arch = "aarch64") {
43 64 * 1024
44 } else {
45 32 * 1024
46 }
47 }
48
49 pub fn l2_or_default(&self) -> usize {
51 if self.l2 > 0 { self.l2 } else { 256 * 1024 }
52 }
53
54 pub fn l2_sharers_or_one(&self) -> usize {
57 self.l2_sharers.max(1)
58 }
59}
60
61pub fn cache_info() -> CacheInfo {
63 static CACHE: OnceLock<CacheInfo> = OnceLock::new();
64 *CACHE.get_or_init(detect)
65}
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub enum LlcKind {
71 Dedicated,
74 SystemLevel,
77}
78
79pub fn last_level_cache() -> Option<(usize, LlcKind)> {
97 static LLC: OnceLock<Option<(usize, LlcKind)>> = OnceLock::new();
103 *LLC.get_or_init(|| {
104 let ci = cache_info();
105 let override_bytes = env_llc_override();
106 let slc =
110 if override_bytes.is_some() || ci.l3 > ci.l2 { 0 } else { system_level_cache_bytes() };
111 resolve_llc(
112 override_bytes,
113 std::env::var_os("TRACT_LLC_CONTENDED").is_some(),
114 ci.l2,
115 ci.l3,
116 slc,
117 )
118 })
119}
120
121fn resolve_llc(
124 override_bytes: Option<usize>,
125 override_contended: bool,
126 l2: usize,
127 l3: usize,
128 slc: usize,
129) -> Option<(usize, LlcKind)> {
130 if let Some(b) = override_bytes.filter(|b| *b > 0) {
131 let kind = if override_contended { LlcKind::SystemLevel } else { LlcKind::Dedicated };
132 return Some((b, kind));
133 }
134 if l3 > l2 {
135 return Some((l3, LlcKind::Dedicated));
136 }
137 if slc > l2 && slc > 0 {
138 return Some((slc, LlcKind::SystemLevel));
139 }
140 None
141}
142
143fn env_llc_override() -> Option<usize> {
144 let b = parse_cache_size(&std::env::var("TRACT_LLC_BYTES").ok()?);
145 (b > 0).then_some(b)
146}
147
148#[cfg(any(target_os = "linux", target_os = "android"))]
155fn system_level_cache_bytes() -> usize {
156 use std::path::Path;
157 fn be_u32(p: &Path) -> Option<u32> {
158 let b = std::fs::read(p).ok()?;
159 (b.len() >= 4).then(|| u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
160 }
161 fn walk(dir: &Path, depth: usize, best: &mut usize) {
162 if depth == 0 {
163 return;
164 }
165 if be_u32(&dir.join("cache-level")) == Some(3) {
166 let sz = be_u32(&dir.join("cache-size")).unwrap_or(0) as usize;
167 *best = (*best).max(sz);
168 }
169 let Ok(rd) = std::fs::read_dir(dir) else { return };
170 for e in rd.flatten() {
171 let p = e.path();
172 if p.is_dir() && p.file_name().and_then(|n| n.to_str()) != Some("cpus") {
174 walk(&p, depth - 1, best);
175 }
176 }
177 }
178 let mut best = 0;
179 for root in ["/proc/device-tree", "/sys/firmware/devicetree/base"] {
180 let p = Path::new(root);
181 if p.exists() {
182 walk(p, 4, &mut best);
183 if best > 0 {
184 break;
185 }
186 }
187 }
188 best
189}
190
191#[cfg(not(any(target_os = "linux", target_os = "android")))]
192fn system_level_cache_bytes() -> usize {
193 0
194}
195
196#[cfg_attr(not(any(target_os = "linux", target_os = "android")), allow(dead_code))]
198fn parse_cache_size(s: &str) -> usize {
199 let s = s.trim();
200 let (num, mult) = if let Some(n) = s.strip_suffix(['K', 'k']) {
201 (n, 1024)
202 } else if let Some(n) = s.strip_suffix(['M', 'm']) {
203 (n, 1024 * 1024)
204 } else {
205 (s, 1)
206 };
207 num.trim().parse::<usize>().unwrap_or(0) * mult
208}
209
210#[cfg(any(target_os = "macos", target_os = "ios"))]
211fn detect() -> CacheInfo {
212 fn sysctl_usize(name: &str) -> Option<usize> {
217 use std::ffi::CString;
218 use std::os::raw::{c_char, c_int, c_void};
219 unsafe extern "C" {
220 fn sysctlbyname(
221 name: *const c_char,
222 oldp: *mut c_void,
223 oldlenp: *mut usize,
224 newp: *mut c_void,
225 newlen: usize,
226 ) -> c_int;
227 }
228 let cname = CString::new(name).ok()?;
229 let mut val: u64 = 0;
230 let mut len = std::mem::size_of::<u64>();
231 let rc = unsafe {
232 sysctlbyname(
233 cname.as_ptr(),
234 &mut val as *mut u64 as *mut c_void,
235 &mut len,
236 std::ptr::null_mut(),
237 0,
238 )
239 };
240 if rc != 0 || val == 0 { None } else { Some(val as usize) }
241 }
242
243 CacheInfo {
244 l1_data: sysctl_usize("hw.perflevel0.l1dcachesize")
246 .or_else(|| sysctl_usize("hw.l1dcachesize"))
247 .unwrap_or(0),
248 l2: sysctl_usize("hw.perflevel0.l2cachesize")
249 .or_else(|| sysctl_usize("hw.l2cachesize"))
250 .unwrap_or(0),
251 l3: sysctl_usize("hw.perflevel0.l3cachesize")
252 .or_else(|| sysctl_usize("hw.l3cachesize"))
253 .unwrap_or(0),
254 l2_sharers: 0,
257 }
258}
259
260#[cfg_attr(not(any(target_os = "linux", target_os = "android")), allow(dead_code))]
263fn count_cpu_list(s: &str) -> usize {
264 s.split(',')
265 .filter_map(|part| {
266 let part = part.trim();
267 if part.is_empty() {
268 return None;
269 }
270 match part.split_once('-') {
271 Some((a, b)) => {
272 let a: usize = a.trim().parse().ok()?;
273 let b: usize = b.trim().parse().ok()?;
274 (b >= a).then_some(b - a + 1)
275 }
276 None => part.parse::<usize>().ok().map(|_| 1),
277 }
278 })
279 .sum()
280}
281
282#[cfg(any(target_os = "linux", target_os = "android"))]
283fn detect() -> CacheInfo {
284 let read = |p: String| std::fs::read_to_string(p).ok();
287 let mut ci = CacheInfo::default();
288 let smt = read("/sys/devices/system/cpu/cpu0/topology/thread_siblings_list".to_string())
291 .map(|s| count_cpu_list(&s))
292 .filter(|&n| n > 0)
293 .unwrap_or(1);
294 for idx in 0..16 {
295 let base = format!("/sys/devices/system/cpu/cpu0/cache/index{idx}/");
296 let Some(level) = read(format!("{base}level")) else {
297 continue;
298 };
299 let level: usize = level.trim().parse().unwrap_or(0);
300 let ctype = read(format!("{base}type")).unwrap_or_default().trim().to_ascii_lowercase();
301 let size = read(format!("{base}size")).map(|s| parse_cache_size(&s)).unwrap_or(0);
302 if size == 0 {
303 continue;
304 }
305 match level {
306 1 if ctype == "data" || ctype == "unified" => {
307 if ci.l1_data == 0 {
308 ci.l1_data = size;
309 }
310 }
311 2 if ci.l2 == 0 => {
312 ci.l2 = size;
313 let cpus =
314 read(format!("{base}shared_cpu_list")).map(|s| count_cpu_list(&s)).unwrap_or(0);
315 ci.l2_sharers = (cpus / smt).max(1);
316 }
317 3 if ci.l3 == 0 => ci.l3 = size,
318 _ => {}
319 }
320 }
321 ci
322}
323
324#[cfg(target_os = "windows")]
325fn detect() -> CacheInfo {
326 let mut ci = CacheInfo::default();
330 if let Ok(out) = std::process::Command::new("wmic")
331 .args(["cpu", "get", "L2CacheSize,L3CacheSize", "/format:value"])
332 .output()
333 {
334 for line in String::from_utf8_lossy(&out.stdout).lines() {
335 let line = line.trim();
336 if let Some(v) = line.strip_prefix("L2CacheSize=") {
337 if let Ok(kb) = v.trim().parse::<usize>() {
338 ci.l2 = kb * 1024;
339 }
340 } else if let Some(v) = line.strip_prefix("L3CacheSize=") {
341 if let Ok(kb) = v.trim().parse::<usize>() {
342 ci.l3 = kb * 1024;
343 }
344 }
345 }
346 }
347 ci
348}
349
350#[cfg(not(any(
351 target_os = "macos",
352 target_os = "ios",
353 target_os = "linux",
354 target_os = "android",
355 target_os = "windows"
356)))]
357fn detect() -> CacheInfo {
358 CacheInfo::default()
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn llc_resolution_priority() {
368 assert_eq!(
370 resolve_llc(Some(8 << 20), false, 1 << 20, 4 << 20, 0),
371 Some((8 << 20, LlcKind::Dedicated))
372 );
373 assert_eq!(
374 resolve_llc(Some(8 << 20), true, 1 << 20, 0, 0),
375 Some((8 << 20, LlcKind::SystemLevel))
376 );
377 assert_eq!(
379 resolve_llc(None, false, 1 << 20, 4 << 20, 0),
380 Some((4 << 20, LlcKind::Dedicated))
381 );
382 assert_eq!(
384 resolve_llc(None, false, 512 << 10, 0, 4 << 20),
385 Some((4 << 20, LlcKind::SystemLevel))
386 );
387 assert_eq!(resolve_llc(None, false, 1 << 20, 0, 0), None);
389 assert_eq!(resolve_llc(None, false, 1 << 20, 1 << 20, 512 << 10), None);
390 assert_eq!(
392 resolve_llc(Some(0), false, 1 << 20, 4 << 20, 0),
393 Some((4 << 20, LlcKind::Dedicated))
394 );
395 }
396
397 #[test]
398 fn slc_probe_never_panics() {
399 let _ = system_level_cache_bytes();
401 let _ = last_level_cache();
402 }
403
404 #[test]
405 fn parse_cache_size_units() {
406 assert_eq!(parse_cache_size("512"), 512);
407 assert_eq!(parse_cache_size("256K"), 256 * 1024);
408 assert_eq!(parse_cache_size("8M"), 8 * 1024 * 1024);
409 assert_eq!(parse_cache_size(" 1024k "), 1024 * 1024);
410 assert_eq!(parse_cache_size("garbage"), 0);
411 }
412
413 #[test]
414 fn cpu_list_counts() {
415 assert_eq!(count_cpu_list("0"), 1);
416 assert_eq!(count_cpu_list("0-15"), 16);
417 assert_eq!(count_cpu_list("0,8"), 2);
418 assert_eq!(count_cpu_list("0-3,8-11"), 8);
419 assert_eq!(count_cpu_list(""), 0);
420 assert_eq!(count_cpu_list("garbage"), 0);
421 }
422
423 #[test]
424 fn defaults_are_nonzero() {
425 let unknown = CacheInfo::default();
426 assert!(unknown.l1_data_or_default() >= 32 * 1024);
427 assert_eq!(unknown.l2_or_default(), 256 * 1024);
428 }
429
430 #[test]
431 fn detected_values_are_sane_when_present() {
432 let ci = cache_info();
436 for (name, v) in [("l1d", ci.l1_data), ("l2", ci.l2), ("l3", ci.l3)] {
437 assert!(v == 0 || (1024..=512 * 1024 * 1024).contains(&v), "{name} implausible: {v}");
438 }
439 if ci.l1_data > 0 && ci.l2 > 0 {
440 assert!(ci.l1_data <= ci.l2, "L1 {} > L2 {}", ci.l1_data, ci.l2);
441 }
442 if ci.l2 > 0 && ci.l3 > 0 {
443 assert!(ci.l2 <= ci.l3, "L2 {} > L3 {}", ci.l2, ci.l3);
444 }
445 }
446}