use std::cell::{Cell, OnceCell};
use std::marker::PhantomData;
use std::sync::atomic::{self, AtomicI64, AtomicU64};
use std::sync::{LazyLock, Mutex};
use crate::ERR_POISONED_LOCK;
#[derive(Debug)]
struct PerThreadCounters {
bytes: AtomicU64,
count: AtomicU64,
outstanding: AtomicI64,
watermark: AtomicI64,
}
impl PerThreadCounters {
#[inline]
const fn new() -> Self {
Self {
bytes: AtomicU64::new(0),
count: AtomicU64::new(0),
outstanding: AtomicI64::new(0),
watermark: AtomicI64::new(0),
}
}
#[inline]
fn bytes(&self) -> u64 {
self.bytes.load(atomic::Ordering::Relaxed)
}
#[inline]
fn count(&self) -> u64 {
self.count.load(atomic::Ordering::Relaxed)
}
fn outstanding(&self) -> i64 {
self.outstanding.load(atomic::Ordering::Relaxed)
}
fn watermark(&self) -> i64 {
self.watermark.load(atomic::Ordering::Relaxed)
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct ThreadCounters {
counters: &'static PerThreadCounters,
_single_threaded: PhantomData<*const ()>,
}
impl ThreadCounters {
fn new(counters: &'static PerThreadCounters) -> Self {
Self {
counters,
_single_threaded: PhantomData,
}
}
#[inline]
fn register_allocation(self, bytes: u64) {
self.add_to_totals(bytes);
self.raise_outstanding(as_delta(bytes));
}
fn register_deallocation(self, bytes: u64) {
self.shift_outstanding(as_delta(bytes).wrapping_neg());
}
fn register_reallocation(self, old_bytes: u64, new_bytes: u64) {
self.add_to_totals(new_bytes);
self.raise_outstanding(as_delta(new_bytes).wrapping_sub(as_delta(old_bytes)));
}
fn add_to_totals(self, bytes: u64) {
let total = self.counters.bytes().wrapping_add(bytes);
self.counters.bytes.store(total, atomic::Ordering::Relaxed);
let count = self.counters.count().wrapping_add(1);
self.counters.count.store(count, atomic::Ordering::Relaxed);
}
fn raise_outstanding(self, delta: i64) {
let outstanding = self.shift_outstanding(delta);
let watermark = self.counters.watermark();
self.counters
.watermark
.store(outstanding.max(watermark), atomic::Ordering::Relaxed);
}
fn shift_outstanding(self, delta: i64) -> i64 {
let outstanding = self.counters.outstanding().wrapping_add(delta);
self.counters
.outstanding
.store(outstanding, atomic::Ordering::Relaxed);
outstanding
}
pub(crate) fn bytes(self) -> u64 {
self.counters.bytes()
}
pub(crate) fn count(self) -> u64 {
self.counters.count()
}
pub(crate) fn outstanding(self) -> i64 {
self.counters.outstanding()
}
pub(crate) fn watermark(self) -> i64 {
self.counters.watermark()
}
pub(crate) fn set_watermark(self, value: i64) {
self.counters
.watermark
.store(value, atomic::Ordering::Relaxed);
}
}
fn as_delta(bytes: u64) -> i64 {
i64::try_from(bytes)
.expect("a single allocation cannot exceed isize::MAX bytes, which `Layout` guarantees")
}
static REGISTRY: LazyLock<Mutex<Vec<&'static PerThreadCounters>>> =
LazyLock::new(|| Mutex::new(Vec::new()));
thread_local! {
static TLS_COUNTERS: OnceCell<&'static PerThreadCounters> = const { OnceCell::new() };
static TLS_INIT_GUARD: Cell<bool> = const { Cell::new(false) };
}
#[inline]
pub(crate) fn get_or_init_thread_counters() -> ThreadCounters {
ThreadCounters::new(TLS_COUNTERS.with(|cell| {
if let Some(counters) = cell.get() {
return *counters;
}
TLS_INIT_GUARD.set(true);
let counters: &'static PerThreadCounters = Box::leak(Box::new(PerThreadCounters::new()));
REGISTRY.lock().expect(ERR_POISONED_LOCK).push(counters);
_ = cell.set(counters);
TLS_INIT_GUARD.set(false);
counters
}))
}
fn existing_thread_counters() -> Option<ThreadCounters> {
TLS_COUNTERS.with(|cell| cell.get().map(|counters| ThreadCounters::new(counters)))
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))] pub(crate) fn thread_has_counters() -> bool {
existing_thread_counters().is_some()
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct AllocationTotals {
pub(crate) bytes: u64,
pub(crate) count: u64,
}
impl AllocationTotals {
#[inline]
const fn zero() -> Self {
Self { bytes: 0, count: 0 }
}
}
#[inline]
pub(crate) fn allocation_totals() -> AllocationTotals {
let registry = REGISTRY.lock().expect(ERR_POISONED_LOCK);
let mut totals = AllocationTotals::zero();
for counters in registry.iter() {
totals.bytes = totals.bytes.wrapping_add(counters.bytes());
totals.count = totals.count.wrapping_add(counters.count());
}
totals
}
fn thread_counters_for_tracking() -> Option<ThreadCounters> {
if let Some(counters) = existing_thread_counters() {
return Some(counters);
}
if TLS_INIT_GUARD.get() {
return None;
}
Some(get_or_init_thread_counters())
}
pub(crate) fn track_allocation(size: usize) {
let size: u64 = size.try_into().expect("usize always fits into u64");
if let Some(counters) = thread_counters_for_tracking() {
counters.register_allocation(size);
}
}
pub(crate) fn track_reallocation(old_size: usize, new_size: usize) {
let old_size: u64 = old_size.try_into().expect("usize always fits into u64");
let new_size: u64 = new_size.try_into().expect("usize always fits into u64");
if let Some(counters) = thread_counters_for_tracking() {
counters.register_reallocation(old_size, new_size);
}
}
pub(crate) fn track_deallocation(size: usize) {
let size: u64 = size.try_into().expect("usize always fits into u64");
if let Some(counters) = existing_thread_counters() {
counters.register_deallocation(size);
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))] pub(crate) fn register_fake_allocation(bytes: u64, count: u64) {
let counters = get_or_init_thread_counters();
for index in 0..count {
counters.register_allocation(if index == 0 { bytes } else { 0 });
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))] pub(crate) fn register_fake_deallocation(bytes: u64) {
get_or_init_thread_counters().register_deallocation(bytes);
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::sync::{Arc, Barrier};
use std::{iter, panic, thread};
use testing::with_watchdog;
use super::*;
static_assertions::assert_impl_all!(PerThreadCounters: Send, Sync);
static_assertions::assert_not_impl_any!(ThreadCounters: Send, Sync);
macro_rules! detached_counters {
() => {{
static COUNTERS: PerThreadCounters = PerThreadCounters::new();
ThreadCounters::new(&COUNTERS)
}};
}
#[test]
fn outstanding_follows_allocations_and_deallocations() {
let counters = detached_counters!();
assert_eq!(counters.outstanding(), 0);
counters.register_allocation(100);
counters.register_allocation(50);
assert_eq!(counters.outstanding(), 150);
counters.register_deallocation(100);
assert_eq!(counters.outstanding(), 50);
assert_eq!(counters.bytes(), 150);
assert_eq!(counters.count(), 2);
}
#[test]
fn outstanding_goes_negative_when_freeing_untracked_memory() {
let counters = detached_counters!();
counters.register_deallocation(100);
assert_eq!(counters.outstanding(), -100);
assert_eq!(counters.watermark(), 0);
}
#[test]
fn watermark_holds_the_high_water_mark() {
let counters = detached_counters!();
counters.register_allocation(100);
counters.register_allocation(50);
assert_eq!(counters.watermark(), 150);
counters.register_deallocation(150);
counters.register_allocation(20);
assert_eq!(counters.outstanding(), 20);
assert_eq!(counters.watermark(), 150);
}
#[test]
fn reallocation_adjusts_outstanding_by_size_difference() {
const INITIAL: u64 = 100;
const GROWN: u64 = 300;
const SHRUNK: u64 = 80;
let counters = detached_counters!();
counters.register_allocation(INITIAL);
counters.register_reallocation(INITIAL, GROWN);
assert_eq!(counters.outstanding(), i64::try_from(GROWN).unwrap());
assert_eq!(counters.watermark(), i64::try_from(GROWN).unwrap());
counters.register_reallocation(GROWN, SHRUNK);
assert_eq!(counters.outstanding(), i64::try_from(SHRUNK).unwrap());
assert_eq!(counters.watermark(), i64::try_from(GROWN).unwrap());
assert_eq!(counters.bytes(), INITIAL + GROWN + SHRUNK);
assert_eq!(counters.count(), 3);
}
#[test]
fn set_watermark_overwrites_the_high_water_mark() {
let counters = detached_counters!();
counters.register_allocation(100);
counters.set_watermark(40);
assert_eq!(counters.watermark(), 40);
}
#[test]
fn concurrent_threads_register_and_totals_reflect_all() {
const THREADS: u64 = 4;
const BYTES_PER_THREAD: u64 = 100;
const COUNT_PER_THREAD: u64 = 10;
let baseline = allocation_totals();
let handles: Vec<_> = iter::repeat_with(|| {
thread::spawn(move || {
register_fake_allocation(BYTES_PER_THREAD, COUNT_PER_THREAD);
})
})
.take(usize::try_from(THREADS).unwrap())
.collect();
for handle in handles {
handle.join().unwrap();
}
let final_totals = allocation_totals();
let bytes_delta = final_totals.bytes.wrapping_sub(baseline.bytes);
let count_delta = final_totals.count.wrapping_sub(baseline.count);
assert!(bytes_delta >= THREADS * BYTES_PER_THREAD);
assert!(count_delta >= THREADS * COUNT_PER_THREAD);
}
#[test]
fn concurrent_register_and_read_totals() {
const WRITER_THREADS: u64 = 4;
const ALLOCS_PER_WRITER: u64 = 10;
const BYTES_PER_ALLOC: u64 = 50;
const READS: usize = 20;
with_watchdog(|| {
let baseline = allocation_totals();
let ready = Arc::new(Barrier::new(usize::try_from(WRITER_THREADS).unwrap() + 1));
let writers: Vec<_> = iter::repeat_with(|| {
let ready = Arc::clone(&ready);
thread::spawn(move || {
let registered = panic::catch_unwind(|| {
register_fake_allocation(BYTES_PER_ALLOC, 1);
});
ready.wait();
registered.unwrap_or_else(|payload| panic::resume_unwind(payload));
for _ in 1..ALLOCS_PER_WRITER {
register_fake_allocation(BYTES_PER_ALLOC, 1);
}
})
})
.take(usize::try_from(WRITER_THREADS).unwrap())
.collect();
ready.wait();
let mut previous = baseline;
for _ in 0..READS {
let totals = allocation_totals();
assert!(totals.bytes >= previous.bytes && totals.count >= previous.count);
previous = totals;
}
for handle in writers {
handle.join().unwrap();
}
let final_totals = allocation_totals();
let bytes_delta = final_totals.bytes.wrapping_sub(baseline.bytes);
assert!(bytes_delta >= WRITER_THREADS * ALLOCS_PER_WRITER * BYTES_PER_ALLOC);
assert!(final_totals.bytes >= previous.bytes);
});
}
}