Skip to main content

memory_budget/
rss.rs

1//! Process-memory reader abstraction.
2//!
3//! Production code uses [`SystemRss`], which reads the platform's native
4//! measure of memory that only swap or compression can reclaim. Tests
5//! substitute a hand-rolled source so the budget logic can be driven
6//! through specific values without spawning a sub-process.
7
8use std::sync::Mutex;
9
10use sysinfo::System;
11
12use crate::footprint::SwappableFootprint;
13
14/// Abstraction over "how much memory does the current process own that
15/// the operating system cannot reclaim without swapping", in bytes.
16///
17/// Implementations may cache their reading; the budget calls
18/// [`RssSource::read_rss_bytes`] once per tick and is happy with a
19/// best-effort value.
20pub trait RssSource: Send + Sync {
21    /// Current process memory in bytes.
22    fn read_rss_bytes(&self) -> u64;
23
24    /// Installed system RAM in bytes. Used at startup when auto-deriving
25    /// the budget target. Implementations must return a positive value.
26    fn total_ram_bytes(&self) -> u64 {
27        panic!("RssSource::total_ram_bytes must be implemented");
28    }
29}
30
31/// Live reader of the current process's swappable memory footprint.
32///
33/// The reading counts anonymous and private copy-on-write pages,
34/// shared-memory pages (tmpfs, memfd, POSIX shm) and, where the platform
35/// reports them, pages already swapped out or compressed. Clean
36/// file-backed pages — memory-mapped immutable files, executable text,
37/// page cache faulted in by reads — are excluded: the kernel drops and
38/// refaults them from their file, so they can never cause swap. Plain
39/// resident-set size counts them, which lets a process that only maps a
40/// large file look like it is exhausting memory.
41///
42/// | Platform | Measurement |
43/// | --- | --- |
44/// | Linux, Android | `RssAnon + RssShmem + VmSwap` from `/proc/self/status` |
45/// | macOS, iOS | `ri_phys_footprint` from `proc_pid_rusage` |
46/// | Windows | private commit charge (`PrivateUsage`) |
47/// | other | resident-set size |
48///
49/// Every read is a handful of system calls on a buffer reused across
50/// reads, cheap enough for once-per-second sampling.
51pub struct SystemRss {
52    inner: Mutex<SystemRssInner>,
53}
54
55struct SystemRssInner {
56    system: System,
57    footprint: SwappableFootprint,
58}
59
60impl SystemRss {
61    /// Construct a reader bound to the current process.
62    ///
63    /// # Panics
64    ///
65    /// Panics if `sysinfo` cannot initialise on this platform or the
66    /// platform cannot identify the current process — both "cannot happen"
67    /// conditions.
68    #[must_use]
69    pub fn new() -> Self {
70        let mut system = System::new().expect("sysinfo cannot initialise on this platform");
71        system.refresh_memory();
72        Self {
73            inner: Mutex::new(SystemRssInner {
74                system,
75                footprint: SwappableFootprint::new(),
76            }),
77        }
78    }
79}
80
81impl Default for SystemRss {
82    fn default() -> Self {
83        Self::new()
84    }
85}
86
87impl RssSource for SystemRss {
88    fn read_rss_bytes(&self) -> u64 {
89        let mut guard = self.inner.lock().expect("system-rss lock");
90        let SystemRssInner { system, footprint } = &mut *guard;
91        footprint.read_bytes(system)
92    }
93
94    fn total_ram_bytes(&self) -> u64 {
95        let mut guard = self.inner.lock().expect("system-rss lock");
96        guard.system.refresh_memory();
97        guard.system.total_memory()
98    }
99}