1use core::sync::atomic::{AtomicUsize, Ordering::Relaxed};
20
21const SHARDS: usize = 16;
24const SHARD_BITS: u32 = SHARDS.trailing_zeros();
25
26const STACK_SHIFT: u32 = 16;
30
31const PEAK_SAMPLE_ALLOCS: usize = 1024;
35
36const PEAK_SAMPLE_BYTES: usize = 1 << 20;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub struct MemStats {
45 pub live_bytes: u64,
47 pub peak_bytes: u64,
49 pub alloc_count: u64,
53 pub free_count: u64,
55}
56
57#[repr(align(64))]
60struct Shard {
61 live: AtomicUsize,
62 allocs: AtomicUsize,
63 frees: AtomicUsize,
64}
65
66impl Shard {
67 const fn new() -> Self {
68 Self {
69 live: AtomicUsize::new(0),
70 allocs: AtomicUsize::new(0),
71 frees: AtomicUsize::new(0),
72 }
73 }
74}
75
76pub(crate) struct Counters {
80 shards: [Shard; SHARDS],
81 peak: AtomicUsize,
82}
83
84impl Counters {
85 pub(crate) const fn new() -> Self {
86 Self {
87 shards: [const { Shard::new() }; SHARDS],
88 peak: AtomicUsize::new(0),
89 }
90 }
91
92 fn shard(&self) -> &Shard {
93 &self.shards[current_shard()]
94 }
95
96 pub(crate) fn record_alloc(&self, size: usize) {
97 let shard = self.shard();
98 shard.live.fetch_add(size, Relaxed);
99 let allocs = shard.allocs.fetch_add(1, Relaxed).wrapping_add(1);
100 if size >= PEAK_SAMPLE_BYTES || allocs.is_multiple_of(PEAK_SAMPLE_ALLOCS) {
101 self.refresh_peak();
102 }
103 }
104
105 pub(crate) fn record_free(&self, size: usize) {
106 let shard = self.shard();
107 shard.live.fetch_sub(size, Relaxed);
108 shard.frees.fetch_add(1, Relaxed);
109 }
110
111 pub(crate) fn record_realloc(&self, old_size: usize, new_size: usize) {
114 let shard = self.shard();
115 if new_size >= old_size {
116 let grew = new_size - old_size;
117 shard.live.fetch_add(grew, Relaxed);
118 if grew >= PEAK_SAMPLE_BYTES {
119 self.refresh_peak();
120 }
121 } else {
122 shard.live.fetch_sub(old_size - new_size, Relaxed);
123 }
124 }
125
126 fn live(&self) -> usize {
130 self.shards
131 .iter()
132 .fold(0usize, |sum, s| sum.wrapping_add(s.live.load(Relaxed)))
133 }
134
135 fn refresh_peak(&self) -> usize {
136 let live = self.live();
137 self.peak.fetch_max(live, Relaxed);
138 live
139 }
140
141 #[cfg(test)]
144 fn touched_shards(&self) -> usize {
145 self.shards
146 .iter()
147 .filter(|s| s.allocs.load(Relaxed) > 0)
148 .count()
149 }
150
151 pub(crate) fn alloc_count(&self) -> Option<u64> {
155 let count = self
156 .shards
157 .iter()
158 .fold(0u64, |sum, s| sum + s.allocs.load(Relaxed) as u64);
159 (count > 0).then_some(count)
160 }
161
162 pub(crate) fn snapshot(&self) -> Option<MemStats> {
165 let (alloc_count, free_count) = self.shards.iter().fold((0u64, 0u64), |(a, f), s| {
166 (
167 a + s.allocs.load(Relaxed) as u64,
168 f + s.frees.load(Relaxed) as u64,
169 )
170 });
171 if alloc_count == 0 {
172 return None;
173 }
174 let live = self.refresh_peak();
175 Some(MemStats {
176 live_bytes: live as u64,
177 peak_bytes: self.peak.load(Relaxed) as u64,
178 alloc_count,
179 free_count,
180 })
181 }
182}
183
184impl Default for Counters {
185 fn default() -> Self {
186 Self::new()
187 }
188}
189
190const fn shard_of(stack_addr: usize) -> usize {
197 const GOLDEN: u64 = 0x9E37_79B9_7F4A_7C15;
198 let key = (stack_addr >> STACK_SHIFT) as u64;
199 (key.wrapping_mul(GOLDEN) >> (u64::BITS - SHARD_BITS)) as usize
200}
201
202fn current_shard() -> usize {
206 let probe = core::mem::MaybeUninit::<u8>::uninit();
207 shard_of(core::hint::black_box(probe.as_ptr()) as usize)
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use std::vec::Vec;
214
215 #[test]
218 fn snapshot_is_none_until_something_allocates() {
219 let counters = Counters::new();
220 assert_eq!(counters.snapshot(), None);
221
222 counters.record_alloc(64);
223 assert!(counters.snapshot().is_some());
224 }
225
226 #[test]
229 fn alloc_count_matches_the_snapshot() {
230 let counters = Counters::new();
231 assert_eq!(counters.alloc_count(), None);
232
233 counters.record_alloc(64);
234 counters.record_alloc(32);
235 counters.record_free(64);
236 let stats = counters.snapshot().expect("block has seen allocations");
237 assert_eq!(counters.alloc_count(), Some(stats.alloc_count));
238 assert_eq!(counters.alloc_count(), Some(2));
239 }
240
241 #[test]
242 fn alloc_and_free_balance_back_to_zero() {
243 let counters = Counters::new();
244 counters.record_alloc(1024);
245 counters.record_alloc(512);
246 counters.record_free(1024);
247 counters.record_free(512);
248
249 let stats = counters.snapshot().expect("block has seen allocations");
250 assert_eq!(stats.live_bytes, 0);
251 assert_eq!(stats.alloc_count, 2);
252 assert_eq!(stats.free_count, 2);
253 }
254
255 #[test]
259 fn peak_holds_the_high_water_mark() {
260 let counters = Counters::new();
261 counters.record_alloc(1000);
262 counters.record_alloc(500);
263 assert_eq!(counters.snapshot().unwrap().live_bytes, 1500);
264
265 counters.record_free(1200);
266 let stats = counters.snapshot().expect("block has seen allocations");
267 assert_eq!(stats.live_bytes, 300);
268 assert_eq!(stats.peak_bytes, 1500);
269 }
270
271 #[test]
274 fn a_large_allocation_refreshes_the_peak_where_it_happens() {
275 let counters = Counters::new();
276 counters.record_alloc(PEAK_SAMPLE_BYTES);
277 counters.record_free(PEAK_SAMPLE_BYTES);
278
279 let stats = counters.snapshot().expect("block has seen allocations");
280 assert_eq!(stats.live_bytes, 0);
281 assert_eq!(stats.peak_bytes as usize, PEAK_SAMPLE_BYTES);
282 }
283
284 #[test]
285 fn realloc_moves_live_bytes_by_the_delta_only() {
286 let counters = Counters::new();
287 counters.record_alloc(100);
288
289 counters.record_realloc(100, 400);
290 assert_eq!(counters.snapshot().unwrap().live_bytes, 400);
291
292 counters.record_realloc(400, 250);
293 let stats = counters.snapshot().unwrap();
294 assert_eq!(stats.live_bytes, 250);
295 assert_eq!(stats.peak_bytes, 400);
296 assert_eq!(stats.alloc_count, 1);
298 assert_eq!(stats.free_count, 0);
299 }
300
301 #[test]
307 fn threads_spread_across_the_table_at_every_plausible_stack_stride() {
308 const KIB: usize = 1024;
309 for stride in [64 * KIB, 512 * KIB, 1 << 20, 2 << 20, 8 << 20] {
310 let shards: std::collections::BTreeSet<usize> = (0..SHARDS)
311 .map(|t| shard_of(0x7000_0000_0000 + t * stride))
312 .collect();
313 assert!(
314 shards.len() >= SHARDS / 2,
315 "{SHARDS} stacks {stride} bytes apart used only {} of {SHARDS} shards",
316 shards.len()
317 );
318 }
319 }
320
321 #[test]
324 fn one_stack_keeps_its_shard_as_it_grows() {
325 let base = 0x7000_0000_0000usize;
326 for depth in [0, 1, 64, 4096, (1 << STACK_SHIFT) - 1] {
327 assert_eq!(shard_of(base), shard_of(base + depth));
328 }
329 }
330
331 #[test]
332 fn every_address_maps_into_the_shard_table() {
333 for addr in [0usize, 1, usize::MAX, 0x7fff_ffff_ffff, 1 << 47] {
334 assert!(shard_of(addr) < SHARDS);
335 }
336 }
337
338 #[test]
342 fn concurrent_threads_sum_to_the_exact_total() {
343 use std::sync::Arc;
344 use std::thread;
345
346 const THREADS: usize = 8;
347 const PER_THREAD: usize = 4096;
348 const SIZE: usize = 128;
349
350 let counters = Arc::new(Counters::new());
351 let handles: Vec<_> = (0..THREADS)
352 .map(|_| {
353 let counters = Arc::clone(&counters);
354 thread::spawn(move || {
355 for _ in 0..PER_THREAD {
356 counters.record_alloc(SIZE);
357 }
358 })
359 })
360 .collect();
361 for h in handles {
362 h.join().expect("counting thread");
363 }
364
365 let stats = counters.snapshot().expect("threads allocated");
366 assert_eq!(stats.alloc_count as usize, THREADS * PER_THREAD);
367 assert_eq!(stats.live_bytes as usize, THREADS * PER_THREAD * SIZE);
368 assert!(
369 counters.touched_shards() > 1,
370 "every thread landed on one shard, which is the contention sharding removes"
371 );
372
373 let handles: Vec<_> = (0..THREADS)
376 .map(|_| {
377 let counters = Arc::clone(&counters);
378 thread::spawn(move || {
379 for _ in 0..PER_THREAD {
380 counters.record_free(SIZE);
381 }
382 })
383 })
384 .collect();
385 for h in handles {
386 h.join().expect("freeing thread");
387 }
388
389 let stats = counters.snapshot().expect("threads allocated");
390 assert_eq!(stats.live_bytes, 0);
391 assert_eq!(stats.free_count as usize, THREADS * PER_THREAD);
392 assert_eq!(stats.peak_bytes as usize, THREADS * PER_THREAD * SIZE);
393 }
394}