use std::collections::{HashMap, HashSet};
use std::fs;
use super::SysRoots;
use crate::snapshot::{OomKill, ProcessInfo};
pub const OOM_KILL_WINDOW_MS: u64 = 60 * 60 * 1000;
const LIKELY_MIN_BYTES: u64 = 64 << 20;
#[derive(Debug, Default)]
pub struct OomTracker {
last_count: Option<u64>,
previous: HashMap<u32, (String, u64)>,
kills: Vec<OomKill>,
}
impl OomTracker {
pub fn new() -> Self {
Self::default()
}
pub fn sample(
&mut self,
roots: &SysRoots,
processes: &[ProcessInfo],
now_ms: u64,
) -> Vec<OomKill> {
let count = fs::read_to_string(roots.proc.join("vmstat"))
.ok()
.and_then(|text| parse_oom_kill(&text));
if let (Some(count), Some(last)) = (count, self.last_count) {
let new = count.saturating_sub(last);
if new > 0 {
let likely = likely_victims(&self.previous, processes, new as usize);
for i in 0..new as usize {
self.kills.push(OomKill {
at_ms: now_ms,
likely_process: likely.get(i).cloned(),
});
}
}
}
if count.is_some() {
self.last_count = count;
}
self.previous = processes
.iter()
.map(|p| (p.pid, (p.name.clone(), p.memory_bytes)))
.collect();
self.kills
.retain(|k| now_ms.saturating_sub(k.at_ms) < OOM_KILL_WINDOW_MS);
self.kills.clone()
}
}
pub(crate) fn parse_oom_kill(text: &str) -> Option<u64> {
text.lines()
.find_map(|line| line.strip_prefix("oom_kill "))
.and_then(|value| value.trim().parse().ok())
}
fn likely_victims(
previous: &HashMap<u32, (String, u64)>,
current: &[ProcessInfo],
count: usize,
) -> Vec<String> {
let alive: HashSet<u32> = current.iter().map(|p| p.pid).collect();
let mut gone: Vec<&(String, u64)> = previous
.iter()
.filter(|(pid, (_, bytes))| !alive.contains(pid) && *bytes >= LIKELY_MIN_BYTES)
.map(|(_, process)| process)
.collect();
gone.sort_by_key(|(_, bytes)| std::cmp::Reverse(*bytes));
gone.into_iter()
.take(count)
.map(|(name, _)| name.clone())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::linux::testutil::write;
fn process(pid: u32, name: &str, gib: u64) -> ProcessInfo {
ProcessInfo {
pid,
name: name.to_owned(),
memory_bytes: gib << 30,
..Default::default()
}
}
#[test]
fn parses_the_counter() {
let text = "pgfault 123\noom_kill 4\nnr_free_pages 5\n";
assert_eq!(parse_oom_kill(text), Some(4));
assert_eq!(parse_oom_kill("pgfault 123\n"), None);
}
#[test]
fn records_kills_with_the_likely_process_and_forgets_them() {
let tmp = tempfile::tempdir().unwrap();
let roots = SysRoots {
sys: tmp.path().join("sys"),
proc: tmp.path().join("proc"),
};
let mut tracker = OomTracker::new();
let before = vec![
process(1, "systemd", 0),
process(10, "python3", 60),
process(11, "bash", 0),
];
write(tmp.path(), "proc/vmstat", "oom_kill 3\n");
assert!(tracker.sample(&roots, &before, 1_000).is_empty());
write(tmp.path(), "proc/vmstat", "oom_kill 5\n");
let after = vec![process(1, "systemd", 0)];
let kills = tracker.sample(&roots, &after, 2_000);
let kill = |name: Option<&str>| OomKill {
at_ms: 2_000,
likely_process: name.map(str::to_owned),
};
assert_eq!(kills, vec![kill(Some("python3")), kill(None)]);
assert_eq!(tracker.sample(&roots, &after, 3_000).len(), 2);
assert!(
tracker
.sample(&roots, &after, 2_000 + OOM_KILL_WINDOW_MS)
.is_empty()
);
}
}