1use crate::collector::{CollectorAvailability, CollectorCapability, CollectorId};
15use std::alloc::{GlobalAlloc, Layout, System};
16
17pub const STAGE_SLOTS: usize = crate::runtime::STAGE_COUNT + 1;
20pub const ROOT_SLOT: usize = crate::runtime::STAGE_COUNT;
22
23#[cfg(feature = "allocation-tracking")]
24mod tracked {
25 use super::*;
26 use std::cell::{Cell, UnsafeCell};
27 use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
28
29 const MAX_STAGE_DEPTH: usize = 64;
30 const HEADER_BYTES: usize = 16;
31 const HEADER_MAGIC: u8 = 0xA5;
32
33 pub(super) static INSTALLED: AtomicBool = AtomicBool::new(false);
34 static ACTIVE_ALLOC_SESSIONS: AtomicUsize = AtomicUsize::new(0);
36 static ALLOC_SESSION_OVERLAP: AtomicBool = AtomicBool::new(false);
38 static ALLOCATIONS: AtomicU64 = AtomicU64::new(0);
39 static DEALLOCATIONS: AtomicU64 = AtomicU64::new(0);
40 static ALLOCATION_BYTES: AtomicU64 = AtomicU64::new(0);
41 static DEALLOCATION_BYTES: AtomicU64 = AtomicU64::new(0);
42 static LIVE_BYTES: AtomicU64 = AtomicU64::new(0);
43 static PEAK_LIVE_BYTES: AtomicU64 = AtomicU64::new(0);
44 static SLOT_ALLOCATIONS: [AtomicU64; STAGE_SLOTS] = [const { AtomicU64::new(0) }; STAGE_SLOTS];
45 static SLOT_ALLOCATION_BYTES: [AtomicU64; STAGE_SLOTS] =
46 [const { AtomicU64::new(0) }; STAGE_SLOTS];
47 static SLOT_DEALLOCATION_BYTES: [AtomicU64; STAGE_SLOTS] =
48 [const { AtomicU64::new(0) }; STAGE_SLOTS];
49 static SLOT_LIVE_BYTES: [AtomicU64; STAGE_SLOTS] = [const { AtomicU64::new(0) }; STAGE_SLOTS];
50 static SLOT_PEAK_LIVE_BYTES: [AtomicU64; STAGE_SLOTS] =
51 [const { AtomicU64::new(0) }; STAGE_SLOTS];
52
53 thread_local! {
54 static STAGE_STACK_DEPTH: Cell<u16> = const { Cell::new(0) };
55 static STAGE_STACK: UnsafeCell<[u8; MAX_STAGE_DEPTH]> =
56 const { UnsafeCell::new([0; MAX_STAGE_DEPTH]) };
57 }
58
59 pub(crate) fn stage_context_push(stage: crate::Stage) {
61 STAGE_STACK_DEPTH.with(|depth| {
62 let current = depth.get();
63 if (current as usize) < MAX_STAGE_DEPTH {
64 STAGE_STACK.with(|stack| {
65 unsafe { (*stack.get())[current as usize] = stage.index() as u8 };
68 });
69 }
70 depth.set(current.saturating_add(1));
71 });
72 }
73
74 pub(crate) fn stage_context_pop() {
76 STAGE_STACK_DEPTH.with(|depth| depth.set(depth.get().saturating_sub(1)));
77 }
78
79 fn current_slot() -> usize {
80 STAGE_STACK_DEPTH.with(|depth| {
81 let current = depth.get();
82 if current == 0 || current as usize > MAX_STAGE_DEPTH {
83 return ROOT_SLOT;
84 }
85 STAGE_STACK.with(|stack| {
86 usize::from(unsafe { (*stack.get())[current as usize - 1] })
89 })
90 })
91 }
92
93 #[inline]
94 fn record_alloc(slot: usize, bytes: u64) {
95 INSTALLED.store(true, Ordering::Relaxed);
96 ALLOCATIONS.fetch_add(1, Ordering::Relaxed);
97 ALLOCATION_BYTES.fetch_add(bytes, Ordering::Relaxed);
98 let live = LIVE_BYTES.fetch_add(bytes, Ordering::Relaxed) + bytes;
99 PEAK_LIVE_BYTES.fetch_max(live, Ordering::Relaxed);
100 SLOT_ALLOCATIONS[slot].fetch_add(1, Ordering::Relaxed);
101 SLOT_ALLOCATION_BYTES[slot].fetch_add(bytes, Ordering::Relaxed);
102 let slot_live = SLOT_LIVE_BYTES[slot].fetch_add(bytes, Ordering::Relaxed) + bytes;
103 SLOT_PEAK_LIVE_BYTES[slot].fetch_max(slot_live, Ordering::Relaxed);
104 }
105
106 #[inline]
107 fn record_dealloc(slot: usize, bytes: u64) {
108 if slot >= STAGE_SLOTS {
109 return;
112 }
113 DEALLOCATIONS.fetch_add(1, Ordering::Relaxed);
114 DEALLOCATION_BYTES.fetch_add(bytes, Ordering::Relaxed);
115 saturating_fetch_sub(&LIVE_BYTES, bytes);
116 SLOT_DEALLOCATION_BYTES[slot].fetch_add(bytes, Ordering::Relaxed);
117 saturating_fetch_sub(&SLOT_LIVE_BYTES[slot], bytes);
118 }
119
120 #[inline]
121 fn saturating_fetch_sub(cell: &AtomicU64, bytes: u64) {
122 let mut current = cell.load(Ordering::Relaxed);
123 loop {
124 let next = current.saturating_sub(bytes);
125 match cell.compare_exchange_weak(current, next, Ordering::Relaxed, Ordering::Relaxed) {
126 Ok(_) => break,
127 Err(observed) => current = observed,
128 }
129 }
130 }
131
132 pub(super) fn snapshot_totals() -> (u64, u64, u64, u64, u64, u64) {
133 (
134 ALLOCATIONS.load(Ordering::Relaxed),
135 DEALLOCATIONS.load(Ordering::Relaxed),
136 ALLOCATION_BYTES.load(Ordering::Relaxed),
137 DEALLOCATION_BYTES.load(Ordering::Relaxed),
138 LIVE_BYTES.load(Ordering::Relaxed),
139 PEAK_LIVE_BYTES.load(Ordering::Relaxed),
140 )
141 }
142
143 pub(super) fn snapshot_slot(slot: usize) -> super::AllocationSlotV2 {
144 let allocated = SLOT_ALLOCATION_BYTES[slot].load(Ordering::Relaxed);
145 let deallocated = SLOT_DEALLOCATION_BYTES[slot].load(Ordering::Relaxed);
146 super::AllocationSlotV2 {
147 allocations: SLOT_ALLOCATIONS[slot].load(Ordering::Relaxed),
148 allocated_bytes: allocated,
149 live_bytes: allocated.saturating_sub(deallocated),
150 peak_live_bytes: SLOT_PEAK_LIVE_BYTES[slot].load(Ordering::Relaxed),
151 }
152 }
153
154 pub(super) fn reset_peaks() {
155 PEAK_LIVE_BYTES.store(LIVE_BYTES.load(Ordering::Relaxed), Ordering::Relaxed);
156 for slot in 0..STAGE_SLOTS {
157 SLOT_PEAK_LIVE_BYTES[slot].store(
158 SLOT_LIVE_BYTES[slot].load(Ordering::Relaxed),
159 Ordering::Relaxed,
160 );
161 }
162 }
163
164 pub(super) fn enter_session() -> (bool, bool) {
170 let prev = ACTIVE_ALLOC_SESSIONS.fetch_add(1, Ordering::AcqRel);
171 if prev == 0 {
172 ALLOC_SESSION_OVERLAP.store(false, Ordering::Release);
173 reset_peaks();
174 (true, false)
175 } else {
176 ALLOC_SESSION_OVERLAP.store(true, Ordering::Release);
177 (true, true)
178 }
179 }
180
181 pub(super) fn leave_session() {
182 ACTIVE_ALLOC_SESSIONS.fetch_sub(1, Ordering::AcqRel);
183 }
184
185 pub(super) fn session_evidence_reliable(joined_overlapped: bool) -> bool {
186 if joined_overlapped {
187 return false;
188 }
189 if ALLOC_SESSION_OVERLAP.load(Ordering::Acquire) {
190 return false;
191 }
192 ACTIVE_ALLOC_SESSIONS.load(Ordering::Acquire) == 1
193 }
194
195 #[repr(C)]
196 struct AllocationHeader {
197 stage: u8,
198 magic: u8,
199 reserved: [u8; 6],
200 bytes: u64,
201 }
202
203 const _: () = assert!(std::mem::size_of::<AllocationHeader>() == HEADER_BYTES);
204
205 pub(super) unsafe fn tracked_alloc(layout: Layout) -> *mut u8 {
207 let offset = layout.align().max(HEADER_BYTES);
208 let Some(total) = layout.size().checked_add(offset) else {
209 return std::ptr::null_mut();
210 };
211 let Ok(real) = Layout::from_size_align(total, offset) else {
212 return std::ptr::null_mut();
213 };
214 let base = unsafe { System.alloc(real) };
216 if base.is_null() {
217 return base;
218 }
219 let slot = current_slot();
220 let header = AllocationHeader {
221 stage: slot as u8,
222 magic: HEADER_MAGIC,
223 reserved: [0; 6],
224 bytes: layout.size() as u64,
225 };
226 unsafe { base.cast::<AllocationHeader>().write(header) };
229 record_alloc(slot, layout.size() as u64);
230 unsafe { base.add(offset) }
232 }
233
234 pub(super) unsafe fn tracked_dealloc(ptr: *mut u8, layout: Layout) {
242 let offset = layout.align().max(HEADER_BYTES);
243 let base = unsafe { ptr.sub(offset) };
246 let header = unsafe { base.cast::<AllocationHeader>().read() };
249 let stage = usize::from(header.stage);
250 let header_ok = header.magic == HEADER_MAGIC
251 && header.bytes == layout.size() as u64
252 && stage < STAGE_SLOTS;
253 let user_bytes = if header_ok {
254 record_dealloc(stage, header.bytes);
255 header.bytes as usize
256 } else {
257 layout.size()
259 };
260 let real = Layout::from_size_align(user_bytes.saturating_add(offset), offset)
261 .unwrap_or_else(|_| {
262 unsafe { Layout::from_size_align_unchecked(layout.size() + offset, offset) }
266 });
267 unsafe { System.dealloc(base, real) };
271 }
272}
273
274#[cfg(not(feature = "allocation-tracking"))]
275mod untracked {
276 pub(super) fn snapshot_totals() -> (u64, u64, u64, u64, u64, u64) {
277 (0, 0, 0, 0, 0, 0)
278 }
279
280 pub(super) fn snapshot_slot(_slot: usize) -> super::AllocationSlotV2 {
281 super::AllocationSlotV2 {
282 allocations: 0,
283 allocated_bytes: 0,
284 live_bytes: 0,
285 peak_live_bytes: 0,
286 }
287 }
288
289 pub(super) fn reset_peaks() {}
290}
291
292#[cfg(feature = "allocation-tracking")]
293use tracked as backend;
294#[cfg(not(feature = "allocation-tracking"))]
295use untracked as backend;
296
297#[cfg(feature = "allocation-tracking")]
298pub(crate) use backend::{stage_context_pop, stage_context_push};
299
300#[cfg(not(feature = "allocation-tracking"))]
303#[inline(always)]
304pub(crate) fn stage_context_push(_stage: crate::Stage) {}
305
306#[cfg(not(feature = "allocation-tracking"))]
309#[inline(always)]
310pub(crate) fn stage_context_pop() {}
311
312pub struct TrackingAllocator;
316
317impl TrackingAllocator {
318 pub const fn new() -> Self {
319 Self
320 }
321}
322
323impl Default for TrackingAllocator {
324 fn default() -> Self {
325 Self::new()
326 }
327}
328
329unsafe impl GlobalAlloc for TrackingAllocator {
332 #[cfg(feature = "allocation-tracking")]
333 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
334 unsafe { tracked::tracked_alloc(layout) }
336 }
337
338 #[cfg(feature = "allocation-tracking")]
339 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
340 unsafe { tracked::tracked_dealloc(ptr, layout) }
342 }
343
344 #[cfg(not(feature = "allocation-tracking"))]
345 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
346 unsafe { System.alloc(layout) }
348 }
349
350 #[cfg(not(feature = "allocation-tracking"))]
351 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
352 unsafe { System.dealloc(ptr, layout) }
354 }
355}
356
357pub fn allocation_tracking_installed() -> bool {
359 #[cfg(feature = "allocation-tracking")]
360 {
361 tracked::INSTALLED.load(std::sync::atomic::Ordering::Relaxed)
362 }
363 #[cfg(not(feature = "allocation-tracking"))]
364 {
365 false
366 }
367}
368
369#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
371pub struct AllocationSlotV2 {
372 pub allocations: u64,
373 pub allocated_bytes: u64,
374 pub live_bytes: u64,
375 pub peak_live_bytes: u64,
376}
377
378#[derive(Clone, Debug, Eq, PartialEq)]
380pub struct AllocationSnapshotV2 {
381 pub allocations: u64,
382 pub deallocations: u64,
383 pub allocated_bytes: u64,
384 pub deallocated_bytes: u64,
385 pub live_bytes: u64,
386 pub peak_live_bytes: u64,
387 pub slots: [AllocationSlotV2; STAGE_SLOTS],
389}
390
391impl AllocationSnapshotV2 {
392 pub fn slot(&self, stage: crate::Stage) -> &AllocationSlotV2 {
394 &self.slots[stage.index()]
395 }
396
397 pub fn root(&self) -> &AllocationSlotV2 {
399 &self.slots[ROOT_SLOT]
400 }
401
402 pub fn live_delta_since(&self, start: &Self) -> u64 {
404 self.live_bytes.saturating_sub(start.live_bytes)
405 }
406}
407
408pub fn allocation_snapshot() -> AllocationSnapshotV2 {
411 let (allocations, deallocations, allocated_bytes, deallocated_bytes, live_bytes, peak) =
412 backend::snapshot_totals();
413 AllocationSnapshotV2 {
414 allocations,
415 deallocations,
416 allocated_bytes,
417 deallocated_bytes,
418 live_bytes,
419 peak_live_bytes: peak,
420 slots: std::array::from_fn(backend::snapshot_slot),
421 }
422}
423
424pub fn reset_allocation_peaks() {
430 backend::reset_peaks();
431}
432
433pub(crate) struct AllocationSessionToken {
438 active: bool,
439 overlapped: bool,
440}
441
442impl AllocationSessionToken {
443 pub(crate) const fn inactive() -> Self {
444 Self {
445 active: false,
446 overlapped: false,
447 }
448 }
449
450 pub(crate) fn evidence_is_reliable(&self) -> bool {
451 if !self.active {
452 return true;
453 }
454 #[cfg(feature = "allocation-tracking")]
455 {
456 backend::session_evidence_reliable(self.overlapped)
457 }
458 #[cfg(not(feature = "allocation-tracking"))]
459 {
460 true
461 }
462 }
463}
464
465impl Drop for AllocationSessionToken {
466 fn drop(&mut self) {
467 if !self.active {
468 return;
469 }
470 self.active = false;
471 #[cfg(feature = "allocation-tracking")]
472 {
473 backend::leave_session();
474 }
475 }
476}
477
478pub(crate) fn enter_allocation_session() -> AllocationSessionToken {
482 #[cfg(feature = "allocation-tracking")]
483 {
484 let (active, overlapped) = backend::enter_session();
485 AllocationSessionToken { active, overlapped }
486 }
487 #[cfg(not(feature = "allocation-tracking"))]
488 {
489 AllocationSessionToken::inactive()
490 }
491}
492
493pub(crate) fn allocation_capability() -> CollectorCapability {
494 #[cfg(not(feature = "allocation-tracking"))]
495 {
496 CollectorCapability::unavailable(
497 CollectorId::AllocationTracking,
498 CollectorAvailability::Disabled,
499 "enable the keyhog-profile allocation-tracking feature",
500 )
501 }
502 #[cfg(feature = "allocation-tracking")]
503 {
504 if allocation_tracking_installed() {
505 CollectorCapability::available(CollectorId::AllocationTracking)
506 } else {
507 CollectorCapability::unavailable(
508 CollectorId::AllocationTracking,
509 CollectorAvailability::Unavailable,
510 "install keyhog_profile::TrackingAllocator as the global allocator to count allocations",
511 )
512 }
513 }
514}