use core::alloc::{GlobalAlloc, Layout};
use std::alloc::System;
use std::hint::black_box;
use std::sync::{Arc, Barrier};
use std::thread;
use mnemosyne::counting::{AllocationDelta, CountingAllocator, measure};
#[cfg(miri)]
#[global_allocator]
static ALLOCATOR: CountingAllocator<System> = CountingAllocator::new(System);
#[cfg(not(miri))]
#[global_allocator]
static ALLOCATOR: CountingAllocator<mnemosyne::Mnemosyne> =
CountingAllocator::new(mnemosyne::Mnemosyne);
fn warm() {
#[cfg(not(miri))]
mnemosyne::warm_current_thread();
}
struct Unsatisfiable;
unsafe impl GlobalAlloc for Unsatisfiable {
unsafe fn alloc(&self, _layout: Layout) -> *mut u8 {
core::ptr::null_mut()
}
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {}
}
struct RefusesRealloc;
unsafe impl GlobalAlloc for RefusesRealloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
unsafe { System.alloc_zeroed(layout) }
}
unsafe fn realloc(&self, _ptr: *mut u8, _layout: Layout, _new_size: usize) -> *mut u8 {
core::ptr::null_mut()
}
}
#[test]
fn window_without_heap_traffic_reads_empty() {
warm();
let (sum, delta) = measure(|| black_box(3_u64) + black_box(4_u64));
assert_eq!(sum, 7);
assert_eq!(delta, AllocationDelta::default());
}
#[test]
fn one_extra_allocation_reads_exactly_one() {
warm();
let (block, delta) = measure(|| black_box(Box::new([0_u8; 24])));
assert_eq!(block.len(), 24);
assert_eq!(
delta,
AllocationDelta {
allocations: 1,
bytes_allocated: 24,
..AllocationDelta::default()
}
);
}
#[test]
fn zeroed_allocation_is_counted() {
warm();
let (zeros, delta) = measure(|| black_box(vec![0_u8; 32]));
assert_eq!(zeros.len(), 32);
assert_eq!(delta.allocations, 1);
assert_eq!(delta.bytes_allocated, 32);
}
#[test]
fn growing_realloc_is_counted_and_retained_bytes_stay_exact() {
warm();
let mut buffer = Vec::<u8>::with_capacity(16);
let ((), delta) = measure(|| {
buffer.reserve_exact(64);
black_box(&mut buffer);
});
assert_eq!(buffer.capacity(), 64);
assert_eq!(
delta,
AllocationDelta {
reallocations: 1,
bytes_allocated: 48,
..AllocationDelta::default()
}
);
assert_eq!(delta.bytes_retained(), 48);
}
#[test]
fn shrinking_realloc_is_counted_as_released_bytes() {
warm();
let mut buffer = Vec::<u8>::with_capacity(64);
let ((), delta) = measure(|| {
buffer.shrink_to(16);
black_box(&mut buffer);
});
assert_eq!(buffer.capacity(), 16);
assert_eq!(
delta,
AllocationDelta {
reallocations: 1,
bytes_deallocated: 48,
..AllocationDelta::default()
}
);
assert_eq!(delta.bytes_retained(), -48);
}
#[test]
fn deallocation_is_counted() {
warm();
let block = black_box(Box::new([0_u8; 40]));
let ((), delta) = measure(|| drop(block));
assert_eq!(
delta,
AllocationDelta {
deallocations: 1,
bytes_deallocated: 40,
..AllocationDelta::default()
}
);
assert_eq!(delta.bytes_retained(), -40);
}
#[test]
fn balanced_window_retains_nothing() {
warm();
let ((), delta) = measure(|| drop(black_box(Vec::<u8>::with_capacity(128))));
assert_eq!(delta.allocations, 1);
assert_eq!(delta.deallocations, 1);
assert_eq!(delta.bytes_allocated, 128);
assert_eq!(delta.bytes_deallocated, 128);
assert_eq!(delta.bytes_retained(), 0);
}
#[test]
fn allocations_on_another_thread_are_not_counted() {
warm();
let start = Arc::new(Barrier::new(2));
let done = Arc::new(Barrier::new(2));
let worker = {
let (start, done) = (Arc::clone(&start), Arc::clone(&done));
thread::spawn(move || {
start.wait();
let ((), own) = measure(|| drop(black_box(Vec::<u8>::with_capacity(1024))));
done.wait();
own
})
};
let ((), observed) = measure(|| {
start.wait();
done.wait();
});
let worker_delta = worker.join().expect("invariant: the worker does not panic");
assert_eq!(observed, AllocationDelta::default());
assert_eq!(
worker_delta,
AllocationDelta {
allocations: 1,
deallocations: 1,
bytes_allocated: 1024,
bytes_deallocated: 1024,
..AllocationDelta::default()
}
);
}
#[test]
fn nested_windows_compose() {
warm();
let (inner, outer) = measure(|| {
let first = black_box(Box::new(1_u64));
let (second, inner) = measure(|| black_box(Box::new(2_u64)));
drop((first, second));
inner
});
assert_eq!(inner.allocations, 1);
assert_eq!(inner.deallocations, 0);
assert_eq!(outer.allocations, 2);
assert_eq!(outer.deallocations, 2);
}
#[test]
fn unsatisfied_allocations_are_not_counted() {
let layout = Layout::new::<[u8; 16]>();
let counting = CountingAllocator::new(Unsatisfiable);
let (pointers, delta) = measure(|| {
let fresh = unsafe { counting.alloc(layout) };
let zeroed = unsafe { counting.alloc_zeroed(layout) };
[fresh, zeroed]
});
assert!(pointers.iter().all(|pointer| pointer.is_null()));
assert_eq!(delta, AllocationDelta::default());
}
#[test]
fn refused_realloc_is_not_counted_and_leaves_the_block_valid() {
let layout = Layout::new::<[u8; 16]>();
let counting = CountingAllocator::new(RefusesRealloc);
let block = unsafe { counting.alloc(layout) };
assert!(!block.is_null());
let (grown, delta) = measure(|| {
unsafe { counting.realloc(block, layout, 32) }
});
unsafe { counting.dealloc(block, layout) };
assert!(grown.is_null());
assert_eq!(delta, AllocationDelta::default());
}