Skip to main content

monitrs_core/model/
memory.rs

1//! Memory and swap metrics.
2//!
3//! §8.4 and §26 both insist that Linux and macOS memory semantics are *not*
4//! equivalent. Rather than papering over the difference, every snapshot records
5//! which definition produced its headline numbers in [`MemorySemantics`], and
6//! the Inspect screen shows it.
7
8use crate::model::MetricState;
9use crate::units::{Percent, Rate};
10
11/// Which platform definition produced the headline memory numbers.
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize))]
14#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
15pub enum MemorySemantics {
16    /// `available` is `/proc/meminfo`'s `MemAvailable`, the kernel's own
17    /// estimate of allocatable memory without swapping. `used` is
18    /// `total - MemAvailable`, so page cache is *not* counted as application use.
19    LinuxMemAvailable,
20    /// `available` is derived from `host_statistics64` free plus inactive plus
21    /// purgeable pages. Wired and compressed pages are reported separately
22    /// because neither is reclaimable the way Linux page cache is.
23    MacosVmStatistics,
24    /// The cross-platform baseline reported by `sysinfo`, used when native
25    /// enrichment is unavailable. Coarser than either native definition.
26    SysinfoBaseline,
27}
28
29impl MemorySemantics {
30    /// The explanation rendered on the Inspect screen and in `docs/metrics.md`.
31    #[must_use]
32    pub const fn description(self) -> &'static str {
33        match self {
34            Self::LinuxMemAvailable => {
35                "used = total - MemAvailable; page cache and buffers are not counted as \
36                 application use"
37            }
38            Self::MacosVmStatistics => {
39                "available = free + inactive + purgeable; wired and compressed pages are \
40                 reported separately and are not reclaimable like Linux page cache"
41            }
42            Self::SysinfoBaseline => {
43                "cross-platform baseline; coarser than the native definition and not \
44                 byte-for-byte comparable with it"
45            }
46        }
47    }
48}
49
50/// The secondary memory breakdown.
51///
52/// Every field is a [`MetricState`] because the two platforms expose disjoint
53/// subsets: `buffers` is Linux-only, `wired` and `compressed` are macOS-only.
54/// §8.4 forbids labelling all non-free memory as application use, so these are
55/// presented as detail rather than folded into `used`.
56#[derive(Clone, Copy, Debug, PartialEq)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize))]
58pub struct MemoryDetail {
59    /// Page cache.
60    pub cached: MetricState<u64>,
61    /// Block-device buffers. Linux only.
62    pub buffers: MetricState<u64>,
63    /// Shared memory.
64    pub shared: MetricState<u64>,
65    /// Recently used pages.
66    pub active: MetricState<u64>,
67    /// Reclaimable pages.
68    pub inactive: MetricState<u64>,
69    /// Pages that cannot be paged out. macOS only.
70    pub wired: MetricState<u64>,
71    /// Pages held in the compressor. macOS only.
72    pub compressed: MetricState<u64>,
73    /// Pages awaiting writeback. Linux only.
74    pub dirty: MetricState<u64>,
75}
76
77impl MemoryDetail {
78    /// A breakdown with nothing measured, for the first frame.
79    pub const WARMING_UP: Self = Self {
80        cached: MetricState::WarmingUp,
81        buffers: MetricState::WarmingUp,
82        shared: MetricState::WarmingUp,
83        active: MetricState::WarmingUp,
84        inactive: MetricState::WarmingUp,
85        wired: MetricState::WarmingUp,
86        compressed: MetricState::WarmingUp,
87        dirty: MetricState::WarmingUp,
88    };
89}
90
91/// Swap capacity and activity.
92///
93/// `in_rate` and `out_rate` are the metrics that actually indicate memory
94/// distress: a large but idle swap file is unremarkable, while sustained
95/// swap-in on a small one is not (§11.2).
96#[derive(Clone, Copy, Debug, PartialEq)]
97#[cfg_attr(feature = "serde", derive(serde::Serialize))]
98pub struct SwapSnapshot {
99    /// Configured swap size. Zero means swap is disabled, which is a fact
100    /// rather than an unavailable metric.
101    pub total_bytes: u64,
102    /// Swap currently in use.
103    pub used: MetricState<u64>,
104    /// Share of swap in use.
105    pub usage: MetricState<Percent>,
106    /// Pages read back from swap per second.
107    pub in_rate: MetricState<Rate>,
108    /// Pages written to swap per second.
109    pub out_rate: MetricState<Rate>,
110}
111
112impl SwapSnapshot {
113    /// Whether swap is configured at all.
114    #[must_use]
115    pub const fn is_enabled(&self) -> bool {
116        self.total_bytes > 0
117    }
118
119    /// A snapshot for a system with swap disabled.
120    #[must_use]
121    pub const fn disabled() -> Self {
122        Self {
123            total_bytes: 0,
124            used: MetricState::Available(0),
125            usage: MetricState::Unsupported,
126            in_rate: MetricState::Unsupported,
127            out_rate: MetricState::Unsupported,
128        }
129    }
130}
131
132/// System memory state.
133#[derive(Clone, Copy, Debug, PartialEq)]
134#[cfg_attr(feature = "serde", derive(serde::Serialize))]
135pub struct MemorySnapshot {
136    /// Total physical memory. Always known.
137    pub total_bytes: u64,
138    /// Memory allocatable without reclaim pressure, per [`Self::semantics`].
139    pub available: MetricState<u64>,
140    /// `total_bytes - available`, per [`Self::semantics`].
141    pub used: MetricState<u64>,
142    /// Completely unused memory. Usually much smaller than `available`.
143    pub free: MetricState<u64>,
144    /// Share of memory in use.
145    pub usage: MetricState<Percent>,
146    /// The secondary breakdown.
147    pub detail: MemoryDetail,
148    /// Swap capacity and activity.
149    pub swap: SwapSnapshot,
150    /// Which definition produced `available` and `used`.
151    pub semantics: MemorySemantics,
152    /// The cgroup memory limit, when running under one, alongside the host
153    /// total in `total_bytes`.
154    ///
155    /// §9.2 requires container limits to be exposed *separately* from host
156    /// totals and both to be shown and labelled where observable.
157    pub cgroup_limit_bytes: MetricState<u64>,
158}
159
160impl MemorySnapshot {
161    /// A snapshot with only the total known, for the first frame.
162    #[must_use]
163    pub const fn warming_up(total_bytes: u64, semantics: MemorySemantics) -> Self {
164        Self {
165            total_bytes,
166            available: MetricState::WarmingUp,
167            used: MetricState::WarmingUp,
168            free: MetricState::WarmingUp,
169            usage: MetricState::WarmingUp,
170            detail: MemoryDetail::WARMING_UP,
171            swap: SwapSnapshot {
172                total_bytes: 0,
173                used: MetricState::WarmingUp,
174                usage: MetricState::WarmingUp,
175                in_rate: MetricState::WarmingUp,
176                out_rate: MetricState::WarmingUp,
177            },
178            semantics,
179            cgroup_limit_bytes: MetricState::WarmingUp,
180        }
181    }
182
183    /// The memory ceiling that actually applies to this process tree.
184    ///
185    /// Inside a container this is the cgroup limit, not the host total; §9.2
186    /// requires the distinction to be observable rather than silently folded
187    /// into one number.
188    #[must_use]
189    pub fn effective_limit_bytes(&self) -> u64 {
190        match self.cgroup_limit_bytes.fresh() {
191            Some(&limit) if limit > 0 && limit < self.total_bytes => limit,
192            _ => self.total_bytes,
193        }
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn each_platform_semantics_explains_itself() {
203        for semantics in [
204            MemorySemantics::LinuxMemAvailable,
205            MemorySemantics::MacosVmStatistics,
206            MemorySemantics::SysinfoBaseline,
207        ] {
208            assert!(!semantics.description().is_empty());
209        }
210        // The Linux description must state that cache is not application use.
211        assert!(
212            MemorySemantics::LinuxMemAvailable
213                .description()
214                .contains("page cache")
215        );
216    }
217
218    #[test]
219    fn disabled_swap_is_a_fact_not_an_unavailable_metric() {
220        let swap = SwapSnapshot::disabled();
221        assert!(!swap.is_enabled());
222        assert_eq!(
223            swap.used.fresh(),
224            Some(&0),
225            "0 of 0 bytes used is a real measurement"
226        );
227        // ...but a percentage of zero capacity is genuinely undefined.
228        assert!(swap.usage.fresh().is_none());
229    }
230
231    #[test]
232    fn a_cgroup_limit_below_the_host_total_becomes_the_effective_ceiling() {
233        let mut memory =
234            MemorySnapshot::warming_up(32 * 1024 * 1024 * 1024, MemorySemantics::LinuxMemAvailable);
235        assert_eq!(memory.effective_limit_bytes(), 32 * 1024 * 1024 * 1024);
236
237        memory.cgroup_limit_bytes = MetricState::Available(2 * 1024 * 1024 * 1024);
238        assert_eq!(memory.effective_limit_bytes(), 2 * 1024 * 1024 * 1024);
239    }
240
241    #[test]
242    fn an_unlimited_cgroup_does_not_shrink_the_ceiling() {
243        let mut memory =
244            MemorySnapshot::warming_up(32 * 1024 * 1024 * 1024, MemorySemantics::LinuxMemAvailable);
245        // cgroup v2 writes an enormous sentinel for "max".
246        memory.cgroup_limit_bytes = MetricState::Available(u64::MAX);
247        assert_eq!(memory.effective_limit_bytes(), 32 * 1024 * 1024 * 1024);
248        memory.cgroup_limit_bytes = MetricState::Available(0);
249        assert_eq!(memory.effective_limit_bytes(), 32 * 1024 * 1024 * 1024);
250    }
251
252    #[test]
253    fn warming_up_preserves_the_requested_semantics() {
254        let memory = MemorySnapshot::warming_up(1024, MemorySemantics::MacosVmStatistics);
255        assert_eq!(memory.semantics, MemorySemantics::MacosVmStatistics);
256        assert_eq!(memory.total_bytes, 1024);
257        assert!(memory.available.is_warming_up());
258    }
259}