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