1use crate::{NumBytes, Number};
2use backtrace::Backtrace;
3use kumo_prometheus::ShardedCountAndSize;
4use parking_lot::Mutex;
5use std::alloc::{GlobalAlloc, Layout};
6use std::cell::Cell;
7use std::collections::HashMap;
8use std::sync::atomic::Ordering::Relaxed;
9use std::sync::atomic::{AtomicBool, AtomicUsize};
10use std::sync::LazyLock;
11
12thread_local! {
18 static IN_TRACKER: Cell<bool> = const { Cell::new(false) };
19}
20
21#[derive(Default)]
22pub struct TrackingAllocator<A: GlobalAlloc> {
23 allocator: A,
24}
25
26impl<A: GlobalAlloc> TrackingAllocator<A> {
27 pub const fn new(allocator: A) -> Self {
28 Self { allocator }
29 }
30}
31
32static STATS: Stats = Stats::new();
33
34const SMALL_SIZE: usize = 128;
35const MEDIUM_SIZE: usize = 4 * 1024;
36
37const MEDIUM_RATE: u64 = 64;
38const BIG_RATE: u64 = 1;
39
40static BIG_TRACKER: LazyLock<Mutex<AllocationTracker>> =
41 LazyLock::new(|| Mutex::new(AllocationTracker::default()));
42static MEDIUM_TRACKER: LazyLock<Mutex<AllocationTracker>> =
43 LazyLock::new(|| Mutex::new(AllocationTracker::default()));
44
45unsafe impl<A: GlobalAlloc> GlobalAlloc for TrackingAllocator<A> {
49 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
50 let ptr = unsafe { self.allocator.alloc(layout) };
51 if !ptr.is_null() {
52 track_allocation(ptr, layout.size());
53 }
54 ptr
55 }
56
57 unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
58 let ptr = unsafe { self.allocator.alloc_zeroed(layout) };
59 if !ptr.is_null() {
60 track_allocation(ptr, layout.size());
61 }
62 ptr
63 }
64
65 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
66 track_dealloc(ptr, layout.size());
67 unsafe { self.allocator.dealloc(ptr, layout) };
68 }
69
70 unsafe fn realloc(&self, old_ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
71 let track_callstacks = STATS.track_callstacks.load(Relaxed);
72 let mut saved_callstack = None;
73 track_dealloc_with(old_ptr, layout.size(), track_callstacks, |entry| {
74 saved_callstack = entry;
75 });
76
77 let new_ptr = unsafe { self.allocator.realloc(old_ptr, layout, new_size) };
78
79 if new_ptr.is_null() {
80 track_allocation_with(old_ptr, layout.size(), track_callstacks, || saved_callstack);
82 } else {
83 if let Some(entry) = saved_callstack {
84 IN_TRACKER.with(|in_track| {
87 let previous = in_track.replace(true);
88 drop(entry);
89 in_track.set(previous);
90 });
91 }
92 track_allocation(new_ptr, new_size);
93 }
94 new_ptr
95 }
96}
97
98fn track_allocation(ptr: *mut u8, size: usize) {
99 track_allocation_with(ptr, size, STATS.track_callstacks.load(Relaxed), || {
100 Some(CallstackEntry {
101 size,
102 bt: Backtrace::new_unresolved(),
103 })
104 });
105}
106
107#[inline]
108fn track_allocation_with(
109 ptr: *mut u8,
110 size: usize,
111 track_callstacks: bool,
112 callstack: impl FnOnce() -> Option<CallstackEntry>,
113) {
114 STATS.live.add(ptr.addr(), size);
115
116 if !track_callstacks {
117 return;
118 }
119
120 if size < SMALL_SIZE {
121 STATS.small.add(size);
122 return;
123 }
124
125 IN_TRACKER.with(|in_track| {
126 if !in_track.get() {
127 in_track.set(true);
128
129 let hash = PtrHash::new(ptr);
130 let track = hash.should_sample_size(size);
131
132 if track {
133 let entry = callstack();
134 if size < MEDIUM_SIZE {
135 STATS.medium.add(size);
136 if let Some(entry) = entry {
137 MEDIUM_TRACKER
138 .lock()
139 .track_allocation(hash, entry.size, entry.bt);
140 }
141 } else {
142 STATS.large.add(size);
143 if let Some(entry) = entry {
144 BIG_TRACKER
145 .lock()
146 .track_allocation(hash, entry.size, entry.bt);
147 }
148 }
149 }
150
151 in_track.set(false);
152 }
153 });
154}
155
156fn track_dealloc(ptr: *mut u8, size: usize) {
157 track_dealloc_with(ptr, size, STATS.track_callstacks.load(Relaxed), drop);
158}
159
160#[inline]
161fn track_dealloc_with(
162 ptr: *mut u8,
163 size: usize,
164 track_callstacks: bool,
165 removed: impl FnOnce(Option<CallstackEntry>),
166) {
167 STATS.live.sub(ptr.addr(), size);
168
169 if !track_callstacks {
170 return;
171 }
172
173 if size < SMALL_SIZE {
174 STATS.small.sub(size);
175 return;
176 }
177
178 IN_TRACKER.with(|in_track| {
179 if !in_track.get() {
180 in_track.set(true);
181
182 let hash = PtrHash::new(ptr);
183 let track = hash.should_sample_size(size);
184
185 if track {
186 if size < MEDIUM_SIZE {
187 let entry = MEDIUM_TRACKER.lock().track_dealloc(hash, size);
188 STATS.medium.sub(size);
189 removed(entry);
190 } else {
191 STATS.large.sub(size);
192 let entry = BIG_TRACKER.lock().track_dealloc(hash, size);
193 removed(entry);
194 }
195 }
196
197 in_track.set(false);
198 }
199 });
200}
201
202fn stochastic_rate_by_size(size: usize) -> u64 {
205 if size < MEDIUM_SIZE {
206 MEDIUM_RATE
207 } else {
208 BIG_RATE
209 }
210}
211
212#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
216struct PtrHash(u64);
217
218impl PtrHash {
219 #[inline]
220 pub fn new(ptr: *mut u8) -> Self {
221 Self(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(ptr))
222 }
223
224 pub fn should_sample_size(&self, size: usize) -> bool {
228 let rate = stochastic_rate_by_size(size);
229 self.should_sample_at_rate(rate)
230 }
231
232 pub fn should_sample_at_rate(&self, rate: u64) -> bool {
238 self.0 & (rate - 1) == 0
239 }
240}
241
242struct CallstackEntry {
243 size: usize,
244 bt: Backtrace,
245}
246
247pub struct CallstackStats {
248 pub count: usize,
249 pub total_size: usize,
250 pub bt: Backtrace,
251 pub stochastic_rate: usize,
252}
253
254#[derive(Default)]
255struct AllocationTracker {
256 live_allocations: ahash::HashMap<PtrHash, CallstackEntry>,
257}
258
259impl AllocationTracker {
260 pub fn track_allocation(&mut self, ptr: PtrHash, size: usize, bt: Backtrace) {
261 self.live_allocations
262 .insert(ptr, CallstackEntry { size, bt });
263 }
264
265 pub fn track_dealloc(&mut self, ptr: PtrHash, _size: usize) -> Option<CallstackEntry> {
266 self.live_allocations.remove(&ptr)
267 }
268
269 pub fn top_callstacks(&self, max_stacks: usize) -> Vec<CallstackStats> {
270 let mut by_stack = HashMap::new();
271
272 for alloc in self.live_allocations.values() {
273 let key = alloc.bt.frames().iter().map(|f| f.ip()).collect::<Vec<_>>();
274 let entry = by_stack.entry(key).or_insert_with(|| CallstackStats {
275 count: 0,
276 total_size: 0,
277 bt: alloc.bt.clone(),
278 stochastic_rate: stochastic_rate_by_size(alloc.size) as usize,
279 });
280
281 entry.count += 1;
282 entry.total_size += alloc.size;
283 }
284
285 let mut stats = by_stack.into_values().collect::<Vec<_>>();
286 stats.sort_by(|a, b| b.total_size.cmp(&a.total_size));
287 stats.truncate(max_stacks);
288 stats.shrink_to_fit();
289 stats
290 }
291}
292
293struct AtomicCountAndSize {
294 pub count: AtomicUsize,
296
297 pub size: AtomicUsize,
299}
300
301impl AtomicCountAndSize {
302 pub const fn zero() -> Self {
303 Self {
304 count: AtomicUsize::new(0),
305 size: AtomicUsize::new(0),
306 }
307 }
308
309 fn add(&self, size: usize) {
311 self.count.fetch_add(1, Relaxed);
312 self.size.fetch_add(size, Relaxed);
313 }
314
315 fn sub(&self, size: usize) {
317 self.count.fetch_sub(1, Relaxed);
318 self.size.fetch_sub(size, Relaxed);
319 }
320}
321
322#[derive(Debug, Clone, Copy)]
323pub struct CountAndSize {
324 pub count: Number,
325 pub size: NumBytes,
326}
327
328struct Stats {
329 live: ShardedCountAndSize,
330 track_callstacks: AtomicBool,
331 small: AtomicCountAndSize,
332 medium: AtomicCountAndSize,
333 large: AtomicCountAndSize,
334}
335
336impl Stats {
337 const fn new() -> Self {
338 Self {
339 live: ShardedCountAndSize::new(),
340 small: AtomicCountAndSize::zero(),
341 medium: AtomicCountAndSize::zero(),
342 large: AtomicCountAndSize::zero(),
343 track_callstacks: AtomicBool::new(false),
344 }
345 }
346}
347
348pub fn counted_usage() -> usize {
353 STATS.live.load_size()
354}
355
356pub fn set_tracking_callstacks(enable: bool) {
357 STATS.track_callstacks.store(enable, Relaxed);
358}
359
360pub struct TrackingStats {
361 pub small_threshold: NumBytes,
362 pub live: CountAndSize,
363 pub top_callstacks: Vec<CallstackStats>,
364}
365
366pub fn tracking_stats() -> TrackingStats {
367 const MAX_STACKS: usize = 128;
368
369 let mut top_callstacks = vec![];
370
371 IN_TRACKER.with(|in_track| {
372 if !in_track.get() {
373 in_track.set(true);
374 top_callstacks = BIG_TRACKER.lock().top_callstacks(MAX_STACKS);
375 top_callstacks.append(&mut MEDIUM_TRACKER.lock().top_callstacks(MAX_STACKS));
376
377 for stack in &mut top_callstacks {
378 stack.bt.resolve();
379 }
380
381 in_track.set(false);
382 }
383 });
384
385 let live = STATS.live.load();
386 TrackingStats {
387 small_threshold: SMALL_SIZE.into(),
388 live: CountAndSize {
389 count: live.count.into(),
390 size: live.size.into(),
391 },
392 top_callstacks,
393 }
394}