Skip to main content

mnemosyne_backend/
recorders.rs

1//! Telemetry and stats tracking for OS virtual memory mappings.
2//!
3//! Pure recorder counters and the per-concern unit tests for `record_*`.
4//! The recorder is exposed as `pub(crate)` so sibling concern modules
5//! ([`crate::mapping`], [`crate::guard`], [`crate::reset`]) can update
6//! counters on confirmed OS outcomes; external consumers reach the
7//! snapshot through [`backend_memory_stats`] and [`BackendMemoryStats`].
8
9use core::sync::atomic::{AtomicUsize, Ordering};
10
11static CURRENT_MAPPED_BYTES: AtomicUsize = AtomicUsize::new(0);
12static PEAK_MAPPED_BYTES: AtomicUsize = AtomicUsize::new(0);
13static MAP_CALLS: AtomicUsize = AtomicUsize::new(0);
14static UNMAP_CALLS: AtomicUsize = AtomicUsize::new(0);
15static PAGE_RESET_CALLS: AtomicUsize = AtomicUsize::new(0);
16static PAGE_RESET_BYTES: AtomicUsize = AtomicUsize::new(0);
17static GUARD_INSTALL_CALLS: AtomicUsize = AtomicUsize::new(0);
18static GUARD_INSTALL_BYTES: AtomicUsize = AtomicUsize::new(0);
19static DECOMMIT_CALLS: AtomicUsize = AtomicUsize::new(0);
20static DECOMMIT_BYTES: AtomicUsize = AtomicUsize::new(0);
21
22/// Snapshot of OS mappings requested by Mnemosyne.
23#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
24pub struct BackendMemoryStats {
25    pub current_mapped_bytes: usize,
26    pub peak_mapped_bytes: usize,
27    pub map_calls: usize,
28    pub unmap_calls: usize,
29    /// Number of `page_reset` calls that the OS confirmed.
30    ///
31    /// A reset releases the physical backing of an addressed range while
32    /// keeping the virtual mapping intact, so this counter is independent
33    /// of `unmap_calls` and `current_mapped_bytes` is not decremented.
34    pub page_reset_calls: usize,
35    /// Cumulative byte count passed to confirmed `page_reset` calls.
36    pub page_reset_bytes: usize,
37    /// Number of `make_guard` calls the OS confirmed.
38    ///
39    /// A guard install changes only the protection bits of an addressed
40    /// range; the mapping remains reserved, so this counter is
41    /// independent of `unmap_calls` and `current_mapped_bytes` is not
42    /// decremented.
43    pub guard_install_calls: usize,
44    /// Cumulative byte count passed to confirmed `make_guard` calls.
45    pub guard_install_bytes: usize,
46    /// Number of `decommit` calls the OS confirmed.
47    ///
48    /// A decommit releases the commit charge / resident backing of an addressed
49    /// range while keeping the reservation, so this counter is independent of
50    /// `unmap_calls` and `current_mapped_bytes` is not decremented (the address
51    /// space remains reserved until `deallocate`).
52    pub decommit_calls: usize,
53    /// Cumulative byte count passed to confirmed `decommit` calls (the commit
54    /// charge / resident backing returned to the OS).
55    pub decommit_bytes: usize,
56}
57
58#[inline]
59pub(crate) fn record_map(size: usize) {
60    MAP_CALLS.fetch_add(1, Ordering::Relaxed);
61    let current = CURRENT_MAPPED_BYTES.fetch_add(size, Ordering::Relaxed) + size;
62    if current > PEAK_MAPPED_BYTES.load(Ordering::Relaxed) {
63        PEAK_MAPPED_BYTES.fetch_max(current, Ordering::Relaxed);
64    }
65}
66
67#[inline]
68pub(crate) fn record_unmap(size: usize) {
69    UNMAP_CALLS.fetch_add(1, Ordering::Relaxed);
70    CURRENT_MAPPED_BYTES.fetch_sub(size, Ordering::Relaxed);
71}
72
73/// Records an attempted but failed OS release.
74///
75/// Increments only the call counter so `current_mapped_bytes` stays consistent
76/// with the OS-side mapping set when the release call itself failed.
77#[inline]
78pub(crate) fn record_unmap_failure() {
79    UNMAP_CALLS.fetch_add(1, Ordering::Relaxed);
80}
81
82/// Records a confirmed page reset.
83///
84/// Unlike `record_unmap`, this does not decrement `current_mapped_bytes`
85/// because the virtual mapping is still committed and remains observable
86/// through the allocator's address-space accounting; the OS has only
87/// released the underlying physical backing.
88#[inline]
89pub(crate) fn record_page_reset(size: usize) {
90    PAGE_RESET_CALLS.fetch_add(1, Ordering::Relaxed);
91    PAGE_RESET_BYTES.fetch_add(size, Ordering::Relaxed);
92}
93
94/// Records a confirmed guard-region install.
95///
96/// Same accounting rationale as `record_page_reset`: the mapping remains
97/// reserved and `current_mapped_bytes` is intentionally unchanged. The
98/// counter increments lets external monitors observe how much of the
99/// reserved address space has been converted into guard regions.
100#[inline]
101pub(crate) fn record_guard_install(size: usize) {
102    GUARD_INSTALL_CALLS.fetch_add(1, Ordering::Relaxed);
103    GUARD_INSTALL_BYTES.fetch_add(size, Ordering::Relaxed);
104}
105
106/// Records a confirmed decommit.
107///
108/// Same accounting rationale as `record_page_reset`: the reservation remains,
109/// so `current_mapped_bytes` is intentionally unchanged; only the commit
110/// charge / resident backing was returned to the OS.
111#[inline]
112pub(crate) fn record_decommit(size: usize) {
113    DECOMMIT_CALLS.fetch_add(1, Ordering::Relaxed);
114    DECOMMIT_BYTES.fetch_add(size, Ordering::Relaxed);
115}
116
117/// Returns the current backend memory mapping counters.
118///
119/// The snapshot uses relaxed atomics because these counters are telemetry only:
120/// allocator correctness never depends on cross-counter synchronization.
121pub fn backend_memory_stats() -> BackendMemoryStats {
122    BackendMemoryStats {
123        current_mapped_bytes: CURRENT_MAPPED_BYTES.load(Ordering::Relaxed),
124        peak_mapped_bytes: PEAK_MAPPED_BYTES.load(Ordering::Relaxed),
125        map_calls: MAP_CALLS.load(Ordering::Relaxed),
126        unmap_calls: UNMAP_CALLS.load(Ordering::Relaxed),
127        page_reset_calls: PAGE_RESET_CALLS.load(Ordering::Relaxed),
128        page_reset_bytes: PAGE_RESET_BYTES.load(Ordering::Relaxed),
129        guard_install_calls: GUARD_INSTALL_CALLS.load(Ordering::Relaxed),
130        guard_install_bytes: GUARD_INSTALL_BYTES.load(Ordering::Relaxed),
131        decommit_calls: DECOMMIT_CALLS.load(Ordering::Relaxed),
132        decommit_bytes: DECOMMIT_BYTES.load(Ordering::Relaxed),
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    extern crate std;
139
140    use super::*;
141
142    static TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
143
144    #[test]
145    fn mapping_telemetry_tracks_deltas_and_peak() {
146        let _guard = TEST_LOCK
147            .lock()
148            .expect("backend telemetry test lock was poisoned");
149        let before = backend_memory_stats();
150        let size = 4096;
151
152        record_map(size);
153        let during = backend_memory_stats();
154
155        assert_eq!(
156            during.current_mapped_bytes,
157            before.current_mapped_bytes + size
158        );
159        assert_eq!(during.map_calls, before.map_calls + 1);
160        assert_eq!(during.unmap_calls, before.unmap_calls);
161        assert!(
162            during.peak_mapped_bytes >= during.current_mapped_bytes,
163            "peak {} below current {} after record_map",
164            during.peak_mapped_bytes,
165            during.current_mapped_bytes
166        );
167        assert!(
168            during.peak_mapped_bytes >= before.peak_mapped_bytes,
169            "peak {} below pre-map peak {}",
170            during.peak_mapped_bytes,
171            before.peak_mapped_bytes
172        );
173
174        record_unmap(size);
175        let after = backend_memory_stats();
176
177        assert_eq!(after.current_mapped_bytes, before.current_mapped_bytes);
178        assert_eq!(after.map_calls, before.map_calls + 1);
179        assert_eq!(after.unmap_calls, before.unmap_calls + 1);
180        assert!(
181            after.peak_mapped_bytes >= during.peak_mapped_bytes,
182            "peak {} regressed below mid-cycle peak {}",
183            after.peak_mapped_bytes,
184            during.peak_mapped_bytes
185        );
186    }
187
188    #[test]
189    fn failed_release_increments_call_count_without_byte_delta() {
190        let _guard = TEST_LOCK
191            .lock()
192            .expect("backend telemetry test lock was poisoned");
193        let before = backend_memory_stats();
194        let size = 4096;
195
196        record_map(size);
197        let mapped = backend_memory_stats();
198        assert_eq!(
199            mapped.current_mapped_bytes,
200            before.current_mapped_bytes + size
201        );
202
203        // Simulate a failed OS release: the wrapper increments the call counter
204        // but must not subtract bytes that remain mapped from the OS perspective.
205        record_unmap_failure();
206        let failed = backend_memory_stats();
207        assert_eq!(failed.current_mapped_bytes, mapped.current_mapped_bytes);
208        assert_eq!(failed.unmap_calls, mapped.unmap_calls + 1);
209        assert_eq!(failed.map_calls, mapped.map_calls);
210
211        record_unmap(size);
212        let cleared = backend_memory_stats();
213        assert_eq!(cleared.current_mapped_bytes, before.current_mapped_bytes);
214    }
215
216    #[test]
217    fn page_reset_telemetry_increments_call_and_byte_counters_only() {
218        let _guard = TEST_LOCK
219            .lock()
220            .expect("backend telemetry test lock was poisoned");
221        // record_page_reset must increment both call and byte counters
222        // without touching current_mapped_bytes, because a reset releases
223        // physical backing while leaving the virtual mapping committed.
224        let before = backend_memory_stats();
225        let size = 8192;
226
227        record_page_reset(size);
228        let after = backend_memory_stats();
229
230        assert_eq!(
231            after.page_reset_calls,
232            before.page_reset_calls + 1,
233            "page_reset_calls counter did not advance"
234        );
235        assert_eq!(
236            after.page_reset_bytes,
237            before.page_reset_bytes + size,
238            "page_reset_bytes counter did not advance by the reset size"
239        );
240        assert_eq!(
241            after.current_mapped_bytes, before.current_mapped_bytes,
242            "page_reset must not decrement current_mapped_bytes"
243        );
244        assert_eq!(
245            after.unmap_calls, before.unmap_calls,
246            "page_reset must not increment unmap_calls"
247        );
248    }
249
250    #[test]
251    fn decommit_telemetry_increments_call_and_byte_counters_only() {
252        let _guard = TEST_LOCK
253            .lock()
254            .expect("backend telemetry test lock was poisoned");
255        // record_decommit must increment both counters without touching
256        // current_mapped_bytes (the reservation persists) or the unmap/reset
257        // counters.
258        let before = backend_memory_stats();
259        let size = 64 * 1024;
260
261        record_decommit(size);
262        let after = backend_memory_stats();
263
264        assert_eq!(
265            after.decommit_calls,
266            before.decommit_calls + 1,
267            "decommit_calls counter did not advance"
268        );
269        assert_eq!(
270            after.decommit_bytes,
271            before.decommit_bytes + size,
272            "decommit_bytes counter did not advance by the decommit size"
273        );
274        assert_eq!(
275            after.current_mapped_bytes, before.current_mapped_bytes,
276            "decommit must not decrement current_mapped_bytes (reservation persists)"
277        );
278        assert_eq!(after.unmap_calls, before.unmap_calls);
279        assert_eq!(after.page_reset_calls, before.page_reset_calls);
280    }
281
282    #[test]
283    fn guard_telemetry_increments_call_and_byte_counters_only() {
284        let _guard = TEST_LOCK
285            .lock()
286            .expect("backend telemetry test lock was poisoned");
287        // record_guard_install must increment both counters without
288        // perturbing current_mapped_bytes, page_reset, or unmap counters.
289        let before = backend_memory_stats();
290        let size = 4096;
291        record_guard_install(size);
292        let after = backend_memory_stats();
293        assert_eq!(
294            after.guard_install_calls,
295            before.guard_install_calls + 1,
296            "guard_install_calls counter did not advance"
297        );
298        assert_eq!(
299            after.guard_install_bytes,
300            before.guard_install_bytes + size,
301            "guard_install_bytes counter did not advance by the guard size"
302        );
303        assert_eq!(
304            after.current_mapped_bytes, before.current_mapped_bytes,
305            "make_guard must not decrement current_mapped_bytes"
306        );
307        assert_eq!(after.page_reset_calls, before.page_reset_calls);
308        assert_eq!(after.unmap_calls, before.unmap_calls);
309    }
310}