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(override_bytes, crate::knobs::TRACT_LLC_CONTENDED.get(), ci.l2, ci.l3, slc)
112 })
113}
114
115fn resolve_llc(
118 override_bytes: Option<usize>,
119 override_contended: bool,
120 l2: usize,
121 l3: usize,
122 slc: usize,
123) -> Option<(usize, LlcKind)> {
124 if let Some(b) = override_bytes.filter(|b| *b > 0) {
125 let kind = if override_contended { LlcKind::SystemLevel } else { LlcKind::Dedicated };
126 return Some((b, kind));
127 }
128 if l3 > l2 {
129 return Some((l3, LlcKind::Dedicated));
130 }
131 if slc > l2 && slc > 0 {
132 return Some((slc, LlcKind::SystemLevel));
133 }
134 None
135}
136
137fn env_llc_override() -> Option<usize> {
138 let b = parse_cache_size(&crate::knobs::TRACT_LLC_BYTES.get());
139 (b > 0).then_some(b)
140}
141
142#[cfg(any(target_os = "linux", target_os = "android"))]
149fn system_level_cache_bytes() -> usize {
150 use std::path::Path;
151 fn be_u32(p: &Path) -> Option<u32> {
152 let b = std::fs::read(p).ok()?;
153 (b.len() >= 4).then(|| u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
154 }
155 fn walk(dir: &Path, depth: usize, best: &mut usize) {
156 if depth == 0 {
157 return;
158 }
159 if be_u32(&dir.join("cache-level")) == Some(3) {
160 let sz = be_u32(&dir.join("cache-size")).unwrap_or(0) as usize;
161 *best = (*best).max(sz);
162 }
163 let Ok(rd) = std::fs::read_dir(dir) else { return };
164 for e in rd.flatten() {
165 let p = e.path();
166 if p.is_dir() && p.file_name().and_then(|n| n.to_str()) != Some("cpus") {
168 walk(&p, depth - 1, best);
169 }
170 }
171 }
172 let mut best = 0;
173 for root in ["/proc/device-tree", "/sys/firmware/devicetree/base"] {
174 let p = Path::new(root);
175 if p.exists() {
176 walk(p, 4, &mut best);
177 if best > 0 {
178 break;
179 }
180 }
181 }
182 best
183}
184
185#[cfg(not(any(target_os = "linux", target_os = "android")))]
186fn system_level_cache_bytes() -> usize {
187 0
188}
189
190#[cfg_attr(not(any(target_os = "linux", target_os = "android")), allow(dead_code))]
192fn parse_cache_size(s: &str) -> usize {
193 let s = s.trim();
194 let (num, mult) = if let Some(n) = s.strip_suffix(['K', 'k']) {
195 (n, 1024)
196 } else if let Some(n) = s.strip_suffix(['M', 'm']) {
197 (n, 1024 * 1024)
198 } else {
199 (s, 1)
200 };
201 num.trim().parse::<usize>().unwrap_or(0) * mult
202}
203
204#[cfg(any(target_os = "macos", target_os = "ios"))]
205fn detect() -> CacheInfo {
206 fn sysctl_usize(name: &str) -> Option<usize> {
211 use std::ffi::CString;
212 use std::os::raw::{c_char, c_int, c_void};
213 unsafe extern "C" {
214 fn sysctlbyname(
215 name: *const c_char,
216 oldp: *mut c_void,
217 oldlenp: *mut usize,
218 newp: *mut c_void,
219 newlen: usize,
220 ) -> c_int;
221 }
222 let cname = CString::new(name).ok()?;
223 let mut val: u64 = 0;
224 let mut len = std::mem::size_of::<u64>();
225 let rc = unsafe {
226 sysctlbyname(
227 cname.as_ptr(),
228 &mut val as *mut u64 as *mut c_void,
229 &mut len,
230 std::ptr::null_mut(),
231 0,
232 )
233 };
234 if rc != 0 || val == 0 { None } else { Some(val as usize) }
235 }
236
237 CacheInfo {
238 l1_data: sysctl_usize("hw.perflevel0.l1dcachesize")
240 .or_else(|| sysctl_usize("hw.l1dcachesize"))
241 .unwrap_or(0),
242 l2: sysctl_usize("hw.perflevel0.l2cachesize")
243 .or_else(|| sysctl_usize("hw.l2cachesize"))
244 .unwrap_or(0),
245 l3: sysctl_usize("hw.perflevel0.l3cachesize")
246 .or_else(|| sysctl_usize("hw.l3cachesize"))
247 .unwrap_or(0),
248 l2_sharers: 0,
251 }
252}
253
254#[cfg_attr(not(any(target_os = "linux", target_os = "android")), allow(dead_code))]
257fn count_cpu_list(s: &str) -> usize {
258 s.split(',')
259 .filter_map(|part| {
260 let part = part.trim();
261 if part.is_empty() {
262 return None;
263 }
264 match part.split_once('-') {
265 Some((a, b)) => {
266 let a: usize = a.trim().parse().ok()?;
267 let b: usize = b.trim().parse().ok()?;
268 (b >= a).then_some(b - a + 1)
269 }
270 None => part.parse::<usize>().ok().map(|_| 1),
271 }
272 })
273 .sum()
274}
275
276#[cfg(any(target_os = "linux", target_os = "android"))]
277fn detect() -> CacheInfo {
278 let read = |p: String| std::fs::read_to_string(p).ok();
281 let mut ci = CacheInfo::default();
282 let smt = read("/sys/devices/system/cpu/cpu0/topology/thread_siblings_list".to_string())
285 .map(|s| count_cpu_list(&s))
286 .filter(|&n| n > 0)
287 .unwrap_or(1);
288 for idx in 0..16 {
289 let base = format!("/sys/devices/system/cpu/cpu0/cache/index{idx}/");
290 let Some(level) = read(format!("{base}level")) else {
291 continue;
292 };
293 let level: usize = level.trim().parse().unwrap_or(0);
294 let ctype = read(format!("{base}type")).unwrap_or_default().trim().to_ascii_lowercase();
295 let size = read(format!("{base}size")).map(|s| parse_cache_size(&s)).unwrap_or(0);
296 if size == 0 {
297 continue;
298 }
299 match level {
300 1 if ctype == "data" || ctype == "unified" => {
301 if ci.l1_data == 0 {
302 ci.l1_data = size;
303 }
304 }
305 2 if ci.l2 == 0 => {
306 ci.l2 = size;
307 let cpus =
308 read(format!("{base}shared_cpu_list")).map(|s| count_cpu_list(&s)).unwrap_or(0);
309 ci.l2_sharers = (cpus / smt).max(1);
310 }
311 3 if ci.l3 == 0 => ci.l3 = size,
312 _ => {}
313 }
314 }
315 ci
316}
317
318#[cfg(target_os = "windows")]
319fn detect() -> CacheInfo {
320 let mut ci = CacheInfo::default();
324 if let Ok(out) = std::process::Command::new("wmic")
325 .args(["cpu", "get", "L2CacheSize,L3CacheSize", "/format:value"])
326 .output()
327 {
328 for line in String::from_utf8_lossy(&out.stdout).lines() {
329 let line = line.trim();
330 if let Some(v) = line.strip_prefix("L2CacheSize=") {
331 if let Ok(kb) = v.trim().parse::<usize>() {
332 ci.l2 = kb * 1024;
333 }
334 } else if let Some(v) = line.strip_prefix("L3CacheSize=") {
335 if let Ok(kb) = v.trim().parse::<usize>() {
336 ci.l3 = kb * 1024;
337 }
338 }
339 }
340 }
341 ci
342}
343
344#[cfg(not(any(
345 target_os = "macos",
346 target_os = "ios",
347 target_os = "linux",
348 target_os = "android",
349 target_os = "windows"
350)))]
351fn detect() -> CacheInfo {
352 CacheInfo::default()
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 #[test]
361 fn llc_resolution_priority() {
362 assert_eq!(
364 resolve_llc(Some(8 << 20), false, 1 << 20, 4 << 20, 0),
365 Some((8 << 20, LlcKind::Dedicated))
366 );
367 assert_eq!(
368 resolve_llc(Some(8 << 20), true, 1 << 20, 0, 0),
369 Some((8 << 20, LlcKind::SystemLevel))
370 );
371 assert_eq!(
373 resolve_llc(None, false, 1 << 20, 4 << 20, 0),
374 Some((4 << 20, LlcKind::Dedicated))
375 );
376 assert_eq!(
378 resolve_llc(None, false, 512 << 10, 0, 4 << 20),
379 Some((4 << 20, LlcKind::SystemLevel))
380 );
381 assert_eq!(resolve_llc(None, false, 1 << 20, 0, 0), None);
383 assert_eq!(resolve_llc(None, false, 1 << 20, 1 << 20, 512 << 10), None);
384 assert_eq!(
386 resolve_llc(Some(0), false, 1 << 20, 4 << 20, 0),
387 Some((4 << 20, LlcKind::Dedicated))
388 );
389 }
390
391 #[test]
392 fn slc_probe_never_panics() {
393 let _ = system_level_cache_bytes();
395 let _ = last_level_cache();
396 }
397
398 #[test]
399 fn parse_cache_size_units() {
400 assert_eq!(parse_cache_size("512"), 512);
401 assert_eq!(parse_cache_size("256K"), 256 * 1024);
402 assert_eq!(parse_cache_size("8M"), 8 * 1024 * 1024);
403 assert_eq!(parse_cache_size(" 1024k "), 1024 * 1024);
404 assert_eq!(parse_cache_size("garbage"), 0);
405 }
406
407 #[test]
408 fn cpu_list_counts() {
409 assert_eq!(count_cpu_list("0"), 1);
410 assert_eq!(count_cpu_list("0-15"), 16);
411 assert_eq!(count_cpu_list("0,8"), 2);
412 assert_eq!(count_cpu_list("0-3,8-11"), 8);
413 assert_eq!(count_cpu_list(""), 0);
414 assert_eq!(count_cpu_list("garbage"), 0);
415 }
416
417 #[test]
418 fn defaults_are_nonzero() {
419 let unknown = CacheInfo::default();
420 assert!(unknown.l1_data_or_default() >= 32 * 1024);
421 assert_eq!(unknown.l2_or_default(), 256 * 1024);
422 }
423
424 #[test]
425 fn detected_values_are_sane_when_present() {
426 let ci = cache_info();
430 for (name, v) in [("l1d", ci.l1_data), ("l2", ci.l2), ("l3", ci.l3)] {
431 assert!(v == 0 || (1024..=512 * 1024 * 1024).contains(&v), "{name} implausible: {v}");
432 }
433 if ci.l1_data > 0 && ci.l2 > 0 {
434 assert!(ci.l1_data <= ci.l2, "L1 {} > L2 {}", ci.l1_data, ci.l2);
435 }
436 if ci.l2 > 0 && ci.l3 > 0 {
437 assert!(ci.l2 <= ci.l3, "L2 {} > L3 {}", ci.l2, ci.l3);
438 }
439 }
440}