1use thiserror::Error;
8
9#[derive(Debug)]
11pub struct CacheTopology {
12 pub caches: Vec<Cache>,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
18pub struct Cache {
19 pub level: u8,
21 pub cache_type: CacheType,
23 pub cpus: Vec<u32>,
25 pub size: u32,
27 pub associativity: Option<u32>,
29 pub line_size: u32,
31}
32
33#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
35pub enum CacheType {
36 Data,
38 Instruction,
40 Unified,
42}
43
44#[derive(Debug, Error)]
46pub enum HostTopologyError {
47 #[error("os error retrieving cache topology")]
49 Os(#[source] std::io::Error),
50}
51
52impl CacheTopology {
53 pub fn from_host() -> Result<Self, HostTopologyError> {
55 let mut caches = Self::host_caches().map_err(HostTopologyError::Os)?;
56 caches.sort();
57 caches.dedup();
58 Ok(Self { caches })
59 }
60}
61
62#[cfg(windows)]
63#[expect(unsafe_code)]
65mod windows {
66 use super::CacheTopology;
67 use crate::Cache;
68 use crate::CacheType;
69 use windows_sys::Win32::Foundation::ERROR_INSUFFICIENT_BUFFER;
70 use windows_sys::Win32::System::SystemInformation;
71
72 impl CacheTopology {
73 pub(crate) fn host_caches() -> std::io::Result<Vec<Cache>> {
74 let mut len = 0;
75 let r = unsafe {
77 SystemInformation::GetLogicalProcessorInformationEx(
78 SystemInformation::RelationCache,
79 std::ptr::null_mut(),
80 &mut len,
81 )
82 };
83 assert_eq!(r, 0);
84 let err = std::io::Error::last_os_error();
85 if err.raw_os_error() != Some(ERROR_INSUFFICIENT_BUFFER as i32) {
86 return Err(err);
87 }
88 let mut buf = vec![0u8; len as usize];
89 let r = unsafe {
92 SystemInformation::GetLogicalProcessorInformationEx(
93 SystemInformation::RelationCache,
94 buf.as_mut_ptr().cast(),
95 &mut len,
96 )
97 };
98 if r == 0 {
99 return Err(std::io::Error::last_os_error());
100 }
101
102 let mut caches = Vec::new();
103
104 let mut buf = buf.as_slice();
105 while !buf.is_empty() {
106 let info = unsafe {
109 &*buf
110 .as_ptr()
111 .cast::<SystemInformation::SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX>()
112 };
113
114 assert_eq!(info.Relationship, SystemInformation::RelationCache);
115 buf = &buf[info.Size as usize..];
116
117 let cache = unsafe { &info.Anonymous.Cache };
120
121 let groups = unsafe {
124 std::slice::from_raw_parts(
125 cache.Anonymous.GroupMasks.as_ptr(),
126 cache.GroupCount as usize,
127 )
128 };
129
130 let mut cpus = Vec::new();
131 for group in groups {
132 for i in 0..usize::BITS {
133 if group.Mask & (1 << i) != 0 {
134 cpus.push(group.Group as u32 * usize::BITS + i);
135 }
136 }
137 }
138
139 caches.push(Cache {
140 cpus,
141 level: cache.Level,
142 cache_type: match cache.Type {
143 SystemInformation::CacheUnified => CacheType::Unified,
144 SystemInformation::CacheInstruction => CacheType::Instruction,
145 SystemInformation::CacheData => CacheType::Data,
146 _ => continue,
147 },
148 size: cache.CacheSize,
149 associativity: if cache.Associativity == !0 {
150 None
151 } else {
152 Some(cache.Associativity.into())
153 },
154 line_size: cache.LineSize.into(),
155 });
156 }
157
158 Ok(caches)
159 }
160 }
161}
162
163#[cfg(target_os = "linux")]
164mod linux {
165 use super::Cache;
166 use super::CacheTopology;
167
168 impl CacheTopology {
169 pub(crate) fn host_caches() -> std::io::Result<Vec<Cache>> {
170 let mut caches = Vec::new();
171 for cpu_entry in fs_err::read_dir("/sys/devices/system/cpu")? {
172 let cpu_path = cpu_entry?.path();
173 if cpu_path
174 .file_name()
175 .unwrap()
176 .to_str()
177 .unwrap()
178 .strip_prefix("cpu")
179 .and_then(|s| s.parse::<u32>().ok())
180 .is_none()
181 {
182 continue;
183 }
184 let cache_dir = cpu_path.join("cache");
185 let cache_entries = match fs_err::read_dir(&cache_dir) {
186 Ok(entries) => entries,
187 Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
188 Err(e) => return Err(e),
189 };
190 for entry in cache_entries {
191 let entry = entry?;
192 let path = entry.path();
193 if !path
194 .file_name()
195 .unwrap()
196 .to_str()
197 .is_some_and(|s| s.starts_with("index"))
198 {
199 continue;
200 }
201
202 let read_optional_file = |name: &str| -> std::io::Result<Option<String>> {
205 match fs_err::read_to_string(path.join(name)) {
206 Ok(s) => Ok(Some(s)),
207 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
208 Err(e) => Err(e),
209 }
210 };
211
212 let Some(assoc_str) = read_optional_file("ways_of_associativity")? else {
213 continue;
214 };
215 let associativity: u32 = assoc_str.trim_end().parse().unwrap();
216
217 let Some(cpu_list_str) = read_optional_file("shared_cpu_list")? else {
218 continue;
219 };
220 let mut cpus = Vec::new();
221 for range in cpu_list_str.trim_end().split(',') {
222 if let Some((start, end)) = range.split_once('-') {
223 cpus.extend(
224 start.parse::<u32>().unwrap()..=end.parse::<u32>().unwrap(),
225 );
226 } else {
227 cpus.push(range.parse().unwrap());
228 }
229 }
230
231 let line_size = match read_optional_file("coherency_line_size")? {
232 Some(s) => s.trim_end().parse::<u32>().unwrap(),
233 None => 64,
234 };
235
236 let Some(level_str) = read_optional_file("level")? else {
237 continue;
238 };
239 let Some(type_str) = read_optional_file("type")? else {
240 continue;
241 };
242 let Some(size_str) = read_optional_file("size")? else {
243 continue;
244 };
245
246 caches.push(Cache {
247 cpus,
248 level: level_str.trim_end().parse().unwrap(),
249 cache_type: match type_str.trim_end() {
250 "Data" => super::CacheType::Data,
251 "Instruction" => super::CacheType::Instruction,
252 "Unified" => super::CacheType::Unified,
253 _ => continue,
254 },
255 size: size_str
256 .strip_suffix("K\n")
257 .unwrap()
258 .parse::<u32>()
259 .unwrap()
260 * 1024,
261 associativity: if associativity == 0 {
262 None
263 } else {
264 Some(associativity)
265 },
266 line_size,
267 });
268 }
269 }
270 Ok(caches)
271 }
272 }
273}
274
275#[cfg(target_os = "macos")]
276mod macos {
277 use super::Cache;
278 use super::CacheTopology;
279
280 impl CacheTopology {
281 pub(crate) fn host_caches() -> std::io::Result<Vec<Cache>> {
282 Ok(Vec::new())
284 }
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 #[test]
291 fn test_host_cache_topology() {
292 let topology = super::CacheTopology::from_host().unwrap();
293 assert!(!topology.caches.is_empty());
294 println!("{topology:?}");
295 }
296}