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}