process_memory/
linux.rs

1use std::{
2    fs::{self, File},
3    io::{self, Read},
4    mem::MaybeUninit,
5};
6
7const SMAPS_ROLLUP_PATH: &str = "/proc/self/smaps_rollup";
8const SMAPS_PATH: &str = "/proc/self/smaps";
9const STATM: &str = "/proc/self/statm";
10const RSS_LINE_PREFIX: &[u8] = b"Rss: ";
11
12enum StatSource {
13    SmapsRollup(Scanner<File>),
14    Smaps(Scanner<File>),
15    Statm(Option<usize>),
16}
17
18/// A memory usage querier.
19pub struct Querier {
20    source: StatSource,
21}
22
23impl Querier {
24    /// Gets the resident set size of this process, in bytes.
25    ///
26    /// If the resident set size cannot be determined, `None` is returned. This could be for a number of underlying
27    /// reasons, but should generally be considered an incredibly rare/unlikely event.
28    pub fn resident_set_size(&mut self) -> Option<usize> {
29        match &mut self.source {
30            StatSource::SmapsRollup(scanner) => {
31                // As smaps_rollup is a pre-aggregated version of smaps, there's only one "Rss:" line that we need to find, so use
32                // the same scanner-based approach as we do for smaps, but just take the first matching line we find.
33                scanner.reset_with_path(SMAPS_ROLLUP_PATH).ok()?;
34
35                while let Ok(Some(raw_rss_line)) = scanner.next_matching_line(RSS_LINE_PREFIX) {
36                    let raw_rss_value = skip_to_line_value(raw_rss_line)?;
37                    if let Some(rss_bytes) = parse_kb_value_as_bytes(raw_rss_value) {
38                        return Some(rss_bytes);
39                    }
40                }
41
42                None
43            }
44            StatSource::Smaps(scanner) => {
45                // Scan all lines in smaps, looking for lines that start with "Rss:". Each of these lines will contain the resident
46                // set size of a particular memory mapping. We simply need to find all of these lines and aggregate their value to
47                // get the RSS for the process.
48                scanner.reset_with_path(SMAPS_PATH).ok()?;
49
50                let mut total_rss_bytes = 0;
51                while let Ok(Some(raw_rss_line)) = scanner.next_matching_line(RSS_LINE_PREFIX) {
52                    let raw_rss_value = skip_to_line_value(raw_rss_line)?;
53                    if let Some(rss_bytes) = parse_kb_value_as_bytes(raw_rss_value) {
54                        total_rss_bytes += rss_bytes;
55                    }
56                }
57
58                if total_rss_bytes > 0 {
59                    Some(total_rss_bytes)
60                } else {
61                    None
62                }
63            }
64            StatSource::Statm(maybe_page_size) => {
65                let page_size = maybe_page_size.as_ref().copied()?;
66
67                // Unlike smaps/smaps_rollup, statm is a drastically simpler format that is written as a single line with
68                // space-delimited fields. Since we have no lines to scan, it's much simpler to just read the entire file into a
69                // stack-allocated buffer.
70                //
71                // With seven integer fields, we can napkin math this to wanting to hold 20 bytes per field, plus the separators and
72                // newline, which is 153... but power-of-two numbers somehow feel better, so we'll go to 256.
73                let mut buf = [0; 256];
74                let mut file = File::open(STATM).ok()?;
75                let n = file.read(&mut buf).ok()?;
76                if n == 0 || n == buf.len() {
77                    // If we read no bytes, or filled the entire buffer, something is very wrong.
78                    return None;
79                }
80
81                // Resident set size is the second field, so we need to skip to it.
82                let raw_rss_field = buf.split(|b| *b == b' ').nth(1)?;
83
84                // We need to parse the field as an integer, and then multiply it by the page size to get the value in bytes.
85                let rss_pages = std::str::from_utf8(raw_rss_field).ok()?.parse::<usize>().ok()?;
86                Some(rss_pages * page_size)
87            }
88        }
89    }
90}
91
92impl Default for Querier {
93    fn default() -> Self {
94        Self {
95            source: determine_stat_source(),
96        }
97    }
98}
99
100fn determine_stat_source() -> StatSource {
101    if fs::metadata(SMAPS_ROLLUP_PATH).is_ok() {
102        StatSource::SmapsRollup(Scanner::new())
103    } else if fs::metadata(SMAPS_PATH).is_ok() {
104        StatSource::Smaps(Scanner::new())
105    } else {
106        StatSource::Statm(page_size())
107    }
108}
109
110fn page_size() -> Option<usize> {
111    let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
112    if page_size <= 0 {
113        None
114    } else {
115        Some(page_size as usize)
116    }
117}
118
119fn parse_kb_value_as_bytes(raw_rss_value: &[u8]) -> Option<usize> {
120    // The raw value here will be in the form of `XXXXXX kB`, so we want to find the first whitespace character, and
121    // take everything before that.
122    match raw_rss_value.iter().position(|&b| b == b' ') {
123        Some(space_idx) => {
124            let raw_value = &raw_rss_value[..space_idx];
125            std::str::from_utf8(raw_value)
126                .ok()?
127                .parse::<usize>()
128                .ok()
129                .map(|value| value * 1024)
130        }
131        None => None,
132    }
133}
134
135struct Scanner<T> {
136    io: Option<T>,
137    eof: bool,
138    buf: Vec<u8>,
139    pending_consume: Option<usize>,
140}
141
142impl<T> Scanner<T>
143where
144    T: Read,
145{
146    fn new() -> Self {
147        Self {
148            io: None,
149            eof: false,
150            buf: Vec::with_capacity(8192),
151            pending_consume: None,
152        }
153    }
154
155    fn reset(&mut self, io: T) {
156        self.buf.clear();
157        self.eof = false;
158        self.pending_consume = None;
159        self.io = Some(io);
160    }
161
162    fn get_io_mut(&mut self) -> io::Result<&mut T> {
163        match self.io.as_mut() {
164            Some(io) => Ok(io),
165            None => Err(io::Error::other("no file set in scanner")),
166        }
167    }
168
169    fn fill_buf(&mut self) -> io::Result<()> {
170        if self.eof {
171            return Ok(());
172        }
173
174        // If our buffer isn't entirely filled, try and fill the remainder.
175        if self.buf.len() < self.buf.capacity() {
176            // If we have any spare capacity, try to read as many bytes as we can hold in it.
177            //
178            // SAFETY: There's no invalid bit patterns for `u8`.
179            let read_buf = unsafe { &mut *(self.buf.spare_capacity_mut() as *mut [MaybeUninit<u8>] as *mut [u8]) };
180            let n = self.get_io_mut()?.read(read_buf)?;
181            if n == 0 {
182                self.eof = true;
183            }
184
185            // SAFETY: We've just read `n` bytes into `buf`, based on the spare capacity, so incrementing our length by
186            // `n` will only cover initialized bytes, and can't result in a length greater than the buffer capacity.
187            unsafe {
188                self.buf.set_len(self.buf.len() + n);
189            }
190        }
191
192        Ok(())
193    }
194
195    fn next_matching_line(&mut self, prefix: &[u8]) -> io::Result<Option<&[u8]>> {
196        loop {
197            // We've reached EOF and have processed the entire file.
198            if self.eof && self.buf.is_empty() {
199                return Ok(None);
200            }
201
202            // If we have a pending consume, take that many bytes from the front of the buffer and shift the rest of the
203            // data forward.
204            if let Some(consume) = self.pending_consume {
205                self.buf.drain(..consume);
206                self.pending_consume = None;
207            }
208
209            // Ensure our buffer is as filled as it can be.
210            self.fill_buf()?;
211
212            // Get the entire buffer that we currently have, and see if it starts with our prefix.
213            //
214            // If it does, we then need to also find a newline character to know where to chop it off.
215            if self.buf.starts_with(prefix) {
216                let maybe_newline_idx = self.buf.iter().position(|&b| b == b'\n');
217                if let Some(newline_idx) = maybe_newline_idx {
218                    // Consume up to and including the newline character, but only hand back the bytes up to the newline.
219                    self.pending_consume = Some(newline_idx + 1);
220
221                    return Ok(Some(&self.buf[..newline_idx]));
222                }
223            } else {
224                // Our buffer doesn't start with the prefix, so we need to find the next newline character and consume
225                // up to that point to reset ourselves.
226                //
227                // We're essentially resetting ourselves to start at the next line in the file.
228                let maybe_newline_idx = self.buf.iter().position(|&b| b == b'\n');
229                if let Some(newline_idx) = maybe_newline_idx {
230                    self.pending_consume = Some(newline_idx + 1);
231                } else {
232                    // We couldn't find a newline character, so we need to clear the entire buffer and just keep reading.
233                    self.buf.clear();
234                }
235            }
236        }
237    }
238}
239
240impl Scanner<File> {
241    fn reset_with_path(&mut self, path: &str) -> io::Result<()> {
242        let file = File::open(path)?;
243        self.reset(file);
244
245        Ok(())
246    }
247}
248
249fn skip_to_line_value(raw_line: &[u8]) -> Option<&[u8]> {
250    // We skip over all non-numeric characters and then return what's left.
251    //
252    // If the line doesn't contain any numeric characters, `None` is returned.
253    raw_line
254        .iter()
255        .position(|b| b.is_ascii_digit())
256        .map(|idx| &raw_line[idx..])
257}
258
259#[cfg(test)]
260mod tests {
261    use super::Querier;
262
263    #[test]
264    fn basic() {
265        let mut querier = Querier::default();
266        assert!(querier.resident_set_size().is_some());
267    }
268
269    #[test]
270    fn skip_to_line_value() {
271        let passing_lines = [
272            "1234 kB".as_bytes(),
273            "    1234 kB".as_bytes(),
274            "\t1234 kB".as_bytes(),
275            "Rss:1234 kB".as_bytes(),
276            "Rss:   1234 kB".as_bytes(),
277        ];
278        for line in &passing_lines {
279            assert_eq!(super::skip_to_line_value(line), Some("1234 kB".as_bytes()));
280        }
281
282        let failing_lines = [
283            "Rss: ".as_bytes(),
284            "Rss: \n".as_bytes(),
285            "Rss:  kB".as_bytes(),
286            "Rss: kB\n".as_bytes(),
287        ];
288        for line in &failing_lines {
289            assert_eq!(super::skip_to_line_value(line), None);
290        }
291    }
292
293    #[test]
294    fn scanner_basic() {
295        let prefix = "Rss: ".as_bytes();
296
297        let mut scanner = super::Scanner::new();
298        let mut buf = Vec::new();
299        buf.extend_from_slice(b"Rss: 1234 kB\nRss: 5678 kB\nRss: 91011 kB\n");
300        scanner.reset(buf.as_slice());
301
302        assert_eq!(
303            scanner.next_matching_line(prefix).unwrap(),
304            Some(b"Rss: 1234 kB".as_ref())
305        );
306        assert_eq!(
307            scanner.next_matching_line(prefix).unwrap(),
308            Some(b"Rss: 5678 kB".as_ref())
309        );
310        assert_eq!(
311            scanner.next_matching_line(prefix).unwrap(),
312            Some(b"Rss: 91011 kB".as_ref())
313        );
314        assert_eq!(scanner.next_matching_line(prefix).unwrap(), None);
315    }
316
317    #[test]
318    fn scanner_skip_non_matching_lines() {
319        let prefix = "Rss: ".as_bytes();
320
321        let mut scanner = super::Scanner::new();
322        let mut buf = Vec::new();
323        buf.extend_from_slice(b"Rss: 1234 kB\nPss: 5678 kB\nHugepages:    42069 kB\nRss: 91011 kB\n");
324        scanner.reset(buf.as_slice());
325
326        assert_eq!(
327            scanner.next_matching_line(prefix).unwrap(),
328            Some(b"Rss: 1234 kB".as_ref())
329        );
330        assert_eq!(
331            scanner.next_matching_line(prefix).unwrap(),
332            Some(b"Rss: 91011 kB".as_ref())
333        );
334        assert_eq!(scanner.next_matching_line(prefix).unwrap(), None);
335    }
336
337    #[test]
338    fn scanner_skips_lines_larger_than_buffer() {
339        let prefix = "Rss: ".as_bytes();
340
341        let mut scanner = super::Scanner::new();
342
343        // We construct a non-matching line that's longer than our internal buffer (8192 bytes) to ensure that we can
344        // still skip over it and find the next matching line without losing data.
345        let mut buf = Vec::new();
346        buf.resize(9000, b'@');
347        buf.extend_from_slice(b"\nRss: 1234 kB\n");
348        scanner.reset(buf.as_slice());
349
350        assert_eq!(
351            scanner.next_matching_line(prefix).unwrap(),
352            Some(b"Rss: 1234 kB".as_ref())
353        );
354        assert_eq!(scanner.next_matching_line(prefix).unwrap(), None);
355    }
356}