Skip to main content

mnemosyne_prof/
lib.rs

1//! Heap profiling runtime for the Mnemosyne allocator: user alloc/free trace
2//! hooks, a Poisson heap sampler, and an every-allocation leak detector, all
3//! reached through the `on_alloc`/`on_free` entry points the allocator crates
4//! call on every allocation and deallocation.
5
6#![cfg_attr(nightly_tls_active, feature(thread_local))]
7#![deny(missing_docs)]
8
9use core::ffi::c_void;
10use core::sync::atomic::{AtomicBool, AtomicPtr, AtomicUsize, Ordering};
11
12mod sampler;
13#[cfg(test)]
14mod tests;
15mod tls;
16
17pub use sampler::{Sample, StackId, dump_leaks, dump_profile};
18
19pub(crate) use tls::{enter_hook, exit_hook, sample_debit, should_skip_alloc_fast_path};
20// `get_profiler_state` exists only on the non-nightly TLS backend (the nightly
21// `#[thread_local]` path reads `THREAD_STATE` directly), so its import must
22// carry the same cfg as its definition — importing it unconditionally is an
23// E0432 whenever `nightly_tls_active` fires. Its sole use site
24// (`sampler.rs`) is already `#[cfg(not(nightly_tls_active))]`.
25#[cfg(not(nightly_tls_active))]
26pub(crate) use tls::get_profiler_state;
27#[cfg(nightly_tls_active)]
28pub(crate) use tls::{get_bytes_until_sample, set_bytes_until_sample};
29
30static ALLOC_HOOK: AtomicPtr<c_void> = AtomicPtr::new(core::ptr::null_mut());
31static FREE_HOOK: AtomicPtr<c_void> = AtomicPtr::new(core::ptr::null_mut());
32
33static PROFILING_ACTIVE: AtomicBool = AtomicBool::new(false);
34static LEAK_DETECTOR_ACTIVE: AtomicBool = AtomicBool::new(false);
35static PROFILING_OR_HOOKS_ACTIVE: AtomicBool = AtomicBool::new(false);
36static SAMPLE_INTERVAL: AtomicUsize = AtomicUsize::new(512 * 1024); // Default 512 KB
37
38/// Serializes control-plane updates (hook registration, sampler and
39/// leak-detector enable/disable) with the recompute of
40/// `PROFILING_OR_HOOKS_ACTIVE`.
41///
42/// Without it the recompute has a lost-update race: registrar A stores its
43/// flag and computes the aggregate; registrar B stores its flag, computes,
44/// and stores its aggregate; then A stores an aggregate that was computed
45/// *before* B's flag store — stranding `PROFILING_OR_HOOKS_ACTIVE` stale
46/// (hooks that silently never fire, or a permanent fast-path tax). Holding
47/// the lock across (flag store + recompute + aggregate store) orders the
48/// critical sections, so whichever registrar recomputes last observes every
49/// earlier flag store. Control-plane only — never taken on alloc/free paths
50/// — so a mutex is appropriate.
51static UPDATE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
52
53/// Runs `store` (the registrar's own flag store) and the aggregate recompute
54/// as one critical section under [`UPDATE_LOCK`].
55fn store_flags_then_update_active(store: impl FnOnce()) {
56    let _guard = UPDATE_LOCK
57        .lock()
58        .unwrap_or_else(std::sync::PoisonError::into_inner);
59    store();
60    let active = PROFILING_ACTIVE.load(Ordering::Acquire)
61        || LEAK_DETECTOR_ACTIVE.load(Ordering::Acquire)
62        || !ALLOC_HOOK.load(Ordering::Acquire).is_null()
63        || !FREE_HOOK.load(Ordering::Acquire).is_null();
64    PROFILING_OR_HOOKS_ACTIVE.store(active, Ordering::Release);
65}
66
67/// Registers a custom user allocation tracing hook.
68pub fn register_alloc_hook(hook: Option<unsafe extern "C" fn(*mut core::ffi::c_void, usize)>) {
69    let ptr = match hook {
70        Some(f) => f as *mut c_void,
71        None => core::ptr::null_mut(),
72    };
73    store_flags_then_update_active(|| ALLOC_HOOK.store(ptr, Ordering::Release));
74}
75
76/// Registers a custom user deallocation tracing hook.
77pub fn register_free_hook(hook: Option<unsafe extern "C" fn(*mut core::ffi::c_void, usize)>) {
78    let ptr = match hook {
79        Some(f) => f as *mut c_void,
80        None => core::ptr::null_mut(),
81    };
82    store_flags_then_update_active(|| FREE_HOOK.store(ptr, Ordering::Release));
83}
84
85/// Enables the built-in Poisson heap sampler.
86pub fn enable_profiling(sample_interval: usize) {
87    store_flags_then_update_active(|| {
88        SAMPLE_INTERVAL.store(sample_interval, Ordering::Release);
89        PROFILING_ACTIVE.store(true, Ordering::Release);
90    });
91}
92
93/// Disables the built-in Poisson heap sampler.
94pub fn disable_profiling() {
95    store_flags_then_update_active(|| PROFILING_ACTIVE.store(false, Ordering::Release));
96}
97
98/// Returns whether the built-in heap sampler is currently active.
99pub fn is_profiling_enabled() -> bool {
100    PROFILING_ACTIVE.load(Ordering::Acquire)
101}
102
103/// Resets the profiler state, trace hooks, and sampled data. Intended for testing.
104pub fn reset_profiler_for_testing() {
105    store_flags_then_update_active(|| {
106        PROFILING_ACTIVE.store(false, Ordering::Release);
107        LEAK_DETECTOR_ACTIVE.store(false, Ordering::Release);
108        ALLOC_HOOK.store(core::ptr::null_mut(), Ordering::Release);
109        FREE_HOOK.store(core::ptr::null_mut(), Ordering::Release);
110        SAMPLE_INTERVAL.store(512 * 1024, Ordering::Release);
111    });
112    // Sampler-internal locks are taken outside `UPDATE_LOCK` to keep the
113    // control-plane lock leaf-level (no nested acquisition order to maintain).
114    sampler::reset_sampler_state();
115}
116
117/// Enables the built-in memory leak detector, tracking every allocation with its backtrace.
118pub fn enable_leak_detector() {
119    store_flags_then_update_active(|| LEAK_DETECTOR_ACTIVE.store(true, Ordering::Release));
120}
121
122/// Disables the built-in memory leak detector.
123pub fn disable_leak_detector() {
124    store_flags_then_update_active(|| LEAK_DETECTOR_ACTIVE.store(false, Ordering::Release));
125}
126
127/// Returns whether the memory leak detector is currently active.
128pub fn is_leak_detector_enabled() -> bool {
129    LEAK_DETECTOR_ACTIVE.load(Ordering::Acquire)
130}
131
132/// Returns whether any tracing hook, the heap sampler, or the leak detector
133/// is currently active (the aggregate flag the allocator fast path checks).
134#[inline(always)]
135pub fn is_active() -> bool {
136    PROFILING_OR_HOOKS_ACTIVE.load(Ordering::Relaxed)
137}
138
139/// Entry point invoked on every successful memory allocation.
140///
141/// Calls any registered custom user hook and registers a sample if the
142/// Poisson heap sampler is active.
143#[inline(always)]
144pub fn on_alloc(ptr: *mut u8, size: usize) {
145    if !PROFILING_OR_HOOKS_ACTIVE.load(Ordering::Relaxed) {
146        return;
147    }
148
149    let hook_ptr = ALLOC_HOOK.load(Ordering::Relaxed);
150    // The budgeted fast skip is only sound when the allocation needs no leak
151    // tracking: pass the INACTIVE sense of the flag (a prior inversion here
152    // let a stale sampling budget hide allocations from the leak detector).
153    let leak_inactive = !LEAK_DETECTOR_ACTIVE.load(Ordering::Relaxed);
154    if should_skip_alloc_fast_path(size, hook_ptr.is_null(), leak_inactive) {
155        return;
156    }
157
158    on_alloc_cold(ptr, size);
159}
160
161#[inline(never)]
162fn on_alloc_cold(ptr: *mut u8, size: usize) {
163    if ptr.is_null() {
164        return;
165    }
166
167    let hook_ptr = ALLOC_HOOK.load(Ordering::Relaxed);
168    let active = PROFILING_ACTIVE.load(Ordering::Relaxed);
169    let leak_active = LEAK_DETECTOR_ACTIVE.load(Ordering::Relaxed);
170    if hook_ptr.is_null() && !active && !leak_active {
171        return;
172    }
173
174    let in_hook = enter_hook();
175    if in_hook {
176        return;
177    }
178
179    if !hook_ptr.is_null() {
180        // SAFETY: `hook_ptr` is non-null (just checked) and was published by
181        // `register_alloc_hook` as `f as *mut c_void` from a real
182        // `unsafe extern "C" fn(*mut c_void, usize)` under `Release`/`Acquire`
183        // ordering, so transmuting it back to that exact signature reconstructs
184        // a valid function pointer.
185        let hook: unsafe extern "C" fn(*mut core::ffi::c_void, usize) =
186            unsafe { core::mem::transmute(hook_ptr) };
187        // SAFETY: `ptr`/`size` are the just-completed allocation's address and
188        // size; the registered hook upholds its own `extern "C"` contract.
189        unsafe { hook(ptr as *mut core::ffi::c_void, size) };
190    }
191
192    if active || leak_active {
193        sampler::sample_alloc_inner(ptr, size, leak_active);
194    }
195
196    exit_hook();
197}
198
199/// Entry point invoked on every successful memory deallocation.
200///
201/// Calls any registered custom user hook and removes the sampled allocation
202/// if one is resident. Sample removal runs whenever resident samples exist —
203/// even after profiling/leak detection has been disabled — so stale samples
204/// drain on free instead of being reported as leaks by a later
205/// [`dump_leaks`].
206#[inline(always)]
207pub fn on_free(ptr: *mut u8, size: usize) {
208    // The cold path has work exactly when a free hook is registered or a
209    // resident sample may need eviction; the profiling/leak flags are
210    // irrelevant to frees (see `on_free_cold`).
211    if FREE_HOOK.load(Ordering::Relaxed).is_null() && !sampler::has_active_sample_for(ptr as usize)
212    {
213        return;
214    }
215
216    on_free_cold(ptr, size);
217}
218
219#[inline(never)]
220fn on_free_cold(ptr: *mut u8, size: usize) {
221    if ptr.is_null() {
222        return;
223    }
224
225    let hook_ptr = FREE_HOOK.load(Ordering::Relaxed);
226    // Sample removal is state hygiene, not sampling: a block recorded while
227    // the sampler or leak detector was active must still be evicted when it
228    // is freed after those modes were disabled. Gating removal on the active
229    // flags would (a) make a later `dump_leaks` falsely report the freed
230    // block and (b) leave the pointer's occupancy flag set forever, taxing
231    // every subsequent free in that shard with this cold call. Gate on
232    // resident samples instead.
233    let samples_resident = sampler::has_active_sample_for(ptr as usize);
234    if hook_ptr.is_null() && !samples_resident {
235        return;
236    }
237
238    let in_hook = enter_hook();
239    if in_hook {
240        return;
241    }
242
243    if !hook_ptr.is_null() {
244        // SAFETY: `hook_ptr` is non-null and was published by
245        // `register_free_hook` as `f as *mut c_void` from a real
246        // `unsafe extern "C" fn(*mut c_void, usize)`, so transmuting it back to
247        // that exact signature reconstructs a valid function pointer.
248        let hook: unsafe extern "C" fn(*mut core::ffi::c_void, usize) =
249            unsafe { core::mem::transmute(hook_ptr) };
250        // SAFETY: `ptr`/`size` describe the allocation being freed; the
251        // registered hook upholds its own `extern "C"` contract.
252        unsafe { hook(ptr as *mut core::ffi::c_void, size) };
253    }
254
255    if samples_resident {
256        sampler::sample_free_inner(ptr);
257    }
258
259    exit_hook();
260}