Skip to main content

mnemosyne/
stats.rs

1use mnemosyne_core::NUM_SIZE_CLASSES;
2
3use crate::{LocalAllocatorSelector, SizeClassOccupancy};
4
5/// Snapshot of Mnemosyne memory mapping and segment cache state.
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub struct MemoryStats {
8    /// Address space currently mapped from the OS, in bytes. Reserved
9    /// space, not resident: a range stays counted after its physical
10    /// backing is released, because the mapping is still held.
11    pub current_mapped_bytes: usize,
12    /// High-water mark of [`Self::current_mapped_bytes`].
13    pub peak_mapped_bytes: usize,
14    /// Successful map requests to the OS.
15    pub map_calls: usize,
16    /// Successful unmap requests to the OS.
17    pub unmap_calls: usize,
18    /// Number of confirmed backend `page_reset` calls (Linux `MADV_DONTNEED`,
19    /// macOS/FreeBSD `MADV_FREE`, Windows `VirtualAlloc(MEM_RESET)`).
20    pub page_reset_calls: usize,
21    /// Cumulative byte count passed to confirmed `page_reset` calls.
22    pub page_reset_bytes: usize,
23    /// Number of confirmed backend `make_guard` calls (Unix `mprotect(PROT_NONE)`,
24    /// Windows `VirtualProtect(PAGE_NOACCESS)`).
25    pub guard_install_calls: usize,
26    /// Cumulative byte count passed to confirmed `make_guard` calls.
27    pub guard_install_bytes: usize,
28    /// Free segments held in the cache for reuse instead of unmapped.
29    pub retained_free_segments: usize,
30    /// Cap on [`Self::retained_free_segments`]; segments beyond it are
31    /// purged rather than retained.
32    pub max_retained_free_segments: usize,
33    /// Bytes represented by the retained free segments.
34    pub retained_free_bytes: usize,
35    /// Segments returned to the OS by decay.
36    pub purged_segments: usize,
37    /// Decay purge passes performed.
38    pub purge_calls: usize,
39    /// Bytes returned to the OS by those purges.
40    pub purged_bytes: usize,
41    /// Number of segments whose physical backing was released by a
42    /// confirmed `page_reset` while the segment itself remained cached
43    /// in the retained pool.
44    pub reset_segments: usize,
45    /// Number of `reset_segment_pool` invocations.
46    pub reset_calls: usize,
47    /// Number of huge blocks currently retained in the huge-allocation cache
48    /// across all NUMA nodes.
49    pub retained_huge_blocks: usize,
50    /// Total bytes of huge blocks currently retained in the huge-allocation
51    /// cache across all NUMA nodes.
52    pub retained_huge_bytes: usize,
53    /// Allocations currently handed out by the calling thread.
54    pub current_thread_live_allocations: usize,
55    /// Segments the calling thread owns and allocates from without
56    /// coordination.
57    pub current_thread_owned_segments: usize,
58    /// Blocks freed by another thread and drained back into this
59    /// thread's pages.
60    pub cross_thread_reclaimed_blocks: usize,
61    /// Times a size class exhausted its page and acquired another; the
62    /// sum of the three sources below.
63    pub page_refills: usize,
64    /// Refills served from an already-held empty page, the cheapest
65    /// outcome.
66    pub recycled_pages: usize,
67    /// Refills that carved a new page from an owned segment.
68    pub fresh_pages: usize,
69    /// Refills that needed a new segment, the only source reaching the
70    /// OS backend.
71    pub fresh_segments: usize,
72    /// Segments inherited from threads that exited still owning them,
73    /// which keeps their memory reusable rather than stranded.
74    pub orphan_segments_adopted: usize,
75    /// Decay-sweep passes over pages looking for empties to recycle.
76    ///
77    /// Against [`Self::recycled_pages`] this shows whether sweeping is
78    /// paying for itself or scanning without finding reusable pages.
79    pub recycle_sweeps: usize,
80    /// Per-size-class occupancy for the calling thread, indexed by size
81    /// class.
82    pub size_class_occupancy: [SizeClassOccupancy; NUM_SIZE_CLASSES],
83}
84
85impl Default for MemoryStats {
86    fn default() -> Self {
87        Self {
88            current_mapped_bytes: 0,
89            peak_mapped_bytes: 0,
90            map_calls: 0,
91            unmap_calls: 0,
92            page_reset_calls: 0,
93            page_reset_bytes: 0,
94            guard_install_calls: 0,
95            guard_install_bytes: 0,
96            retained_free_segments: 0,
97            max_retained_free_segments: 0,
98            retained_free_bytes: 0,
99            purged_segments: 0,
100            purge_calls: 0,
101            purged_bytes: 0,
102            reset_segments: 0,
103            reset_calls: 0,
104            retained_huge_blocks: 0,
105            retained_huge_bytes: 0,
106            current_thread_live_allocations: 0,
107            current_thread_owned_segments: 0,
108            cross_thread_reclaimed_blocks: 0,
109            page_refills: 0,
110            recycled_pages: 0,
111            fresh_pages: 0,
112            fresh_segments: 0,
113            orphan_segments_adopted: 0,
114            recycle_sweeps: 0,
115            size_class_occupancy: [SizeClassOccupancy::default(); NUM_SIZE_CLASSES],
116        }
117    }
118}
119
120/// Returns current Mnemosyne allocator memory counters for a specific backend.
121pub fn memory_stats_generic<B: mnemosyne_arena::HasSegmentPool + LocalAllocatorSelector<B>>()
122-> MemoryStats {
123    let backend = mnemosyne_backend::backend_memory_stats();
124    let arena = mnemosyne_arena::arena_memory_stats::<B>();
125    let local = mnemosyne_local::thread_allocator_stats::<B>();
126    MemoryStats {
127        current_mapped_bytes: backend.current_mapped_bytes,
128        peak_mapped_bytes: backend.peak_mapped_bytes,
129        map_calls: backend.map_calls,
130        unmap_calls: backend.unmap_calls,
131        page_reset_calls: backend.page_reset_calls,
132        page_reset_bytes: backend.page_reset_bytes,
133        guard_install_calls: backend.guard_install_calls,
134        guard_install_bytes: backend.guard_install_bytes,
135        retained_free_segments: arena.retained_free_segments,
136        max_retained_free_segments: arena.max_retained_free_segments,
137        retained_free_bytes: arena.retained_free_bytes,
138        purged_segments: arena.purged_segments,
139        purge_calls: arena.purge_calls,
140        purged_bytes: arena.purged_bytes,
141        reset_segments: arena.reset_segments,
142        reset_calls: arena.reset_calls,
143        retained_huge_blocks: arena.retained_huge_blocks,
144        retained_huge_bytes: arena.retained_huge_bytes,
145        current_thread_live_allocations: local.current_thread_live_allocations,
146        current_thread_owned_segments: local.current_thread_owned_segments,
147        cross_thread_reclaimed_blocks: local.cross_thread_reclaimed_blocks,
148        page_refills: local.page_refills,
149        recycled_pages: local.recycled_pages,
150        fresh_pages: local.fresh_pages,
151        fresh_segments: local.fresh_segments,
152        orphan_segments_adopted: local.orphan_segments_adopted,
153        recycle_sweeps: local.recycle_sweeps,
154        size_class_occupancy: local.size_class_occupancy,
155    }
156}
157
158/// Returns current Mnemosyne allocator memory counters.
159pub fn memory_stats() -> MemoryStats {
160    memory_stats_generic::<mnemosyne_backend::MemoryBackendWrapper>()
161}
162
163/// Purges the global segment pool for a specific backend, releasing all retained/cached segments back to the OS.
164pub fn purge_generic<B: mnemosyne_arena::HasSegmentPool>() {
165    // Safety: Purging the segment pool releases only free segments that are
166    // no longer actively referenced by any thread allocator cache.
167    unsafe {
168        mnemosyne_arena::purge_segment_pool::<B>();
169    }
170}
171
172/// Purges the global segment pool, releasing all retained/cached segments back to the OS.
173pub fn purge() {
174    purge_generic::<mnemosyne_backend::MemoryBackendWrapper>();
175}
176
177/// Asks the OS to drop the physical backing of every retained free
178/// segment for a specific backend without removing them from the cache.
179///
180/// Use this as a lighter-weight RSS-reduction knob than `purge`: the
181/// segment cache stays warm so subsequent allocations skip the OS
182/// mapping syscall, while the resident memory footprint of idle
183/// segments drops to the kernel's demand-fault baseline.
184pub fn reset_generic<B: mnemosyne_arena::HasSegmentPool>() {
185    // Safety: reset_segment_pool drains the retained pool, issues
186    // page_reset on each segment's mapping, and pushes them back into
187    // the cache; no segment is released or accessed by another path.
188    unsafe {
189        mnemosyne_arena::reset_segment_pool::<B>();
190    }
191}
192
193/// Asks the OS to drop the physical backing of every retained free
194/// segment without removing them from the cache.
195pub fn reset() {
196    reset_generic::<mnemosyne_backend::MemoryBackendWrapper>();
197}
198
199/// Triggers a manual background decay and defragmentation cycle across all active memory backends.
200pub fn decay() {
201    mnemosyne_decay::decay_step();
202}