concinnity_core/memory/
tracking.rs1use core::alloc::{GlobalAlloc, Layout};
13
14use crate::memory::counters::Counters;
15
16pub(crate) static COUNTERS: Counters = Counters::new();
17
18pub struct TrackingAlloc<A> {
20 inner: A,
21 counters: &'static Counters,
25}
26
27impl<A> TrackingAlloc<A> {
28 pub const fn new(inner: A) -> Self {
30 Self {
31 inner,
32 counters: &COUNTERS,
33 }
34 }
35
36 #[cfg(test)]
38 pub(crate) const fn with_counters(inner: A, counters: &'static Counters) -> Self {
39 Self { inner, counters }
40 }
41}
42
43unsafe impl<A: GlobalAlloc> GlobalAlloc for TrackingAlloc<A> {
48 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
49 let ptr = unsafe { self.inner.alloc(layout) };
52 if !ptr.is_null() {
53 self.counters.record_alloc(layout.size());
54 crate::memory::detail::record_alloc(layout.size());
55 }
56 ptr
57 }
58
59 unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
60 let ptr = unsafe { self.inner.alloc_zeroed(layout) };
62 if !ptr.is_null() {
63 self.counters.record_alloc(layout.size());
64 crate::memory::detail::record_alloc(layout.size());
65 }
66 ptr
67 }
68
69 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
70 unsafe { self.inner.dealloc(ptr, layout) };
74 self.counters.record_free(layout.size());
75 crate::memory::detail::record_free(layout.size());
76 }
77
78 unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
82 let new_ptr = unsafe { self.inner.realloc(ptr, layout, new_size) };
85 if !new_ptr.is_null() {
86 self.counters.record_realloc(layout.size(), new_size);
87 crate::memory::detail::record_realloc(layout.size(), new_size);
88 }
89 new_ptr
90 }
91}
92
93#[macro_export]
105macro_rules! install_global_allocator {
106 () => {
107 #[global_allocator]
108 static CN_GLOBAL_ALLOC: $crate::memory::TrackingAlloc<std::alloc::System> =
109 $crate::memory::TrackingAlloc::new(std::alloc::System);
110 };
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use core::ptr;
117 use std::alloc::System;
118
119 const SIZE: usize = 4096;
120 const GROWN: usize = 64 * 1024;
121 const SHRUNK: usize = 1024;
122
123 fn layout(size: usize) -> Layout {
124 Layout::from_size_align(size, 8).expect("valid layout")
125 }
126
127 struct Failing;
130
131 unsafe impl GlobalAlloc for Failing {
134 unsafe fn alloc(&self, _layout: Layout) -> *mut u8 {
135 ptr::null_mut()
136 }
137
138 unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
139 unreachable!("this allocator never hands out a block to free")
140 }
141 }
142
143 #[test]
148 fn tracking_alloc_forwards_and_counts() {
149 static BLOCK: Counters = Counters::new();
150 let alloc = TrackingAlloc::with_counters(System, &BLOCK);
151 let layout = layout(SIZE);
152 assert_eq!(BLOCK.snapshot(), None, "nothing has allocated yet");
153
154 let ptr = unsafe { alloc.alloc(layout) };
157 assert!(!ptr.is_null(), "system allocator returned null for 4 KiB");
158 unsafe { ptr.write_bytes(0xAB, SIZE) };
160
161 let during = BLOCK.snapshot().expect("the wrapper just allocated");
162 assert_eq!(during.live_bytes, SIZE as u64);
163 assert_eq!(during.alloc_count, 1);
164 assert_eq!(during.free_count, 0);
165
166 unsafe { alloc.dealloc(ptr, layout) };
169 let after = BLOCK.snapshot().expect("counters are live");
170 assert_eq!(after.live_bytes, 0);
171 assert_eq!(after.alloc_count, 1);
172 assert_eq!(after.free_count, 1);
173 assert_eq!(after.peak_bytes, SIZE as u64);
174 }
175
176 #[test]
179 fn alloc_zeroed_forwards_the_zeroing_and_counts() {
180 static BLOCK: Counters = Counters::new();
181 let alloc = TrackingAlloc::with_counters(System, &BLOCK);
182 let layout = layout(SIZE);
183
184 let ptr = unsafe { alloc.alloc_zeroed(layout) };
187 assert!(!ptr.is_null(), "system allocator returned null for 4 KiB");
188 let bytes = unsafe { core::slice::from_raw_parts(ptr, SIZE) };
191 assert!(bytes.iter().all(|&b| b == 0), "the block was not zeroed");
192
193 let during = BLOCK.snapshot().expect("the wrapper just allocated");
194 assert_eq!(during.live_bytes, SIZE as u64);
195 assert_eq!(during.alloc_count, 1);
196
197 unsafe { alloc.dealloc(ptr, layout) };
200 assert_eq!(BLOCK.snapshot().expect("counters are live").live_bytes, 0);
201 }
202
203 #[test]
206 fn realloc_counts_the_resize_only() {
207 static BLOCK: Counters = Counters::new();
208 let alloc = TrackingAlloc::with_counters(System, &BLOCK);
209
210 let ptr = unsafe { alloc.alloc(layout(SIZE)) };
213 assert!(!ptr.is_null(), "system allocator returned null for 4 KiB");
214
215 let ptr = unsafe { alloc.realloc(ptr, layout(SIZE), GROWN) };
218 assert!(!ptr.is_null(), "system allocator returned null for 64 KiB");
219 let grown = BLOCK.snapshot().expect("the wrapper just allocated");
220 assert_eq!(grown.live_bytes, GROWN as u64);
221 assert_eq!(grown.alloc_count, 1, "a resize is not an allocation");
222 assert_eq!(grown.free_count, 0, "a resize is not a free");
223
224 let ptr = unsafe { alloc.realloc(ptr, layout(GROWN), SHRUNK) };
227 assert!(!ptr.is_null(), "system allocator returned null for 1 KiB");
228 let shrunk = BLOCK.snapshot().expect("counters are live");
229 assert_eq!(shrunk.live_bytes, SHRUNK as u64);
230 assert_eq!(shrunk.peak_bytes, GROWN as u64);
231
232 unsafe { alloc.dealloc(ptr, layout(SHRUNK)) };
234 let after = BLOCK.snapshot().expect("counters are live");
235 assert_eq!(after.live_bytes, 0);
236 assert_eq!(after.alloc_count, 1);
237 assert_eq!(after.free_count, 1);
238 }
239
240 #[test]
243 fn a_failed_allocation_counts_nothing() {
244 static BLOCK: Counters = Counters::new();
245 let alloc = TrackingAlloc::with_counters(Failing, &BLOCK);
246
247 assert!(unsafe { alloc.alloc(layout(SIZE)) }.is_null());
250 assert!(unsafe { alloc.alloc_zeroed(layout(SIZE)) }.is_null());
252 assert_eq!(BLOCK.snapshot(), None, "a failed allocation was counted");
253 }
254}