#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FootprintKind {
Pss,
Rss,
}
impl FootprintKind {
pub fn as_str(self) -> &'static str {
match self {
FootprintKind::Pss => "pss",
FootprintKind::Rss => "rss",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Footprint {
pub bytes: u64,
pub kind: FootprintKind,
}
pub fn parse_smaps_rollup_pss(text: &str) -> Option<u64> {
parse_proc_kb_field(text, "Pss:")
}
pub fn parse_status_rss(text: &str) -> Option<u64> {
parse_proc_kb_field(text, "VmRSS:")
}
fn parse_proc_kb_field(text: &str, name: &str) -> Option<u64> {
for line in text.lines() {
let Some(rest) = line.strip_prefix(name) else {
continue;
};
let mut parts = rest.split_whitespace();
let value: u64 = parts.next()?.parse().ok()?;
if parts.next()? != "kB" {
return None;
}
return Some(value * 1024);
}
None
}
#[derive(Debug)]
pub struct ProbeCache<T> {
ttl_ms: u64,
latest: Option<(T, u64)>,
}
impl<T: Clone> ProbeCache<T> {
pub fn new(ttl_ms: u64) -> Self {
ProbeCache {
ttl_ms,
latest: None,
}
}
pub fn get_or_probe(&mut self, now_ms: u64, probe: impl FnOnce() -> Option<T>) -> Option<T> {
if let Some((value, taken_at)) = &self.latest {
if now_ms.saturating_sub(*taken_at) < self.ttl_ms {
return Some(value.clone());
}
}
let fresh = probe()?;
self.latest = Some((fresh.clone(), now_ms));
Some(fresh)
}
}
#[cfg(test)]
mod tests {
use super::*;
const ROLLUP: &str = "\
55d0c0000000-7ffd0f7fe000 ---p 00000000 00:00 0 [rollup]
Rss: 1048576 kB
Pss: 524288 kB
Pss_Dirty: 131072 kB
Shared_Clean: 786432 kB
";
#[test]
fn a_rollup_reports_its_single_already_summed_pss_line() {
assert_eq!(parse_smaps_rollup_pss(ROLLUP), Some(524_288 * 1024));
}
#[test]
fn a_missing_or_malformed_field_is_absent_rather_than_zero() {
assert_eq!(parse_smaps_rollup_pss("Rss: 100 kB\n"), None);
assert_eq!(parse_smaps_rollup_pss(""), None);
assert_eq!(parse_smaps_rollup_pss("Pss: notanumber kB\n"), None);
assert_eq!(parse_smaps_rollup_pss("Pss: 100 mB\n"), None);
assert_eq!(parse_smaps_rollup_pss("Pss: 100\n"), None);
}
#[test]
fn vmrss_is_the_fallback_on_a_kernel_without_a_rollup() {
let status = "Name:\tferrox-server\nVmPeak:\t 900 kB\nVmRSS:\t 4096 kB\n";
assert_eq!(parse_status_rss(status), Some(4096 * 1024));
}
#[test]
fn concurrent_callers_within_the_ttl_share_one_probe() {
let mut cache = ProbeCache::new(1000);
let mut probes = 0;
for now in [0u64, 100, 999] {
let value = cache.get_or_probe(now, || {
probes += 1;
Some(42u64)
});
assert_eq!(value, Some(42));
}
assert_eq!(probes, 1, "one probe covers the whole TTL");
let value = cache.get_or_probe(1000, || {
probes += 1;
Some(43)
});
assert_eq!(value, Some(43), "past the TTL it re-probes");
assert_eq!(probes, 2);
}
#[test]
fn a_failed_probe_is_neither_cached_nor_answered_with_a_stale_value() {
let mut cache = ProbeCache::new(100);
assert_eq!(cache.get_or_probe(0, || Some(7u64)), Some(7));
assert_eq!(
cache.get_or_probe(100, || None),
None,
"not the stale 7: the caller asked what is true now"
);
let mut probes = 0;
assert_eq!(
cache.get_or_probe(101, || {
probes += 1;
Some(8)
}),
Some(8)
);
assert_eq!(
probes, 1,
"the next caller probes rather than being told None"
);
}
}