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    pub active_pages: usize,
12    pub empty_pages: usize,
13    pub live_allocations: usize,
14    pub total_slots: usize,
15}
16
17/// Snapshot of the current thread-local allocator state.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub struct ThreadAllocatorStats {
20    pub current_thread_live_allocations: usize,
21    pub current_thread_owned_segments: usize,
22    pub cross_thread_reclaimed_blocks: usize,
23    pub page_refills: usize,
24    pub recycled_pages: usize,
25    pub fresh_pages: usize,
26    pub fresh_segments: usize,
27    pub orphan_segments_adopted: usize,
28    pub recycle_sweeps: usize,
29    pub size_class_occupancy: [SizeClassOccupancy; NUM_SIZE_CLASSES],
30}
31
32impl Default for ThreadAllocatorStats {
33    fn default() -> Self {
34        Self {
35            current_thread_live_allocations: 0,
36            current_thread_owned_segments: 0,
37            cross_thread_reclaimed_blocks: 0,
38            page_refills: 0,
39            recycled_pages: 0,
40            fresh_pages: 0,
41            fresh_segments: 0,
42            orphan_segments_adopted: 0,
43            recycle_sweeps: 0,
44            size_class_occupancy: [SizeClassOccupancy::default(); NUM_SIZE_CLASSES],
45        }
46    }
47}
48
49impl<B: HasSegmentPool> ThreadAllocator<B> {
50    /// Returns a statistics snapshot for this thread allocator.
51    ///
52    /// The snapshot walks the allocator's active/full/empty page lists instead
53    /// of every page in every owned segment. The page lists are the
54    /// authoritative membership structure for initialized pages, so diagnostic
55    /// work scales with pages that carry allocator state rather than
56    /// `owned_segment_count * PAGES_PER_SEGMENT`.
57    pub fn stats(&self) -> ThreadAllocatorStats {
58        let mut snapshot = ThreadAllocatorStats {
59            current_thread_owned_segments: self.owned_segment_count,
60            // Process-wide total: the global fold point (contributions from
61            // already-terminated threads) plus this live thread's own
62            // not-yet-folded count. This reproduces the pre-split observable
63            // for the calling thread exactly.
64            cross_thread_reclaimed_blocks: CROSS_THREAD_RECLAIMED_BLOCKS.load(Ordering::Relaxed)
65                + self.cross_thread_reclaimed,
66            page_refills: self.page_refills,
67            recycled_pages: self.recycled_pages,
68            fresh_pages: self.fresh_pages,
69            fresh_segments: self.fresh_segments,
70            orphan_segments_adopted: self.orphan_segments_adopted,
71            recycle_sweeps: self.recycle_sweeps,
72            ..ThreadAllocatorStats::default()
73        };
74
75        for class in 0..NUM_SIZE_CLASSES {
76            // SAFETY: `active_pages[class]`/`full_pages[class]` are the heads of
77            // this allocator's own intrusive page lists; every linked `Page` is
78            // live and owned by this thread, satisfying the read-only walk's
79            // precondition.
80            unsafe { accumulate_active_list(&mut snapshot, self.active_pages[class]) };
81            unsafe { accumulate_active_list(&mut snapshot, self.full_pages[class]) };
82        }
83        // Empty pages are tracked separately: they retain stale size_class/block_size
84        // from their last use, so they must not be counted as live active pages.
85        // SAFETY: `empty_pages` is the head of this allocator's own empty-page
86        // list; every linked `Page` is live and owned by this thread.
87        unsafe { accumulate_empty_list(&mut snapshot, self.empty_pages) };
88
89        snapshot
90    }
91}
92
93/// Accumulates stats for pages in an active or full list.
94/// Empty pages must not pass through this function — use `accumulate_empty_list`.
95///
96/// # Safety
97///
98/// `current` must be the head of an intrusive page list owned by the calling
99/// thread's allocator; every `Page` reachable via `next_page` must be live for
100/// the duration of the walk and not mutably aliased elsewhere.
101unsafe fn accumulate_active_list(
102    snapshot: &mut ThreadAllocatorStats,
103    mut current: Option<NonNull<Page>>,
104) {
105    while let Some(page_ptr) = current {
106        // SAFETY: `page_ptr` is a live, non-null `Page` from the caller-owned
107        // list (its head, then each `next_page`); the shared `&` is sound
108        // because no mutable borrow of the page is live during this read-only
109        // diagnostic walk.
110        let page = unsafe { page_ptr.as_ref() };
111        if page.block_size > 0 {
112            let class = page.size_class as usize;
113            debug_assert!(class < NUM_SIZE_CLASSES);
114            let occupancy = &mut snapshot.size_class_occupancy[class];
115            occupancy.active_pages += 1;
116            if page.alloc_count == 0 {
117                occupancy.empty_pages += 1;
118            }
119            occupancy.live_allocations += page.alloc_count;
120            occupancy.total_slots += page.max_blocks();
121            snapshot.current_thread_live_allocations += page.alloc_count;
122        }
123        current = page.next_page;
124    }
125}
126
127/// Accumulates stats for pages in the empty recycle list.
128///
129/// Empty pages retain stale `size_class`/`block_size` from their last active
130/// use, so they must not be counted as live active pages or add to total_slots.
131///
132/// # Safety
133///
134/// `current` must be the head of the calling thread's allocator's empty-page
135/// list; every `Page` reachable via `next_page` must be live for the duration
136/// of the walk and not mutably aliased elsewhere.
137unsafe fn accumulate_empty_list(
138    snapshot: &mut ThreadAllocatorStats,
139    mut current: Option<NonNull<Page>>,
140) {
141    while let Some(page_ptr) = current {
142        // SAFETY: `page_ptr` is a live, non-null `Page` from the caller-owned
143        // empty list; the shared `&` is sound because no mutable borrow of the
144        // page is live during this read-only 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            snapshot.size_class_occupancy[class].empty_pages += 1;
150        }
151        current = page.next_page;
152    }
153}