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
18pub struct Querier {
20 source: StatSource,
21}
22
23impl Querier {
24 pub fn resident_set_size(&mut self) -> Option<usize> {
29 match &mut self.source {
30 StatSource::SmapsRollup(scanner) => {
31 scanner.reset_with_path(SMAPS_ROLLUP_PATH).ok()?;
34 first_rss_value(scanner)
35 }
36 StatSource::Smaps(scanner) => {
37 scanner.reset_with_path(SMAPS_PATH).ok()?;
41 sum_rss_values(scanner)
42 }
43 StatSource::Statm(maybe_page_size) => {
44 let page_size = maybe_page_size.as_ref().copied()?;
45
46 let mut buf = [0; 256];
53 let mut file = File::open(STATM).ok()?;
54 let n = file.read(&mut buf).ok()?;
55 if n == 0 || n == buf.len() {
56 return None;
58 }
59
60 parse_statm_rss_bytes(&buf, page_size)
61 }
62 }
63 }
64}
65
66fn first_rss_value<T: Read>(scanner: &mut Scanner<T>) -> Option<usize> {
70 while let Ok(Some(raw_rss_line)) = scanner.next_matching_line(RSS_LINE_PREFIX) {
71 let raw_rss_value = skip_to_line_value(raw_rss_line)?;
72 if let Some(rss_bytes) = parse_kb_value_as_bytes(raw_rss_value) {
73 return Some(rss_bytes);
74 }
75 }
76
77 None
78}
79
80fn sum_rss_values<T: Read>(scanner: &mut Scanner<T>) -> Option<usize> {
84 let mut total_rss_bytes = 0;
85 while let Ok(Some(raw_rss_line)) = scanner.next_matching_line(RSS_LINE_PREFIX) {
86 let raw_rss_value = skip_to_line_value(raw_rss_line)?;
87 if let Some(rss_bytes) = parse_kb_value_as_bytes(raw_rss_value) {
88 total_rss_bytes += rss_bytes;
89 }
90 }
91
92 if total_rss_bytes > 0 {
93 Some(total_rss_bytes)
94 } else {
95 None
96 }
97}
98
99fn parse_statm_rss_bytes(raw: &[u8], page_size: usize) -> Option<usize> {
104 let raw_rss_field = raw.split(|b| *b == b' ').nth(1)?;
106
107 let rss_pages = simdutf8::basic::from_utf8(raw_rss_field).ok()?.parse::<usize>().ok()?;
109 Some(rss_pages * page_size)
110}
111
112impl Default for Querier {
113 fn default() -> Self {
114 Self {
115 source: determine_stat_source(),
116 }
117 }
118}
119
120fn determine_stat_source() -> StatSource {
121 select_stat_source(
122 fs::metadata(SMAPS_ROLLUP_PATH).is_ok(),
123 fs::metadata(SMAPS_PATH).is_ok(),
124 page_size(),
125 )
126}
127
128fn select_stat_source(has_smaps_rollup: bool, has_smaps: bool, page_size: Option<usize>) -> StatSource {
133 if has_smaps_rollup {
134 StatSource::SmapsRollup(Scanner::new())
135 } else if has_smaps {
136 StatSource::Smaps(Scanner::new())
137 } else {
138 StatSource::Statm(page_size)
139 }
140}
141
142fn page_size() -> Option<usize> {
143 let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
144 if page_size <= 0 {
145 None
146 } else {
147 Some(page_size as usize)
148 }
149}
150
151fn parse_kb_value_as_bytes(raw_rss_value: &[u8]) -> Option<usize> {
152 match raw_rss_value.iter().position(|&b| b == b' ') {
155 Some(space_idx) => {
156 let raw_value = &raw_rss_value[..space_idx];
157 simdutf8::basic::from_utf8(raw_value)
158 .ok()?
159 .parse::<usize>()
160 .ok()
161 .map(|value| value * 1024)
162 }
163 None => None,
164 }
165}
166
167struct Scanner<T> {
168 io: Option<T>,
169 eof: bool,
170 buf: Vec<u8>,
171 pending_consume: Option<usize>,
172}
173
174impl<T> Scanner<T>
175where
176 T: Read,
177{
178 fn new() -> Self {
179 Self {
180 io: None,
181 eof: false,
182 buf: Vec::with_capacity(8192),
183 pending_consume: None,
184 }
185 }
186
187 fn reset(&mut self, io: T) {
188 self.buf.clear();
189 self.eof = false;
190 self.pending_consume = None;
191 self.io = Some(io);
192 }
193
194 fn get_io_mut(&mut self) -> io::Result<&mut T> {
195 match self.io.as_mut() {
196 Some(io) => Ok(io),
197 None => Err(io::Error::other("no file set in scanner")),
198 }
199 }
200
201 fn fill_buf(&mut self) -> io::Result<()> {
202 if self.eof {
203 return Ok(());
204 }
205
206 if self.buf.len() < self.buf.capacity() {
208 let read_buf = unsafe { &mut *(self.buf.spare_capacity_mut() as *mut [MaybeUninit<u8>] as *mut [u8]) };
212 let n = self.get_io_mut()?.read(read_buf)?;
213 if n == 0 {
214 self.eof = true;
215 }
216
217 unsafe {
220 self.buf.set_len(self.buf.len() + n);
221 }
222 }
223
224 Ok(())
225 }
226
227 fn next_matching_line(&mut self, prefix: &[u8]) -> io::Result<Option<&[u8]>> {
228 loop {
229 if self.eof && self.buf.is_empty() {
231 return Ok(None);
232 }
233
234 if let Some(consume) = self.pending_consume {
237 self.buf.drain(..consume);
238 self.pending_consume = None;
239 }
240
241 self.fill_buf()?;
243
244 if self.buf.starts_with(prefix) {
248 let maybe_newline_idx = self.buf.iter().position(|&b| b == b'\n');
249 if let Some(newline_idx) = maybe_newline_idx {
250 self.pending_consume = Some(newline_idx + 1);
252
253 return Ok(Some(&self.buf[..newline_idx]));
254 }
255 } else {
256 let maybe_newline_idx = self.buf.iter().position(|&b| b == b'\n');
261 if let Some(newline_idx) = maybe_newline_idx {
262 self.pending_consume = Some(newline_idx + 1);
263 } else {
264 self.buf.clear();
266 }
267 }
268 }
269 }
270}
271
272impl Scanner<File> {
273 fn reset_with_path(&mut self, path: &str) -> io::Result<()> {
274 let file = File::open(path)?;
275 self.reset(file);
276
277 Ok(())
278 }
279}
280
281fn skip_to_line_value(raw_line: &[u8]) -> Option<&[u8]> {
282 raw_line
286 .iter()
287 .position(|b| b.is_ascii_digit())
288 .map(|idx| &raw_line[idx..])
289}
290
291#[cfg(test)]
292mod tests {
293 use super::Querier;
294
295 #[test]
296 fn basic() {
297 let mut querier = Querier::default();
298 assert!(querier.resident_set_size().is_some());
299 }
300
301 #[test]
302 fn skip_to_line_value() {
303 let passing_lines = [
304 "1234 kB".as_bytes(),
305 " 1234 kB".as_bytes(),
306 "\t1234 kB".as_bytes(),
307 "Rss:1234 kB".as_bytes(),
308 "Rss: 1234 kB".as_bytes(),
309 ];
310 for line in &passing_lines {
311 assert_eq!(super::skip_to_line_value(line), Some("1234 kB".as_bytes()));
312 }
313
314 let failing_lines = [
315 "Rss: ".as_bytes(),
316 "Rss: \n".as_bytes(),
317 "Rss: kB".as_bytes(),
318 "Rss: kB\n".as_bytes(),
319 ];
320 for line in &failing_lines {
321 assert_eq!(super::skip_to_line_value(line), None);
322 }
323 }
324
325 #[test]
326 fn scanner_basic() {
327 let prefix = "Rss: ".as_bytes();
328
329 let mut scanner = super::Scanner::new();
330 let mut buf = Vec::new();
331 buf.extend_from_slice(b"Rss: 1234 kB\nRss: 5678 kB\nRss: 91011 kB\n");
332 scanner.reset(buf.as_slice());
333
334 assert_eq!(
335 scanner.next_matching_line(prefix).unwrap(),
336 Some(b"Rss: 1234 kB".as_ref())
337 );
338 assert_eq!(
339 scanner.next_matching_line(prefix).unwrap(),
340 Some(b"Rss: 5678 kB".as_ref())
341 );
342 assert_eq!(
343 scanner.next_matching_line(prefix).unwrap(),
344 Some(b"Rss: 91011 kB".as_ref())
345 );
346 assert_eq!(scanner.next_matching_line(prefix).unwrap(), None);
347 }
348
349 #[test]
350 fn scanner_skip_non_matching_lines() {
351 let prefix = "Rss: ".as_bytes();
352
353 let mut scanner = super::Scanner::new();
354 let mut buf = Vec::new();
355 buf.extend_from_slice(b"Rss: 1234 kB\nPss: 5678 kB\nHugepages: 42069 kB\nRss: 91011 kB\n");
356 scanner.reset(buf.as_slice());
357
358 assert_eq!(
359 scanner.next_matching_line(prefix).unwrap(),
360 Some(b"Rss: 1234 kB".as_ref())
361 );
362 assert_eq!(
363 scanner.next_matching_line(prefix).unwrap(),
364 Some(b"Rss: 91011 kB".as_ref())
365 );
366 assert_eq!(scanner.next_matching_line(prefix).unwrap(), None);
367 }
368
369 #[test]
370 fn scanner_skips_lines_larger_than_buffer() {
371 let prefix = "Rss: ".as_bytes();
372
373 let mut scanner = super::Scanner::new();
374
375 let mut buf = Vec::new();
378 buf.resize(9000, b'@');
379 buf.extend_from_slice(b"\nRss: 1234 kB\n");
380 scanner.reset(buf.as_slice());
381
382 assert_eq!(
383 scanner.next_matching_line(prefix).unwrap(),
384 Some(b"Rss: 1234 kB".as_ref())
385 );
386 assert_eq!(scanner.next_matching_line(prefix).unwrap(), None);
387 }
388
389 #[test]
390 fn parse_kb_value_as_bytes_converts_kilobytes_to_bytes() {
391 assert_eq!(super::parse_kb_value_as_bytes(b"1234 kB"), Some(1234 * 1024));
393 assert_eq!(super::parse_kb_value_as_bytes(b"0 kB"), Some(0));
394
395 assert_eq!(super::parse_kb_value_as_bytes(b"1234"), None);
397
398 assert_eq!(super::parse_kb_value_as_bytes(b"abc kB"), None);
400 }
401
402 #[test]
403 fn smaps_rollup_uses_the_first_rss_line() {
404 let mut scanner = super::Scanner::new();
406 scanner.reset(b"Pss: 10 kB\nRss: 512 kB\nRss: 999 kB\n".as_slice());
407 assert_eq!(super::first_rss_value(&mut scanner), Some(512 * 1024));
408 }
409
410 #[test]
411 fn smaps_sums_every_rss_line() {
412 let mut scanner = super::Scanner::new();
414 scanner.reset(b"Rss: 4 kB\nPss: 100 kB\nRss: 8 kB\nRss: 16 kB\n".as_slice());
415 assert_eq!(super::sum_rss_values(&mut scanner), Some((4 + 8 + 16) * 1024));
416 }
417
418 #[test]
419 fn smaps_strategies_return_none_when_no_rss_lines_present() {
420 let mut scanner = super::Scanner::new();
422 scanner.reset(b"Pss: 4 kB\nShared_Clean: 8 kB\n".as_slice());
423 assert_eq!(super::first_rss_value(&mut scanner), None);
424
425 let mut scanner = super::Scanner::new();
426 scanner.reset(b"Pss: 4 kB\nShared_Clean: 8 kB\n".as_slice());
427 assert_eq!(super::sum_rss_values(&mut scanner), None);
428 }
429
430 #[test]
431 fn parse_statm_rss_bytes_multiplies_resident_pages_by_page_size() {
432 assert_eq!(
434 super::parse_statm_rss_bytes(b"1000 42 5 1 0 3 0", 4096),
435 Some(42 * 4096)
436 );
437
438 assert_eq!(super::parse_statm_rss_bytes(b"1000", 4096), None);
440 assert_eq!(super::parse_statm_rss_bytes(b"1000 xyz", 4096), None);
441 }
442
443 #[test]
444 fn stat_source_selection_follows_documented_fallback_order() {
445 use super::{select_stat_source, StatSource};
446
447 assert!(matches!(
449 select_stat_source(true, true, Some(4096)),
450 StatSource::SmapsRollup(_)
451 ));
452 assert!(matches!(
453 select_stat_source(true, false, None),
454 StatSource::SmapsRollup(_)
455 ));
456
457 assert!(matches!(
459 select_stat_source(false, true, Some(4096)),
460 StatSource::Smaps(_)
461 ));
462
463 assert!(matches!(
465 select_stat_source(false, false, Some(4096)),
466 StatSource::Statm(Some(4096))
467 ));
468 assert!(matches!(
469 select_stat_source(false, false, None),
470 StatSource::Statm(None)
471 ));
472 }
473}