use std::hint::black_box;
use std::thread;
use alloc_tracker::{Allocator, Session};
#[global_allocator]
static ALLOCATOR: Allocator<std::alloc::System> = Allocator::system();
#[test]
#[cfg_attr(miri, ignore)] fn no_span_is_empty_session() {
let session = Session::new();
let _op = session.operation("test_no_span");
assert!(session.is_empty());
}
#[test]
#[cfg_attr(miri, ignore)] fn span_with_no_allocation_is_not_empty_session() {
let session = Session::new();
{
let op = session.operation("test_no_allocation");
drop(op.measure_process());
}
assert!(
!session.is_empty(),
"Session should not be empty after creating a span"
);
}
#[test]
#[cfg_attr(miri, ignore)] fn single_thread_allocations() {
const BYTES_PER_ITERATION: usize = 100;
const TEST_ITERATIONS: usize = 5;
let session = Session::new();
let process_total = {
let process_op = session.operation("process_single_thread");
for i in 1..=TEST_ITERATIONS {
let _span = process_op.measure_process();
let _data = vec![0_u8; i * BYTES_PER_ITERATION];
black_box(&_data);
}
process_op.total_bytes_allocated()
};
let thread_total = {
let thread_op = session.operation("thread_single_thread");
for i in 1..=TEST_ITERATIONS {
let _span = thread_op.measure_thread();
let _data = vec![0_u8; i * BYTES_PER_ITERATION];
black_box(&_data);
}
thread_op.total_bytes_allocated()
};
assert!(process_total > 0);
assert!(thread_total > 0);
assert!(process_total >= thread_total);
}
#[test]
#[cfg_attr(miri, ignore)] fn multithreaded_allocations_show_span_differences() {
const NUM_WORKER_THREADS: u32 = 4;
const ALLOCATIONS_PER_THREAD: u32 = 50;
const MAIN_THREAD_ALLOCATIONS: u32 = 10;
const TEST_ITERATIONS: usize = 3;
let session = Session::new();
let spawn_workers = || {
let handles: Vec<_> = (0..NUM_WORKER_THREADS)
.map(|thread_id| {
thread::spawn(move || {
for i in 0..ALLOCATIONS_PER_THREAD {
let size = ((thread_id + 1) * 100 + i) as usize;
let data = vec![42_u8; size];
black_box(data);
}
})
})
.collect();
for i in 0..MAIN_THREAD_ALLOCATIONS {
#[expect(
clippy::cast_possible_truncation,
reason = "small test values will not truncate"
)]
let data = vec![i as u8; 100];
black_box(data);
}
for handle in handles {
handle.join().unwrap();
}
};
let process_total = {
let process_op = session.operation("process_multithreaded");
for _ in 0..TEST_ITERATIONS {
let _span = process_op.measure_process();
spawn_workers();
}
process_op.total_bytes_allocated()
};
let thread_total = {
let thread_op = session.operation("thread_multithreaded");
for _ in 0..TEST_ITERATIONS {
let _span = thread_op.measure_thread();
spawn_workers();
}
thread_op.total_bytes_allocated()
};
assert!(process_total > 0);
assert!(thread_total > 0);
assert!(
process_total > thread_total * 2,
"Process span should capture much more allocation than thread span in multithreaded context. Process: {process_total}, Thread: {thread_total}"
);
}
#[test]
#[cfg_attr(miri, ignore)] fn mixed_span_types_in_multithreaded_context() {
const ITERATIONS: usize = 3;
let session = Session::new();
let mixed_op = session.operation("mixed_multithreaded");
for iteration in 1..=ITERATIONS {
if iteration % 2 == 0 {
let _span = mixed_op.measure_process();
let handle = thread::spawn(|| {
let data = vec![0_u8; 500];
black_box(data);
});
let data = vec![0_u8; 100];
black_box(data);
handle.join().unwrap();
} else {
let _span = mixed_op.measure_thread();
let handle = thread::spawn(|| {
let data = vec![0_u8; 500];
black_box(data);
});
let data = vec![0_u8; 100];
black_box(data);
handle.join().unwrap();
}
}
let total = mixed_op.total_bytes_allocated();
assert!(total > 0);
}
#[test]
#[cfg_attr(miri, ignore)] fn report_is_empty_matches_session_is_empty() {
let session = Session::new();
let report = session.to_report();
assert_eq!(session.is_empty(), report.is_empty());
assert!(session.is_empty());
assert!(report.is_empty());
let _operation = session.operation("test");
let report = session.to_report();
assert_eq!(session.is_empty(), report.is_empty());
assert!(session.is_empty());
assert!(report.is_empty());
{
let operation = session.operation("test_with_spans");
let _span = operation.measure_process();
}
let report = session.to_report();
assert_eq!(session.is_empty(), report.is_empty());
assert!(!session.is_empty());
assert!(!report.is_empty());
}
#[test]
#[cfg_attr(miri, ignore)] fn report_mean_with_known_allocations() {
const NUM_ITERATIONS: u64 = 5;
let session = Session::new();
{
let operation = session.operation("known_allocation");
for _ in 0..NUM_ITERATIONS {
let _span = operation.measure_thread();
let boxed_value = Box::new(42_u64);
black_box(boxed_value); }
}
let report = session.to_report();
let operations: Vec<_> = report.operations().collect();
assert_eq!(operations.len(), 1);
let (_name, op) = operations.first().unwrap();
assert_eq!(op.total_iterations(), NUM_ITERATIONS);
let total_bytes = op.total_bytes_allocated();
assert!(total_bytes > 0);
assert!(total_bytes >= NUM_ITERATIONS * 8);
let mean_bytes = op.mean();
assert!(mean_bytes > 0);
#[expect(
clippy::integer_division,
reason = "Integer division is intended for mean calculation"
)]
let expected_mean = total_bytes / NUM_ITERATIONS;
assert_eq!(mean_bytes, expected_mean);
assert!(mean_bytes >= 8);
}
#[test]
#[cfg_attr(miri, ignore)] fn process_report_includes_allocations_from_multiple_threads() {
const THREAD_A_ALLOCS: usize = 40;
const THREAD_B_ALLOCS: usize = 25;
const SIZE_A: usize = 128; const SIZE_B: usize = 256;
let session = Session::new();
{
let op = session.operation("two_thread_process");
let _span = op.measure_process();
let handle_a = thread::spawn(|| {
let mut total = 0_usize;
for _ in 0..THREAD_A_ALLOCS {
let v = vec![0_u8; SIZE_A];
total += v.len();
black_box(&v);
}
total
});
let handle_b = thread::spawn(|| {
let mut total = 0_usize;
for _ in 0..THREAD_B_ALLOCS {
let v = vec![1_u8; SIZE_B];
black_box(&v);
total += v.len();
}
total
});
let main_alloc = vec![2_u8; 64];
black_box(&main_alloc);
let a_bytes = handle_a.join().unwrap();
let b_bytes = handle_b.join().unwrap();
assert_eq!(a_bytes, THREAD_A_ALLOCS * SIZE_A);
assert_eq!(b_bytes, THREAD_B_ALLOCS * SIZE_B);
}
let report = session.to_report();
let operations: Vec<_> = report.operations().collect();
assert_eq!(
operations.len(),
1,
"expected exactly one operation in report"
);
let (_name, op) = operations.first().unwrap();
let total = op.total_bytes_allocated();
let min_expected = (THREAD_A_ALLOCS * SIZE_A + THREAD_B_ALLOCS * SIZE_B) as u64;
assert!(
total >= min_expected,
"total {total} < expected minimum {min_expected}"
);
assert!(total >= (THREAD_A_ALLOCS * SIZE_A) as u64);
assert!(total >= (THREAD_B_ALLOCS * SIZE_B) as u64);
}