use core::sync::atomic::{AtomicUsize, Ordering::Relaxed};
const SHARDS: usize = 16;
const SHARD_BITS: u32 = SHARDS.trailing_zeros();
const STACK_SHIFT: u32 = 16;
const PEAK_SAMPLE_ALLOCS: usize = 1024;
const PEAK_SAMPLE_BYTES: usize = 1 << 20;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct MemStats {
pub live_bytes: u64,
pub peak_bytes: u64,
pub alloc_count: u64,
pub free_count: u64,
}
#[repr(align(64))]
struct Shard {
live: AtomicUsize,
allocs: AtomicUsize,
frees: AtomicUsize,
}
impl Shard {
const fn new() -> Self {
Self {
live: AtomicUsize::new(0),
allocs: AtomicUsize::new(0),
frees: AtomicUsize::new(0),
}
}
}
pub(crate) struct Counters {
shards: [Shard; SHARDS],
peak: AtomicUsize,
}
impl Counters {
pub(crate) const fn new() -> Self {
Self {
shards: [const { Shard::new() }; SHARDS],
peak: AtomicUsize::new(0),
}
}
fn shard(&self) -> &Shard {
&self.shards[current_shard()]
}
pub(crate) fn record_alloc(&self, size: usize) {
let shard = self.shard();
shard.live.fetch_add(size, Relaxed);
let allocs = shard.allocs.fetch_add(1, Relaxed).wrapping_add(1);
if size >= PEAK_SAMPLE_BYTES || allocs.is_multiple_of(PEAK_SAMPLE_ALLOCS) {
self.refresh_peak();
}
}
pub(crate) fn record_free(&self, size: usize) {
let shard = self.shard();
shard.live.fetch_sub(size, Relaxed);
shard.frees.fetch_add(1, Relaxed);
}
pub(crate) fn record_realloc(&self, old_size: usize, new_size: usize) {
let shard = self.shard();
if new_size >= old_size {
let grew = new_size - old_size;
shard.live.fetch_add(grew, Relaxed);
if grew >= PEAK_SAMPLE_BYTES {
self.refresh_peak();
}
} else {
shard.live.fetch_sub(old_size - new_size, Relaxed);
}
}
fn live(&self) -> usize {
self.shards
.iter()
.fold(0usize, |sum, s| sum.wrapping_add(s.live.load(Relaxed)))
}
fn refresh_peak(&self) -> usize {
let live = self.live();
self.peak.fetch_max(live, Relaxed);
live
}
#[cfg(test)]
fn touched_shards(&self) -> usize {
self.shards
.iter()
.filter(|s| s.allocs.load(Relaxed) > 0)
.count()
}
pub(crate) fn alloc_count(&self) -> Option<u64> {
let count = self
.shards
.iter()
.fold(0u64, |sum, s| sum + s.allocs.load(Relaxed) as u64);
(count > 0).then_some(count)
}
pub(crate) fn snapshot(&self) -> Option<MemStats> {
let (alloc_count, free_count) = self.shards.iter().fold((0u64, 0u64), |(a, f), s| {
(
a + s.allocs.load(Relaxed) as u64,
f + s.frees.load(Relaxed) as u64,
)
});
if alloc_count == 0 {
return None;
}
let live = self.refresh_peak();
Some(MemStats {
live_bytes: live as u64,
peak_bytes: self.peak.load(Relaxed) as u64,
alloc_count,
free_count,
})
}
}
impl Default for Counters {
fn default() -> Self {
Self::new()
}
}
const fn shard_of(stack_addr: usize) -> usize {
const GOLDEN: u64 = 0x9E37_79B9_7F4A_7C15;
let key = (stack_addr >> STACK_SHIFT) as u64;
(key.wrapping_mul(GOLDEN) >> (u64::BITS - SHARD_BITS)) as usize
}
fn current_shard() -> usize {
let probe = core::mem::MaybeUninit::<u8>::uninit();
shard_of(core::hint::black_box(probe.as_ptr()) as usize)
}
#[cfg(test)]
mod tests {
use super::*;
use std::vec::Vec;
#[test]
fn snapshot_is_none_until_something_allocates() {
let counters = Counters::new();
assert_eq!(counters.snapshot(), None);
counters.record_alloc(64);
assert!(counters.snapshot().is_some());
}
#[test]
fn alloc_count_matches_the_snapshot() {
let counters = Counters::new();
assert_eq!(counters.alloc_count(), None);
counters.record_alloc(64);
counters.record_alloc(32);
counters.record_free(64);
let stats = counters.snapshot().expect("block has seen allocations");
assert_eq!(counters.alloc_count(), Some(stats.alloc_count));
assert_eq!(counters.alloc_count(), Some(2));
}
#[test]
fn alloc_and_free_balance_back_to_zero() {
let counters = Counters::new();
counters.record_alloc(1024);
counters.record_alloc(512);
counters.record_free(1024);
counters.record_free(512);
let stats = counters.snapshot().expect("block has seen allocations");
assert_eq!(stats.live_bytes, 0);
assert_eq!(stats.alloc_count, 2);
assert_eq!(stats.free_count, 2);
}
#[test]
fn peak_holds_the_high_water_mark() {
let counters = Counters::new();
counters.record_alloc(1000);
counters.record_alloc(500);
assert_eq!(counters.snapshot().unwrap().live_bytes, 1500);
counters.record_free(1200);
let stats = counters.snapshot().expect("block has seen allocations");
assert_eq!(stats.live_bytes, 300);
assert_eq!(stats.peak_bytes, 1500);
}
#[test]
fn a_large_allocation_refreshes_the_peak_where_it_happens() {
let counters = Counters::new();
counters.record_alloc(PEAK_SAMPLE_BYTES);
counters.record_free(PEAK_SAMPLE_BYTES);
let stats = counters.snapshot().expect("block has seen allocations");
assert_eq!(stats.live_bytes, 0);
assert_eq!(stats.peak_bytes as usize, PEAK_SAMPLE_BYTES);
}
#[test]
fn realloc_moves_live_bytes_by_the_delta_only() {
let counters = Counters::new();
counters.record_alloc(100);
counters.record_realloc(100, 400);
assert_eq!(counters.snapshot().unwrap().live_bytes, 400);
counters.record_realloc(400, 250);
let stats = counters.snapshot().unwrap();
assert_eq!(stats.live_bytes, 250);
assert_eq!(stats.peak_bytes, 400);
assert_eq!(stats.alloc_count, 1);
assert_eq!(stats.free_count, 0);
}
#[test]
fn threads_spread_across_the_table_at_every_plausible_stack_stride() {
const KIB: usize = 1024;
for stride in [64 * KIB, 512 * KIB, 1 << 20, 2 << 20, 8 << 20] {
let shards: std::collections::BTreeSet<usize> = (0..SHARDS)
.map(|t| shard_of(0x7000_0000_0000 + t * stride))
.collect();
assert!(
shards.len() >= SHARDS / 2,
"{SHARDS} stacks {stride} bytes apart used only {} of {SHARDS} shards",
shards.len()
);
}
}
#[test]
fn one_stack_keeps_its_shard_as_it_grows() {
let base = 0x7000_0000_0000usize;
for depth in [0, 1, 64, 4096, (1 << STACK_SHIFT) - 1] {
assert_eq!(shard_of(base), shard_of(base + depth));
}
}
#[test]
fn every_address_maps_into_the_shard_table() {
for addr in [0usize, 1, usize::MAX, 0x7fff_ffff_ffff, 1 << 47] {
assert!(shard_of(addr) < SHARDS);
}
}
#[test]
fn concurrent_threads_sum_to_the_exact_total() {
use std::sync::Arc;
use std::thread;
const THREADS: usize = 8;
const PER_THREAD: usize = 4096;
const SIZE: usize = 128;
let counters = Arc::new(Counters::new());
let handles: Vec<_> = (0..THREADS)
.map(|_| {
let counters = Arc::clone(&counters);
thread::spawn(move || {
for _ in 0..PER_THREAD {
counters.record_alloc(SIZE);
}
})
})
.collect();
for h in handles {
h.join().expect("counting thread");
}
let stats = counters.snapshot().expect("threads allocated");
assert_eq!(stats.alloc_count as usize, THREADS * PER_THREAD);
assert_eq!(stats.live_bytes as usize, THREADS * PER_THREAD * SIZE);
assert!(
counters.touched_shards() > 1,
"every thread landed on one shard, which is the contention sharding removes"
);
let handles: Vec<_> = (0..THREADS)
.map(|_| {
let counters = Arc::clone(&counters);
thread::spawn(move || {
for _ in 0..PER_THREAD {
counters.record_free(SIZE);
}
})
})
.collect();
for h in handles {
h.join().expect("freeing thread");
}
let stats = counters.snapshot().expect("threads allocated");
assert_eq!(stats.live_bytes, 0);
assert_eq!(stats.free_count as usize, THREADS * PER_THREAD);
assert_eq!(stats.peak_bytes as usize, THREADS * PER_THREAD * SIZE);
}
}