1#![doc = include_str!("../README.md")]
2#![deny(missing_docs)]
3
4use std::fs::File;
5use std::io::{BufRead, BufReader};
6use std::path::{Path, PathBuf};
7use std::str::FromStr;
8
9#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)]
11pub enum Property {
12 File(PathBuf),
14 N(usize, usize),
16 Heap,
18 Stack,
20 Huge,
22 Anon(usize),
24 Dirty(usize),
26 Mapped(usize),
28 MapMax(usize),
30 SwapCache(usize),
32 Active(usize),
34 Writeback(usize),
36 Kernelpagesize(usize),
38}
39
40impl Property {
41 #[must_use]
43 pub fn page_size(&self) -> Option<usize> {
44 match self {
45 Self::Kernelpagesize(page_size) => Some(*page_size),
46 _ => None,
47 }
48 }
49
50 #[must_use]
53 pub fn normalize(self, page_size: usize) -> Option<Self> {
54 use Property::*;
55 match self {
56 File(p) => Some(File(p)),
57 N(node, pages) => Some(N(node, pages * page_size)),
58 Heap => Some(Heap),
59 Stack => Some(Stack),
60 Huge => Some(Huge),
61 Anon(pages) => Some(Anon(pages * page_size)),
62 Dirty(pages) => Some(Dirty(pages * page_size)),
63 Mapped(pages) => Some(Mapped(pages * page_size)),
64 MapMax(pages) => Some(MapMax(pages)),
65 SwapCache(pages) => Some(SwapCache(pages * page_size)),
66 Active(pages) => Some(Active(pages * page_size)),
67 Writeback(pages) => Some(Writeback(pages * page_size)),
68 Kernelpagesize(_) => None,
69 }
70 }
71}
72
73impl FromStr for Property {
74 type Err = String;
75
76 fn from_str(s: &str) -> Result<Self, Self::Err> {
77 let (key, val) = if let Some(index) = s.find('=') {
78 let (key, val) = s.split_at(index);
79 (key, Some(&val[1..]))
80 } else {
81 (s, None)
82 };
83 match (key, val) {
84 (key, Some(val)) if key.starts_with('N') => {
85 let node = key[1..].parse().map_err(|e| format!("{e}"))?;
86 let count = val.parse().map_err(|e| format!("{e}"))?;
87 Ok(Self::N(node, count))
88 }
89 ("file", Some(val)) => Ok(Self::File(PathBuf::from(val))),
90 ("heap", _) => Ok(Self::Heap),
91 ("stack", _) => Ok(Self::Stack),
92 ("huge", _) => Ok(Self::Huge),
93 ("anon", Some(val)) => val.parse().map(Self::Anon).map_err(|e| format!("{e}")),
94 ("dirty", Some(val)) => val.parse().map(Self::Dirty).map_err(|e| format!("{e}")),
95 ("mapped", Some(val)) => val.parse().map(Self::Mapped).map_err(|e| format!("{e}")),
96 ("mapmax", Some(val)) => val.parse().map(Self::MapMax).map_err(|e| format!("{e}")),
97 ("swapcache", Some(val)) => {
98 val.parse().map(Self::SwapCache).map_err(|e| format!("{e}"))
99 }
100 ("active", Some(val)) => val.parse().map(Self::Active).map_err(|e| format!("{e}")),
101 ("writeback", Some(val)) => {
102 val.parse().map(Self::Writeback).map_err(|e| format!("{e}"))
103 }
104 ("kernelpagesize_kB", Some(val)) => val
105 .parse()
106 .map(|sz: usize| Self::Kernelpagesize(sz << 10))
107 .map_err(|e| format!("{e}")),
108 (key, None) => Err(format!("unknown key: {key}")),
109 (key, Some(val)) => Err(format!("unknown key/value: {key}={val}")),
110 }
111 }
112}
113
114#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)]
116pub struct Range {
117 pub address: usize,
119 pub policy: String,
121 pub properties: Vec<Property>,
123}
124
125impl Range {
126 fn parse(line: &str) -> Option<Self> {
131 let mut parts = line.split_whitespace();
132 let address = <usize>::from_str_radix(parts.next()?, 16).ok()?;
133 let policy = parts.next()?.to_string();
134 let mut properties = Vec::new();
135 for part in parts {
136 match part.parse::<Property>() {
137 Ok(property) => properties.push(property),
138 Err(err) => eprintln!("Failed to parse numa_map entry \"{part}\": {err}"),
139 }
140 }
141 Some(Self {
142 address,
143 policy,
144 properties,
145 })
146 }
147
148 pub fn normalize(&mut self) {
150 let page_size = self.properties.iter().find_map(Property::page_size);
151 if let Some(page_size) = page_size {
152 let mut properties: Vec<_> = self
153 .properties
154 .drain(..)
155 .filter_map(|p| Property::normalize(p, page_size))
156 .collect();
157 properties.sort();
158 self.properties = properties;
159 }
160 }
161}
162
163#[derive(Default)]
165pub struct NumaMap {
166 pub ranges: Vec<Range>,
168}
169
170impl NumaMap {
171 pub fn from_file<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
181 let file = File::open(path)?;
182 let reader = BufReader::new(file);
183
184 let mut ranges = Vec::new();
185 for line in reader.lines() {
186 if let Some(range) = Range::parse(&(line?)) {
187 ranges.push(range);
188 }
189 }
190 Ok(Self { ranges })
191 }
192}
193
194#[cfg(test)]
195mod test {
196 use super::*;
197
198 #[test]
199 fn test_read() -> std::io::Result<()> {
200 let map = NumaMap::from_file("resources/numa_maps")?;
201
202 assert_eq!(map.ranges.len(), 23);
203
204 use Property::{Active, Anon, Dirty, File, Heap, Kernelpagesize, MapMax, Mapped, Stack, N};
205 let expected = [
206 Range {
207 address: 93893825802240,
208 policy: "default".to_string(),
209 properties: vec![
210 File("/usr/bin/cat".into()),
211 Mapped(2),
212 MapMax(3),
213 Active(0),
214 N(0, 2),
215 Kernelpagesize(4096),
216 ],
217 },
218 Range {
219 address: 93893825810432,
220 policy: "default".to_string(),
221 properties: vec![
222 File("/usr/bin/cat".into()),
223 Mapped(6),
224 MapMax(3),
225 Active(0),
226 N(0, 6),
227 Kernelpagesize(4096),
228 ],
229 },
230 Range {
231 address: 93893825835008,
232 policy: "default".to_string(),
233 properties: vec![
234 File("/usr/bin/cat".into()),
235 Mapped(3),
236 MapMax(3),
237 Active(0),
238 N(0, 3),
239 Kernelpagesize(4096),
240 ],
241 },
242 Range {
243 address: 93893825847296,
244 policy: "default".to_string(),
245 properties: vec![
246 File("/usr/bin/cat".into()),
247 Anon(1),
248 Dirty(1),
249 Active(0),
250 N(0, 1),
251 Kernelpagesize(4096),
252 ],
253 },
254 Range {
255 address: 93893825851392,
256 policy: "default".to_string(),
257 properties: vec![
258 File("/usr/bin/cat".into()),
259 Anon(1),
260 Dirty(1),
261 Active(0),
262 N(0, 1),
263 Kernelpagesize(4096),
264 ],
265 },
266 Range {
267 address: 93893836886016,
268 policy: "default".to_string(),
269 properties: vec![Heap, Anon(2), Dirty(2), N(0, 2), Kernelpagesize(4096)],
270 },
271 Range {
272 address: 140172716933120,
273 policy: "default".to_string(),
274 properties: vec![
275 File("/usr/lib/locale/locale-archive".into()),
276 Mapped(94),
277 MapMax(138),
278 Active(0),
279 N(0, 94),
280 Kernelpagesize(4096),
281 ],
282 },
283 Range {
284 address: 140172721541120,
285 policy: "default".to_string(),
286 properties: vec![Anon(3), Dirty(3), Active(1), N(0, 3), Kernelpagesize(4096)],
287 },
288 Range {
289 address: 140172721692672,
290 policy: "default".to_string(),
291 properties: vec![
292 File("/usr/lib/x86_64-linux-gnu/libc.so.6".into()),
293 Mapped(38),
294 MapMax(174),
295 N(0, 38),
296 Kernelpagesize(4096),
297 ],
298 },
299 Range {
300 address: 140172721848320,
301 policy: "default".to_string(),
302 properties: vec![
303 File("/usr/lib/x86_64-linux-gnu/libc.so.6".into()),
304 Mapped(192),
305 MapMax(185),
306 N(0, 192),
307 Kernelpagesize(4096),
308 ],
309 },
310 Range {
311 address: 140172723245056,
312 policy: "default".to_string(),
313 properties: vec![
314 File("/usr/lib/x86_64-linux-gnu/libc.so.6".into()),
315 Mapped(43),
316 MapMax(181),
317 N(0, 43),
318 Kernelpagesize(4096),
319 ],
320 },
321 Range {
322 address: 140172723589120,
323 policy: "default".to_string(),
324 properties: vec![
325 File("/usr/lib/x86_64-linux-gnu/libc.so.6".into()),
326 Anon(4),
327 Dirty(4),
328 Active(0),
329 N(0, 4),
330 Kernelpagesize(4096),
331 ],
332 },
333 Range {
334 address: 140172723605504,
335 policy: "default".to_string(),
336 properties: vec![
337 File("/usr/lib/x86_64-linux-gnu/libc.so.6".into()),
338 Anon(2),
339 Dirty(2),
340 Active(0),
341 N(0, 2),
342 Kernelpagesize(4096),
343 ],
344 },
345 Range {
346 address: 140172723613696,
347 policy: "default".to_string(),
348 properties: vec![Anon(5), Dirty(5), Active(1), N(0, 5), Kernelpagesize(4096)],
349 },
350 Range {
351 address: 140172723773440,
352 policy: "default".to_string(),
353 properties: vec![Anon(1), Dirty(1), Active(0), N(0, 1), Kernelpagesize(4096)],
354 },
355 Range {
356 address: 140172723781632,
357 policy: "default".to_string(),
358 properties: vec![
359 File("/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2".into()),
360 Mapped(1),
361 MapMax(173),
362 N(0, 1),
363 Kernelpagesize(4096),
364 ],
365 },
366 Range {
367 address: 140172723785728,
368 policy: "default".to_string(),
369 properties: vec![
370 File("/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2".into()),
371 Mapped(37),
372 MapMax(181),
373 N(0, 37),
374 Kernelpagesize(4096),
375 ],
376 },
377 Range {
378 address: 140172723937280,
379 policy: "default".to_string(),
380 properties: vec![
381 File("/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2".into()),
382 Mapped(10),
383 MapMax(173),
384 N(0, 10),
385 Kernelpagesize(4096),
386 ],
387 },
388 Range {
389 address: 140172723978240,
390 policy: "default".to_string(),
391 properties: vec![
392 File("/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2".into()),
393 Anon(2),
394 Dirty(2),
395 Active(0),
396 N(0, 2),
397 Kernelpagesize(4096),
398 ],
399 },
400 Range {
401 address: 140172723986432,
402 policy: "default".to_string(),
403 properties: vec![
404 File("/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2".into()),
405 Anon(2),
406 Dirty(2),
407 Active(0),
408 N(0, 2),
409 Kernelpagesize(4096),
410 ],
411 },
412 Range {
413 address: 140736328499200,
414 policy: "default".to_string(),
415 properties: vec![
416 Stack,
417 Anon(3),
418 Dirty(3),
419 Active(0),
420 N(0, 3),
421 Kernelpagesize(4096),
422 ],
423 },
424 Range {
425 address: 140736330227712,
426 policy: "default".to_string(),
427 properties: vec![],
428 },
429 Range {
430 address: 140736330244096,
431 policy: "default".to_string(),
432 properties: vec![],
433 },
434 ];
435
436 assert_eq!(&expected[..], &map.ranges[..]);
437
438 Ok(())
439 }
440
441 #[test]
442 fn test_normalize() {
443 use Property::*;
444
445 let line = "7fbd0c10f000 default anon=5 dirty=5 active=1 N0=5 kernelpagesize_kB=4";
446 let mut range = Range::parse(line).unwrap();
447 range.normalize();
448 range.properties.sort();
449 let expected = vec![
450 N(0, 5 << 12),
451 Anon(5 << 12),
452 Dirty(5 << 12),
453 Active(1 << 12),
454 ];
455 assert_eq!(&expected, &range.properties);
456 }
457
458 #[test]
459 #[cfg(target_os = "linux")]
460 fn test_read_self() -> std::io::Result<()> {
461 let map = NumaMap::from_file("/proc/self/numa_maps")?;
462 assert!(map.ranges.len() > 0);
463 Ok(())
464 }
465}