1#![cfg(not(target_arch = "wasm32"))]
3
4use std::{
5 alloc::{GlobalAlloc, Layout, System},
6 cell::Cell,
7 time::{Duration, Instant},
8};
9
10#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
17pub struct Measurement {
18 pub allocations: usize,
20 pub reallocations: usize,
22 pub net_bytes: i128,
24 pub peak_bytes: usize,
26 pub elapsed: Duration,
28}
29
30#[derive(Clone, Copy, Default)]
31struct State {
32 active: bool,
33 counts: Measurement,
34}
35thread_local! { static STATE: Cell<State> = const { Cell::new(State {
36 active: false,
37 counts: Measurement { allocations: 0, reallocations: 0, net_bytes: 0, peak_bytes: 0, elapsed: Duration::ZERO },
38}) }; }
39
40fn account(old: usize, new: usize, allocation: bool, reallocation: bool) {
41 let _ = STATE.try_with(|cell| {
42 let mut state = cell.get();
43 if state.active {
44 state.counts.net_bytes += new as i128 - old as i128;
45 state.counts.peak_bytes = state
46 .counts
47 .peak_bytes
48 .max(usize::try_from(state.counts.net_bytes).unwrap_or(0));
49 state.counts.allocations += usize::from(allocation);
50 state.counts.reallocations += usize::from(reallocation);
51 cell.set(state);
52 }
53 });
54}
55
56pub struct MeasuredAllocator<A = System> {
62 allocator: A,
63}
64impl<A> MeasuredAllocator<A> {
65 #[must_use]
67 pub const fn new(allocator: A) -> Self {
68 Self { allocator }
69 }
70}
71
72#[expect(
73 unsafe_code,
74 reason = "forward the GlobalAlloc contract to the caller-selected allocator"
75)]
76unsafe impl<A: GlobalAlloc> GlobalAlloc for MeasuredAllocator<A> {
77 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
78 let pointer = unsafe { self.allocator.alloc(layout) };
80 if !pointer.is_null() {
81 account(0, layout.size(), true, false);
82 }
83 pointer
84 }
85 unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
86 let pointer = unsafe { self.allocator.alloc_zeroed(layout) };
88 if !pointer.is_null() {
89 account(0, layout.size(), true, false);
90 }
91 pointer
92 }
93 unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
94 account(layout.size(), 0, false, false);
95 unsafe { self.allocator.dealloc(pointer, layout) };
97 }
98 unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, size: usize) -> *mut u8 {
99 let result = unsafe { self.allocator.realloc(pointer, layout, size) };
101 if !result.is_null() {
102 account(layout.size(), size, false, true);
103 }
104 result
105 }
106}
107
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
115pub struct AlreadyMeasuring;
116impl std::fmt::Display for AlreadyMeasuring {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 f.write_str("allocation measurement already active on this thread")
119 }
120}
121impl std::error::Error for AlreadyMeasuring {}
122
123struct Guard;
124impl Drop for Guard {
125 fn drop(&mut self) {
126 STATE.with(|cell| cell.set(State::default()));
127 }
128}
129
130pub fn measure(
138 iterations: usize,
139 mut operation: impl FnMut(),
140) -> Result<Measurement, AlreadyMeasuring> {
141 STATE.with(|cell| {
142 if cell.get().active {
143 return Err(AlreadyMeasuring);
144 }
145 cell.set(State {
146 active: true,
147 ..State::default()
148 });
149 Ok(())
150 })?;
151 let guard = Guard;
152 let started = Instant::now();
153 for _ in 0..iterations {
154 operation();
155 }
156 let elapsed = started.elapsed();
157 let mut counts = STATE.with(|cell| cell.get().counts);
158 drop(guard);
159 counts.elapsed = elapsed;
160 Ok(counts)
161}