#[cfg(any(feature = "alloc-count", feature = "fault-alloc"))]
use std::alloc::{GlobalAlloc, Layout, System};
#[cfg(feature = "alloc-count")]
pub struct CountingAlloc {
inner: System,
}
#[cfg(feature = "alloc-count")]
mod counters {
use std::sync::atomic::AtomicU64;
pub(super) static ALLOCS: AtomicU64 = AtomicU64::new(0);
pub(super) static REALLOCS: AtomicU64 = AtomicU64::new(0);
pub(super) static DEALLOCS: AtomicU64 = AtomicU64::new(0);
}
#[cfg(feature = "alloc-count")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AllocSnapshot {
pub allocs: u64,
pub reallocs: u64,
pub deallocs: u64,
}
#[cfg(feature = "alloc-count")]
impl AllocSnapshot {
#[must_use]
pub fn delta_allocs(self, later: AllocSnapshot) -> u64 {
later.allocs.saturating_sub(self.allocs)
}
#[must_use]
pub fn delta_reallocs(self, later: AllocSnapshot) -> u64 {
later.reallocs.saturating_sub(self.reallocs)
}
#[must_use]
pub fn delta_allocating(self, later: AllocSnapshot) -> u64 {
self.delta_allocs(later)
.saturating_add(self.delta_reallocs(later))
}
}
#[cfg(feature = "alloc-count")]
impl CountingAlloc {
#[must_use]
pub const fn new() -> Self {
Self { inner: System }
}
#[must_use]
pub fn snapshot() -> AllocSnapshot {
use std::sync::atomic::Ordering::Relaxed;
AllocSnapshot {
allocs: counters::ALLOCS.load(Relaxed),
reallocs: counters::REALLOCS.load(Relaxed),
deallocs: counters::DEALLOCS.load(Relaxed),
}
}
pub fn scope<R>(body: impl FnOnce() -> R) -> (R, AllocSnapshot) {
let before = Self::snapshot();
let result = body();
let after = Self::snapshot();
let delta = AllocSnapshot {
allocs: before.delta_allocs(after),
reallocs: before.delta_reallocs(after),
deallocs: after.deallocs.saturating_sub(before.deallocs),
};
(result, delta)
}
}
#[cfg(feature = "alloc-count")]
impl Default for CountingAlloc {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "alloc-count")]
unsafe impl GlobalAlloc for CountingAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
counters::ALLOCS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
unsafe { self.inner.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
counters::DEALLOCS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
unsafe { self.inner.dealloc(ptr, layout) }
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
counters::ALLOCS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
unsafe { self.inner.alloc_zeroed(layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
counters::REALLOCS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
unsafe { self.inner.realloc(ptr, layout, new_size) }
}
}
#[cfg(all(test, feature = "alloc-count"))]
mod alloc_delta_tests {
use super::AllocSnapshot;
fn snap(allocs: u64, reallocs: u64, deallocs: u64) -> AllocSnapshot {
AllocSnapshot {
allocs,
reallocs,
deallocs,
}
}
#[test]
fn deltas_report_the_exact_inter_snapshot_difference() {
let before = snap(10, 4, 1);
let after = snap(17, 9, 3);
let mut failures: Vec<String> = Vec::new();
let allocs = before.delta_allocs(after);
if allocs != 7 {
failures.push(format!("delta_allocs must be 7, got {allocs}"));
}
let reallocs = before.delta_reallocs(after);
if reallocs != 5 {
failures.push(format!("delta_reallocs must be 5, got {reallocs}"));
}
let allocating = before.delta_allocating(after);
if allocating != 12 {
failures.push(format!("delta_allocating must be 7+5=12, got {allocating}"));
}
assert!(
failures.is_empty(),
"delta accounting mismatches: {failures:?}"
);
}
}
#[cfg(feature = "fault-alloc")]
pub struct FailingAlloc {
inner: System,
}
#[cfg(feature = "fault-alloc")]
mod fail_state {
use std::sync::atomic::{AtomicBool, AtomicU64};
pub(super) static ARMED: AtomicBool = AtomicBool::new(false);
pub(super) static SEEN: AtomicU64 = AtomicU64::new(0);
pub(super) static FAIL_AT: AtomicU64 = AtomicU64::new(u64::MAX);
}
#[cfg(feature = "fault-alloc")]
impl FailingAlloc {
#[must_use]
pub const fn new() -> Self {
Self { inner: System }
}
pub fn fail_after(k: u64) {
use std::sync::atomic::Ordering::SeqCst;
fail_state::SEEN.store(0, SeqCst);
fail_state::FAIL_AT.store(k, SeqCst);
fail_state::ARMED.store(true, SeqCst);
}
pub fn disarm() {
use std::sync::atomic::Ordering::SeqCst;
fail_state::ARMED.store(false, SeqCst);
fail_state::FAIL_AT.store(u64::MAX, SeqCst);
}
fn should_fail(&self) -> bool {
use std::sync::atomic::Ordering::SeqCst;
if !fail_state::ARMED.load(SeqCst) {
return false;
}
let seen = fail_state::SEEN.fetch_add(1, SeqCst).saturating_add(1);
seen >= fail_state::FAIL_AT.load(SeqCst)
}
}
#[cfg(feature = "fault-alloc")]
impl Default for FailingAlloc {
fn default() -> Self {
Self::new()
}
}
#[cfg(all(test, feature = "fault-alloc"))]
mod failing_alloc_tests {
use super::FailingAlloc;
#[test]
fn arm_disarm_and_trip_point_are_exact() {
let alloc = FailingAlloc::new();
FailingAlloc::disarm();
assert!(
!alloc.should_fail(),
"a disarmed allocator must never signal failure"
);
FailingAlloc::fail_after(3);
assert!(
!alloc.should_fail(),
"1st post-arm allocation must succeed (seen 1 >= 3 is false)"
);
assert!(
!alloc.should_fail(),
"2nd post-arm allocation must succeed (seen 2 >= 3 is false)"
);
assert!(
alloc.should_fail(),
"3rd post-arm allocation must fail (seen 3 >= 3)"
);
assert!(
alloc.should_fail(),
"4th and later allocations must keep failing (seen 4 >= 3)"
);
FailingAlloc::disarm();
assert!(
!alloc.should_fail(),
"after disarm every allocation must succeed again"
);
}
}
#[cfg(feature = "fault-alloc")]
unsafe impl GlobalAlloc for FailingAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
if self.should_fail() {
return std::ptr::null_mut();
}
unsafe { self.inner.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { self.inner.dealloc(ptr, layout) }
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
if self.should_fail() {
return std::ptr::null_mut();
}
unsafe { self.inner.alloc_zeroed(layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
if self.should_fail() {
return std::ptr::null_mut();
}
unsafe { self.inner.realloc(ptr, layout, new_size) }
}
}