Skip to main content

mnemosyne/stats/
types.rs

1//! The snapshot type and its derived views.
2
3use core::fmt::Write as _;
4
5use mnemosyne_core::NUM_SIZE_CLASSES;
6
7use crate::SizeClassOccupancy;
8
9/// Snapshot of Mnemosyne memory mapping and segment cache state.
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub struct MemoryStats {
12    /// Address space currently mapped from the OS, in bytes. Reserved
13    /// space, not resident: a range stays counted after its physical
14    /// backing is released, because the mapping is still held.
15    pub current_mapped_bytes: usize,
16    /// High-water mark of [`Self::current_mapped_bytes`].
17    pub peak_mapped_bytes: usize,
18    /// Successful map requests to the OS.
19    pub map_calls: usize,
20    /// Successful unmap requests to the OS.
21    pub unmap_calls: usize,
22    /// Number of confirmed backend `page_reset` calls (Linux `MADV_DONTNEED`,
23    /// macOS/FreeBSD `MADV_FREE`, Windows `VirtualAlloc(MEM_RESET)`).
24    pub page_reset_calls: usize,
25    /// Cumulative byte count passed to confirmed `page_reset` calls.
26    pub page_reset_bytes: usize,
27    /// Number of confirmed backend `make_guard` calls (Unix `mprotect(PROT_NONE)`,
28    /// Windows `VirtualProtect(PAGE_NOACCESS)`).
29    pub guard_install_calls: usize,
30    /// Cumulative byte count passed to confirmed `make_guard` calls.
31    pub guard_install_bytes: usize,
32    /// Free segments held in the cache for reuse instead of unmapped.
33    pub retained_free_segments: usize,
34    /// Cap on [`Self::retained_free_segments`]; segments beyond it are
35    /// purged rather than retained.
36    pub max_retained_free_segments: usize,
37    /// Bytes represented by the retained free segments.
38    pub retained_free_bytes: usize,
39    /// Segments returned to the OS by decay.
40    pub purged_segments: usize,
41    /// Decay purge passes performed.
42    pub purge_calls: usize,
43    /// Bytes returned to the OS by those purges.
44    pub purged_bytes: usize,
45    /// Number of segments whose physical backing was released by a
46    /// confirmed `page_reset` while the segment itself remained cached
47    /// in the retained pool.
48    pub reset_segments: usize,
49    /// Number of `reset_segment_pool` invocations.
50    pub reset_calls: usize,
51    /// Number of huge blocks currently retained in the huge-allocation cache
52    /// across all NUMA nodes.
53    pub retained_huge_blocks: usize,
54    /// Total bytes of huge blocks currently retained in the huge-allocation
55    /// cache across all NUMA nodes.
56    pub retained_huge_bytes: usize,
57    /// Allocations currently handed out by the calling thread.
58    pub current_thread_live_allocations: usize,
59    /// Segments the calling thread owns and allocates from without
60    /// coordination.
61    pub current_thread_owned_segments: usize,
62    /// Blocks freed by another thread and drained back into this
63    /// thread's pages.
64    pub cross_thread_reclaimed_blocks: usize,
65    /// Times a size class exhausted its page and acquired another; the
66    /// sum of the three sources below.
67    pub page_refills: usize,
68    /// Refills served from an already-held empty page, the cheapest
69    /// outcome.
70    pub recycled_pages: usize,
71    /// Refills that carved a new page from an owned segment.
72    pub fresh_pages: usize,
73    /// Refills that needed a new segment, the only source reaching the
74    /// OS backend.
75    pub fresh_segments: usize,
76    /// Segments inherited from threads that exited still owning them,
77    /// which keeps their memory reusable rather than stranded.
78    pub orphan_segments_adopted: usize,
79    /// Decay-sweep passes over pages looking for empties to recycle.
80    ///
81    /// Against [`Self::recycled_pages`] this shows whether sweeping is
82    /// paying for itself or scanning without finding reusable pages.
83    pub recycle_sweeps: usize,
84    /// Per-size-class occupancy for the calling thread, indexed by size
85    /// class.
86    pub size_class_occupancy: [SizeClassOccupancy; NUM_SIZE_CLASSES],
87}
88
89impl Default for MemoryStats {
90    fn default() -> Self {
91        Self {
92            current_mapped_bytes: 0,
93            peak_mapped_bytes: 0,
94            map_calls: 0,
95            unmap_calls: 0,
96            page_reset_calls: 0,
97            page_reset_bytes: 0,
98            guard_install_calls: 0,
99            guard_install_bytes: 0,
100            retained_free_segments: 0,
101            max_retained_free_segments: 0,
102            retained_free_bytes: 0,
103            purged_segments: 0,
104            purge_calls: 0,
105            purged_bytes: 0,
106            reset_segments: 0,
107            reset_calls: 0,
108            retained_huge_blocks: 0,
109            retained_huge_bytes: 0,
110            current_thread_live_allocations: 0,
111            current_thread_owned_segments: 0,
112            cross_thread_reclaimed_blocks: 0,
113            page_refills: 0,
114            recycled_pages: 0,
115            fresh_pages: 0,
116            fresh_segments: 0,
117            orphan_segments_adopted: 0,
118            recycle_sweeps: 0,
119            size_class_occupancy: [SizeClassOccupancy::default(); NUM_SIZE_CLASSES],
120        }
121    }
122}
123
124impl MemoryStats {
125    /// Serializes this snapshot plus per-bin counters to a JSON string.
126    ///
127    /// Use [`crate::stats::memory_stats_json`] for a convenient one-call version that
128    /// captures the current stats automatically.
129    pub fn to_json_with_bins(
130        &self,
131        bins: &[mnemosyne_local::BinSnapshot],
132    ) -> alloc::string::String {
133        use alloc::format;
134        use alloc::string::String;
135
136        let mut out = String::with_capacity(4096);
137        out.push('{');
138        macro_rules! kv_usize {
139            ($key:expr, $val:expr, $comma:expr) => {
140                if $comma {
141                    out.push(',');
142                }
143                out.push('"');
144                out.push_str($key);
145                out.push_str("\":");
146                out.push_str(&format!("{}", $val));
147            };
148        }
149        kv_usize!("current_mapped_bytes", self.current_mapped_bytes, false);
150        kv_usize!("peak_mapped_bytes", self.peak_mapped_bytes, true);
151        kv_usize!("map_calls", self.map_calls, true);
152        kv_usize!("unmap_calls", self.unmap_calls, true);
153        kv_usize!("page_reset_calls", self.page_reset_calls, true);
154        kv_usize!("page_reset_bytes", self.page_reset_bytes, true);
155        kv_usize!(
156            "decommit_bytes",
157            mnemosyne_backend::backend_memory_stats().decommit_bytes,
158            true
159        );
160        kv_usize!("purged_bytes", self.purged_bytes, true);
161        kv_usize!("retained_free_segments", self.retained_free_segments, true);
162        kv_usize!(
163            "max_retained_free_segments",
164            self.max_retained_free_segments,
165            true
166        );
167        kv_usize!("retained_free_bytes", self.retained_free_bytes, true);
168        kv_usize!("purged_segments", self.purged_segments, true);
169        kv_usize!("purge_calls", self.purge_calls, true);
170        kv_usize!("reset_segments", self.reset_segments, true);
171        kv_usize!("reset_calls", self.reset_calls, true);
172        kv_usize!("retained_huge_blocks", self.retained_huge_blocks, true);
173        kv_usize!("retained_huge_bytes", self.retained_huge_bytes, true);
174        kv_usize!(
175            "current_thread_live_allocations",
176            self.current_thread_live_allocations,
177            true
178        );
179        kv_usize!(
180            "current_thread_owned_segments",
181            self.current_thread_owned_segments,
182            true
183        );
184        kv_usize!(
185            "cross_thread_reclaimed_blocks",
186            self.cross_thread_reclaimed_blocks,
187            true
188        );
189        kv_usize!("page_refills", self.page_refills, true);
190        kv_usize!("recycled_pages", self.recycled_pages, true);
191        kv_usize!("fresh_pages", self.fresh_pages, true);
192        kv_usize!("fresh_segments", self.fresh_segments, true);
193        kv_usize!(
194            "orphan_segments_adopted",
195            self.orphan_segments_adopted,
196            true
197        );
198        kv_usize!("recycle_sweeps", self.recycle_sweeps, true);
199        // Per-bin array
200        out.push_str(",\"bins\":[");
201        for (i, bin) in bins.iter().enumerate() {
202            if i > 0 {
203                out.push(',');
204            }
205            // `write!` formats straight into `out`; `push_str(&format!(..))`
206            // would allocate a second `String` per bin only to copy it in.
207            let _ = write!(
208                out,
209                "{{\"block_size\":{},\"alloc_count\":{},\"dealloc_count\":{},\
210                 \"live_estimate\":{},\"requested_bytes\":{},\
211                 \"fragmentation\":{:.4},\"internal_fragmentation\":{:.4}}}",
212                bin.block_size,
213                bin.alloc_count,
214                bin.dealloc_count,
215                bin.live_estimate,
216                bin.requested_bytes,
217                bin.fragmentation_ratio(),
218                bin.internal_fragmentation_ratio()
219            );
220        }
221        out.push_str("]}");
222        out
223    }
224}