Skip to main content

mnemosyne_local/local_alloc/
stats.rs

1use crate::local_alloc::{CROSS_THREAD_RECLAIMED_BLOCKS, ThreadAllocator};
2use core::ptr::NonNull;
3use core::sync::atomic::Ordering;
4use mnemosyne_arena::HasSegmentPool;
5use mnemosyne_core::constants::NUM_SIZE_CLASSES;
6use mnemosyne_core::types::Page;
7
8/// Occupancy counters for a single size class in the current thread allocator.
9#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
10pub struct SizeClassOccupancy {
11    /// Pages holding at least one live allocation.
12    pub active_pages: usize,
13    /// Pages with no live allocation, retained for reuse rather than
14    /// returned, so a refill of this size class costs no syscall.
15    pub empty_pages: usize,
16    /// Allocations currently handed out from this size class.
17    pub live_allocations: usize,
18    /// Slots across all pages of this size class, live or free.
19    ///
20    /// Against [`Self::live_allocations`] this gives the class's internal
21    /// fragmentation: a large gap means pages are pinned by a few
22    /// scattered survivors.
23    pub total_slots: usize,
24}
25
26/// Snapshot of the current thread-local allocator state.
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub struct ThreadAllocatorStats {
29    /// Allocations currently handed out by this thread across every size
30    /// class.
31    pub current_thread_live_allocations: usize,
32    /// Segments this thread owns and allocates from without coordination.
33    pub current_thread_owned_segments: usize,
34    /// Blocks freed by another thread and returned to this thread's pages.
35    ///
36    /// Cross-thread frees are queued to the owning thread rather than
37    /// mutating its pages directly, so this counts the deferred returns
38    /// that have been drained.
39    pub cross_thread_reclaimed_blocks: usize,
40    /// Times a size class exhausted its page and had to acquire another.
41    ///
42    /// The sum of the three sources below, and the headline number for how
43    /// often the fast path fell through.
44    pub page_refills: usize,
45    /// Refills served from an empty page this thread already held — the
46    /// cheapest outcome, no segment or OS work.
47    pub recycled_pages: usize,
48    /// Refills that carved a new page from an owned segment.
49    pub fresh_pages: usize,
50    /// Refills that required a new segment, the only source that can reach
51    /// the OS backend.
52    pub fresh_segments: usize,
53    /// Segments inherited from threads that exited while still owning them.
54    ///
55    /// Adoption is what keeps a terminated thread's memory reusable
56    /// instead of stranded until process exit.
57    pub orphan_segments_adopted: usize,
58    /// Sweeps over pages looking for empties to recycle.
59    ///
60    /// Against [`Self::recycled_pages`] this shows whether sweeping is
61    /// paying for itself or scanning without finding reusable pages.
62    pub recycle_sweeps: usize,
63    /// Per-size-class occupancy, indexed by size class.
64    pub size_class_occupancy: [SizeClassOccupancy; NUM_SIZE_CLASSES],
65}
66
67impl Default for ThreadAllocatorStats {
68    fn default() -> Self {
69        Self {
70            current_thread_live_allocations: 0,
71            current_thread_owned_segments: 0,
72            cross_thread_reclaimed_blocks: 0,
73            page_refills: 0,
74            recycled_pages: 0,
75            fresh_pages: 0,
76            fresh_segments: 0,
77            orphan_segments_adopted: 0,
78            recycle_sweeps: 0,
79            size_class_occupancy: [SizeClassOccupancy::default(); NUM_SIZE_CLASSES],
80        }
81    }
82}
83
84impl<B: HasSegmentPool> ThreadAllocator<B> {
85    /// Returns a statistics snapshot for this thread allocator.
86    ///
87    /// The snapshot walks the allocator's active/full/empty page lists instead
88    /// of every page in every owned segment. The page lists are the
89    /// authoritative membership structure for initialized pages, so diagnostic
90    /// work scales with pages that carry allocator state rather than
91    /// `owned_segment_count * PAGES_PER_SEGMENT`.
92    pub fn stats(&self) -> ThreadAllocatorStats {
93        let mut snapshot = ThreadAllocatorStats {
94            current_thread_owned_segments: self.owned_segment_count,
95            // Process-wide total: the global fold point (contributions from
96            // already-terminated threads) plus this live thread's own
97            // not-yet-folded count. This reproduces the pre-split observable
98            // for the calling thread exactly.
99            cross_thread_reclaimed_blocks: CROSS_THREAD_RECLAIMED_BLOCKS.load(Ordering::Relaxed)
100                + self.cross_thread_reclaimed,
101            page_refills: self.page_refills,
102            recycled_pages: self.recycled_pages,
103            fresh_pages: self.fresh_pages,
104            fresh_segments: self.fresh_segments,
105            orphan_segments_adopted: self.orphan_segments_adopted,
106            recycle_sweeps: self.recycle_sweeps,
107            ..ThreadAllocatorStats::default()
108        };
109
110        for class in 0..NUM_SIZE_CLASSES {
111            // SAFETY: `active_pages[class]`/`full_pages[class]` are the heads of
112            // this allocator's own intrusive page lists; every linked `Page` is
113            // live and owned by this thread, satisfying the read-only walk's
114            // precondition.
115            unsafe { accumulate_active_list(&mut snapshot, self.active_pages[class]) };
116            unsafe { accumulate_active_list(&mut snapshot, self.full_pages[class]) };
117        }
118        // Empty pages are tracked separately: they retain stale size_class/block_size
119        // from their last use, so they must not be counted as live active pages.
120        // SAFETY: `empty_pages` is the head of this allocator's own empty-page
121        // list; every linked `Page` is live and owned by this thread.
122        unsafe { accumulate_empty_list(&mut snapshot, self.empty_pages) };
123
124        snapshot
125    }
126}
127
128/// Accumulates stats for pages in an active or full list.
129/// Empty pages must not pass through this function — use `accumulate_empty_list`.
130///
131/// # Safety
132///
133/// `current` must be the head of an intrusive page list owned by the calling
134/// thread's allocator; every `Page` reachable via `next_page` must be live for
135/// the duration of the walk and not mutably aliased elsewhere.
136unsafe fn accumulate_active_list(
137    snapshot: &mut ThreadAllocatorStats,
138    mut current: Option<NonNull<Page>>,
139) {
140    while let Some(page_ptr) = current {
141        // SAFETY: `page_ptr` is a live, non-null `Page` from the caller-owned
142        // list (its head, then each `next_page`); the shared `&` is sound
143        // because no mutable borrow of the page is live during this read-only
144        // diagnostic walk.
145        let page = unsafe { page_ptr.as_ref() };
146        if page.block_size > 0 {
147            let class = page.size_class as usize;
148            debug_assert!(class < NUM_SIZE_CLASSES);
149            let occupancy = &mut snapshot.size_class_occupancy[class];
150            occupancy.active_pages += 1;
151            if page.alloc_count == 0 {
152                occupancy.empty_pages += 1;
153            }
154            occupancy.live_allocations += page.alloc_count as usize;
155            occupancy.total_slots += page.max_blocks();
156            snapshot.current_thread_live_allocations += page.alloc_count as usize;
157        }
158        current = page.next_page;
159    }
160}
161
162/// Accumulates stats for pages in the empty recycle list.
163///
164/// Empty pages retain stale `size_class`/`block_size` from their last active
165/// use, so they must not be counted as live active pages or add to total_slots.
166///
167/// # Safety
168///
169/// `current` must be the head of the calling thread's allocator's empty-page
170/// list; every `Page` reachable via `next_page` must be live for the duration
171/// of the walk and not mutably aliased elsewhere.
172unsafe fn accumulate_empty_list(
173    snapshot: &mut ThreadAllocatorStats,
174    mut current: Option<NonNull<Page>>,
175) {
176    while let Some(page_ptr) = current {
177        // SAFETY: `page_ptr` is a live, non-null `Page` from the caller-owned
178        // empty list; the shared `&` is sound because no mutable borrow of the
179        // page is live during this read-only diagnostic walk.
180        let page = unsafe { page_ptr.as_ref() };
181        if page.block_size > 0 {
182            let class = page.size_class as usize;
183            debug_assert!(class < NUM_SIZE_CLASSES);
184            snapshot.size_class_occupancy[class].empty_pages += 1;
185        }
186        current = page.next_page;
187    }
188}