numa_maps/
lib.rs

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/// Properties for memory regions. Consult `man 7 numa` for details.
10#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)]
11pub enum Property {
12    /// File backing the memory region.
13    File(PathBuf),
14    /// Size on each numa node `(numa_node, size)`. Size in pages, or bytes after normalizing.
15    N(usize, usize),
16    /// Memory range is used for the heap.
17    Heap,
18    /// Memory range is used for the stack.
19    Stack,
20    /// Memory range backed by huge pages. Page size differs from other entries.
21    Huge,
22    /// Number of anonymous pages in range.
23    Anon(usize),
24    /// Number of dirty pages in range.
25    Dirty(usize),
26    /// Number of mapped pages in range.
27    Mapped(usize),
28    /// Number of processes mapping a single page.
29    MapMax(usize),
30    /// Number of pages that have an associated entry on a swap device.
31    SwapCache(usize),
32    /// Number of pages on the active list.
33    Active(usize),
34    /// Number of pages that are currently being written out to disk.
35    Writeback(usize),
36    /// The size of the pages in this region in bytes.
37    Kernelpagesize(usize),
38}
39
40impl Property {
41    /// Returns the kernel page size if the property matches the page size property.
42    #[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    /// Normalize the property given the page size. Returns an
51    /// optional value, which is set for all but the page size property.
52    #[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/// A memory range, with a base address and a list of properties.
115#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)]
116pub struct Range {
117    /// The base address of this memory range.
118    pub address: usize,
119    /// The range's memory policy.
120    pub policy: String,
121    /// Properties associated with the memory range.
122    pub properties: Vec<Property>,
123}
124
125impl Range {
126    /// Parse a numa map line. Prints errors to stderr.
127    ///
128    /// Returns no value if the line does not contain an address, or the address is
129    /// malformed.
130    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    /// Normalize the range using the page size property, if it exists.
149    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/// A whole `numu_maps` file.
164#[derive(Default)]
165pub struct NumaMap {
166    /// Individual memory ranges.
167    pub ranges: Vec<Range>,
168}
169
170impl NumaMap {
171    /// Read a `numa_maps` file from `path`.
172    ///
173    /// Parses the contents and returns them as [`NumaMap`]. Each line translates
174    /// to an entry in [`NumaMap::ranges`], which stores the properties gathered
175    /// from the file as [`Property`].
176    ///
177    /// # Errors
178    ///
179    /// Returns an error if it fails to read the file.
180    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}