use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
use std::sync::atomic::{AtomicBool, Ordering};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Allocations {
pub count: u64,
pub bytes: u64,
pub freed: u64,
}
impl Allocations {
#[must_use]
pub fn since(self, earlier: Self) -> Self {
Self {
count: self.count.saturating_sub(earlier.count),
bytes: self.bytes.saturating_sub(earlier.bytes),
freed: self.freed.saturating_sub(earlier.freed),
}
}
#[must_use]
pub fn retained(self) -> i64 {
i64::try_from(self.bytes).unwrap_or(i64::MAX)
- i64::try_from(self.freed).unwrap_or(i64::MAX)
}
}
impl std::ops::AddAssign for Allocations {
fn add_assign(&mut self, other: Self) {
self.count += other.count;
self.bytes += other.bytes;
self.freed += other.freed;
}
}
static INSTALLED: AtomicBool = AtomicBool::new(false);
struct Tally {
count: Cell<u64>,
bytes: Cell<u64>,
freed: Cell<u64>,
held: Cell<i64>,
bound: Cell<i64>,
exhausted: Cell<bool>,
wire: Cell<*const AtomicBool>,
}
#[derive(Clone, Copy)]
struct Charge {
held: i64,
bound: i64,
exhausted: bool,
wire: *const AtomicBool,
}
impl Tally {
const fn new() -> Self {
Self {
count: Cell::new(0),
bytes: Cell::new(0),
freed: Cell::new(0),
held: Cell::new(0),
bound: Cell::new(i64::MAX),
exhausted: Cell::new(false),
wire: Cell::new(std::ptr::null()),
}
}
fn charge(&self, bytes: usize) {
self.count.set(self.count.get() + 1);
self.bytes.set(self.bytes.get() + bytes as u64);
self.hold(i64::try_from(bytes).unwrap_or(i64::MAX));
}
fn credit(&self, bytes: usize) {
self.freed.set(self.freed.get() + bytes as u64);
self.hold(-i64::try_from(bytes).unwrap_or(i64::MAX));
}
fn hold(&self, bytes: i64) {
let held = self.held.get().saturating_add(bytes);
self.held.set(held);
if held > self.bound.get() && !self.exhausted.get() {
self.trip();
}
}
fn trip(&self) {
self.exhausted.set(true);
let wire = self.wire.get();
if !wire.is_null() {
unsafe { (*wire).store(true, Ordering::Relaxed) };
}
}
fn swap(&self, next: Charge) -> Charge {
let outer = Charge {
held: self.held.replace(next.held),
bound: self.bound.replace(next.bound),
exhausted: self.exhausted.replace(next.exhausted),
wire: self.wire.replace(next.wire),
};
if next.held > next.bound && !next.exhausted {
self.trip();
}
outer
}
}
thread_local! {
static TALLY: Tally = const { Tally::new() };
}
#[must_use]
pub fn allocations() -> Option<Allocations> {
if !INSTALLED.load(Ordering::Relaxed) {
return None;
}
TALLY
.try_with(|t| Allocations {
count: t.count.get(),
bytes: t.bytes.get(),
freed: t.freed.get(),
})
.ok()
}
fn charge(bytes: usize) {
let _ = TALLY.try_with(|t| t.charge(bytes));
}
fn credit(bytes: usize) {
let _ = TALLY.try_with(|t| t.credit(bytes));
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Account {
pub held: i64,
pub bound: Option<u64>,
pub exhausted: bool,
}
impl Account {
#[must_use]
pub const fn new(bound: Option<u64>) -> Self {
Self {
held: 0,
bound,
exhausted: false,
}
}
}
struct Charged<'a> {
account: &'a mut Account,
outer: Charge,
}
impl Drop for Charged<'_> {
fn drop(&mut self) {
let _ = TALLY.try_with(|t| {
let inner = t.swap(self.outer);
self.account.held = inner.held;
self.account.exhausted = inner.exhausted;
});
}
}
pub fn charged_to<R>(account: &mut Account, wire: Option<&AtomicBool>, f: impl FnOnce() -> R) -> R {
let next = Charge {
held: account.held,
bound: account
.bound
.map_or(i64::MAX, |b| i64::try_from(b).unwrap_or(i64::MAX)),
exhausted: account.exhausted,
wire: wire.map_or(std::ptr::null(), |w| w as *const AtomicBool),
};
let Ok(outer) = TALLY.try_with(|t| t.swap(next)) else {
return f();
};
let _scope = Charged { account, outer };
f()
}
#[must_use]
pub fn apart<R>(f: impl FnOnce() -> R) -> (R, i64) {
let mut account = Account::new(None);
let value = charged_to(&mut account, None, f);
(value, account.held)
}
pub fn charge_held(bytes: i64) {
let _ = TALLY.try_with(|t| t.hold(bytes));
}
#[must_use]
pub fn bound() -> Option<u64> {
if !INSTALLED.load(Ordering::Relaxed) {
return None;
}
TALLY
.try_with(|t| t.bound.get())
.ok()
.filter(|b| *b != i64::MAX)
.and_then(|b| u64::try_from(b).ok())
}
#[must_use]
pub fn exhausted() -> bool {
TALLY.try_with(|t| t.exhausted.get()).unwrap_or(false)
}
pub struct Counting;
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
INSTALLED.store(true, Ordering::Relaxed);
charge(layout.size());
unsafe { System.alloc(layout) }
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
INSTALLED.store(true, Ordering::Relaxed);
charge(layout.size());
unsafe { System.alloc_zeroed(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
credit(layout.size());
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
if new_size > layout.size() {
charge(new_size - layout.size());
} else {
credit(layout.size() - new_size);
}
unsafe { System.realloc(ptr, layout, new_size) }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_uncounted_binary_reads_as_unmeasured() {
let _ = vec![1u8; 64];
assert_eq!(allocations(), None);
}
#[test]
fn since_is_the_difference_and_never_underflows() {
let a = Allocations {
count: 3,
bytes: 100,
freed: 40,
};
let b = Allocations {
count: 5,
bytes: 160,
freed: 50,
};
assert_eq!(
b.since(a),
Allocations {
count: 2,
bytes: 60,
freed: 10
}
);
assert_eq!(a.since(b), Allocations::default());
}
}