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}