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}
29
30impl CacheInfo {
31 pub fn l1_data_or_default(&self) -> usize {
34 if self.l1_data > 0 {
35 self.l1_data
36 } else if cfg!(target_arch = "aarch64") {
37 64 * 1024
38 } else {
39 32 * 1024
40 }
41 }
42
43 pub fn l2_or_default(&self) -> usize {
45 if self.l2 > 0 { self.l2 } else { 256 * 1024 }
46 }
47}
48
49pub fn cache_info() -> CacheInfo {
51 static CACHE: OnceLock<CacheInfo> = OnceLock::new();
52 *CACHE.get_or_init(detect)
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub enum LlcKind {
59 Dedicated,
62 SystemLevel,
65}
66
67pub fn last_level_cache() -> Option<(usize, LlcKind)> {
85 static LLC: OnceLock<Option<(usize, LlcKind)>> = OnceLock::new();
91 *LLC.get_or_init(|| {
92 let ci = cache_info();
93 let override_bytes = env_llc_override();
94 let slc =
98 if override_bytes.is_some() || ci.l3 > ci.l2 { 0 } else { system_level_cache_bytes() };
99 resolve_llc(
100 override_bytes,
101 std::env::var_os("TRACT_LLC_CONTENDED").is_some(),
102 ci.l2,
103 ci.l3,
104 slc,
105 )
106 })
107}
108
109fn resolve_llc(
112 override_bytes: Option<usize>,
113 override_contended: bool,
114 l2: usize,
115 l3: usize,
116 slc: usize,
117) -> Option<(usize, LlcKind)> {
118 if let Some(b) = override_bytes.filter(|b| *b > 0) {
119 let kind = if override_contended { LlcKind::SystemLevel } else { LlcKind::Dedicated };
120 return Some((b, kind));
121 }
122 if l3 > l2 {
123 return Some((l3, LlcKind::Dedicated));
124 }
125 if slc > l2 && slc > 0 {
126 return Some((slc, LlcKind::SystemLevel));
127 }
128 None
129}
130
131fn env_llc_override() -> Option<usize> {
132 let b = parse_cache_size(&std::env::var("TRACT_LLC_BYTES").ok()?);
133 (b > 0).then_some(b)
134}
135
136#[cfg(any(target_os = "linux", target_os = "android"))]
143fn system_level_cache_bytes() -> usize {
144 use std::path::Path;
145 fn be_u32(p: &Path) -> Option<u32> {
146 let b = std::fs::read(p).ok()?;
147 (b.len() >= 4).then(|| u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
148 }
149 fn walk(dir: &Path, depth: usize, best: &mut usize) {
150 if depth == 0 {
151 return;
152 }
153 if be_u32(&dir.join("cache-level")) == Some(3) {
154 let sz = be_u32(&dir.join("cache-size")).unwrap_or(0) as usize;
155 *best = (*best).max(sz);
156 }
157 let Ok(rd) = std::fs::read_dir(dir) else { return };
158 for e in rd.flatten() {
159 let p = e.path();
160 if p.is_dir() && p.file_name().and_then(|n| n.to_str()) != Some("cpus") {
162 walk(&p, depth - 1, best);
163 }
164 }
165 }
166 let mut best = 0;
167 for root in ["/proc/device-tree", "/sys/firmware/devicetree/base"] {
168 let p = Path::new(root);
169 if p.exists() {
170 walk(p, 4, &mut best);
171 if best > 0 {
172 break;
173 }
174 }
175 }
176 best
177}
178
179#[cfg(not(any(target_os = "linux", target_os = "android")))]
180fn system_level_cache_bytes() -> usize {
181 0
182}
183
184#[cfg_attr(not(any(target_os = "linux", target_os = "android")), allow(dead_code))]
186fn parse_cache_size(s: &str) -> usize {
187 let s = s.trim();
188 let (num, mult) = if let Some(n) = s.strip_suffix(['K', 'k']) {
189 (n, 1024)
190 } else if let Some(n) = s.strip_suffix(['M', 'm']) {
191 (n, 1024 * 1024)
192 } else {
193 (s, 1)
194 };
195 num.trim().parse::<usize>().unwrap_or(0) * mult
196}
197
198#[cfg(any(target_os = "macos", target_os = "ios"))]
199fn detect() -> CacheInfo {
200 fn sysctl_usize(name: &str) -> Option<usize> {
205 use std::ffi::CString;
206 use std::os::raw::{c_char, c_int, c_void};
207 unsafe extern "C" {
208 fn sysctlbyname(
209 name: *const c_char,
210 oldp: *mut c_void,
211 oldlenp: *mut usize,
212 newp: *mut c_void,
213 newlen: usize,
214 ) -> c_int;
215 }
216 let cname = CString::new(name).ok()?;
217 let mut val: u64 = 0;
218 let mut len = std::mem::size_of::<u64>();
219 let rc = unsafe {
220 sysctlbyname(
221 cname.as_ptr(),
222 &mut val as *mut u64 as *mut c_void,
223 &mut len,
224 std::ptr::null_mut(),
225 0,
226 )
227 };
228 if rc != 0 || val == 0 { None } else { Some(val as usize) }
229 }
230
231 CacheInfo {
232 l1_data: sysctl_usize("hw.perflevel0.l1dcachesize")
234 .or_else(|| sysctl_usize("hw.l1dcachesize"))
235 .unwrap_or(0),
236 l2: sysctl_usize("hw.perflevel0.l2cachesize")
237 .or_else(|| sysctl_usize("hw.l2cachesize"))
238 .unwrap_or(0),
239 l3: sysctl_usize("hw.perflevel0.l3cachesize")
240 .or_else(|| sysctl_usize("hw.l3cachesize"))
241 .unwrap_or(0),
242 }
243}
244
245#[cfg(any(target_os = "linux", target_os = "android"))]
246fn detect() -> CacheInfo {
247 let mut ci = CacheInfo::default();
250 for idx in 0..16 {
251 let base = format!("/sys/devices/system/cpu/cpu0/cache/index{idx}/");
252 let Ok(level) = std::fs::read_to_string(format!("{base}level")) else {
253 continue;
254 };
255 let level: usize = level.trim().parse().unwrap_or(0);
256 let ctype = std::fs::read_to_string(format!("{base}type"))
257 .unwrap_or_default()
258 .trim()
259 .to_ascii_lowercase();
260 let size = std::fs::read_to_string(format!("{base}size"))
261 .map(|s| parse_cache_size(&s))
262 .unwrap_or(0);
263 if size == 0 {
264 continue;
265 }
266 match level {
267 1 if ctype == "data" || ctype == "unified" => {
268 if ci.l1_data == 0 {
269 ci.l1_data = size;
270 }
271 }
272 2 if ci.l2 == 0 => ci.l2 = size,
273 3 if ci.l3 == 0 => ci.l3 = size,
274 _ => {}
275 }
276 }
277 ci
278}
279
280#[cfg(target_os = "windows")]
281fn detect() -> CacheInfo {
282 let mut ci = CacheInfo::default();
286 if let Ok(out) = std::process::Command::new("wmic")
287 .args(["cpu", "get", "L2CacheSize,L3CacheSize", "/format:value"])
288 .output()
289 {
290 for line in String::from_utf8_lossy(&out.stdout).lines() {
291 let line = line.trim();
292 if let Some(v) = line.strip_prefix("L2CacheSize=") {
293 if let Ok(kb) = v.trim().parse::<usize>() {
294 ci.l2 = kb * 1024;
295 }
296 } else if let Some(v) = line.strip_prefix("L3CacheSize=") {
297 if let Ok(kb) = v.trim().parse::<usize>() {
298 ci.l3 = kb * 1024;
299 }
300 }
301 }
302 }
303 ci
304}
305
306#[cfg(not(any(
307 target_os = "macos",
308 target_os = "ios",
309 target_os = "linux",
310 target_os = "android",
311 target_os = "windows"
312)))]
313fn detect() -> CacheInfo {
314 CacheInfo::default()
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn llc_resolution_priority() {
324 assert_eq!(
326 resolve_llc(Some(8 << 20), false, 1 << 20, 4 << 20, 0),
327 Some((8 << 20, LlcKind::Dedicated))
328 );
329 assert_eq!(
330 resolve_llc(Some(8 << 20), true, 1 << 20, 0, 0),
331 Some((8 << 20, LlcKind::SystemLevel))
332 );
333 assert_eq!(
335 resolve_llc(None, false, 1 << 20, 4 << 20, 0),
336 Some((4 << 20, LlcKind::Dedicated))
337 );
338 assert_eq!(
340 resolve_llc(None, false, 512 << 10, 0, 4 << 20),
341 Some((4 << 20, LlcKind::SystemLevel))
342 );
343 assert_eq!(resolve_llc(None, false, 1 << 20, 0, 0), None);
345 assert_eq!(resolve_llc(None, false, 1 << 20, 1 << 20, 512 << 10), None);
346 assert_eq!(
348 resolve_llc(Some(0), false, 1 << 20, 4 << 20, 0),
349 Some((4 << 20, LlcKind::Dedicated))
350 );
351 }
352
353 #[test]
354 fn slc_probe_never_panics() {
355 let _ = system_level_cache_bytes();
357 let _ = last_level_cache();
358 }
359
360 #[test]
361 fn parse_cache_size_units() {
362 assert_eq!(parse_cache_size("512"), 512);
363 assert_eq!(parse_cache_size("256K"), 256 * 1024);
364 assert_eq!(parse_cache_size("8M"), 8 * 1024 * 1024);
365 assert_eq!(parse_cache_size(" 1024k "), 1024 * 1024);
366 assert_eq!(parse_cache_size("garbage"), 0);
367 }
368
369 #[test]
370 fn defaults_are_nonzero() {
371 let unknown = CacheInfo::default();
372 assert!(unknown.l1_data_or_default() >= 32 * 1024);
373 assert_eq!(unknown.l2_or_default(), 256 * 1024);
374 }
375
376 #[test]
377 fn detected_values_are_sane_when_present() {
378 let ci = cache_info();
382 for (name, v) in [("l1d", ci.l1_data), ("l2", ci.l2), ("l3", ci.l3)] {
383 assert!(v == 0 || (1024..=512 * 1024 * 1024).contains(&v), "{name} implausible: {v}");
384 }
385 if ci.l1_data > 0 && ci.l2 > 0 {
386 assert!(ci.l1_data <= ci.l2, "L1 {} > L2 {}", ci.l1_data, ci.l2);
387 }
388 if ci.l2 > 0 && ci.l3 > 0 {
389 assert!(ci.l2 <= ci.l3, "L2 {} > L3 {}", ci.l2, ci.l3);
390 }
391 }
392}