Skip to main content

mimalloc_pprof/
lib.rs

1//! Rust global allocator support for the in-tree mimalloc build.
2//!
3//! ```no_run
4//! use mimalloc_pprof::{prof, MiMalloc};
5//! #[global_allocator] static ALLOCATOR: MiMalloc = MiMalloc;
6//! # fn main() -> std::io::Result<()> {
7//! prof::start(512 * 1024);
8//! prof::dump_file(std::path::Path::new("heap.prof"))?;
9//! # Ok(()) }
10//! ```
11//!
12//! Profiling is enabled by default. To build the allocator without profiler
13//! hooks, depend on this crate with `default-features = false`; in that mode the
14//! profiling API remains available but cannot start a profiler.
15//!
16//! See the README's Rust integration guide for frame-pointer and line-table
17//! build flags. Open the resulting profile with `pprof -http=: app.exe heap.prof`.
18
19use core::alloc::{GlobalAlloc, Layout};
20use core::ffi::c_void;
21use std::ffi::CString;
22use std::path::PathBuf;
23
24pub mod sys;
25
26/// A `#[global_allocator]` implementation backed by mimalloc.
27pub struct MiMalloc;
28
29unsafe impl GlobalAlloc for MiMalloc {
30    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
31        sys::mi_malloc_aligned(layout.size(), layout.align()).cast()
32    }
33
34    unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
35        sys::mi_free(ptr.cast::<c_void>());
36    }
37
38    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
39        sys::mi_realloc_aligned(ptr.cast::<c_void>(), new_size, layout.align()).cast()
40    }
41
42    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
43        sys::mi_zalloc_aligned(layout.size(), layout.align()).cast()
44    }
45}
46
47/// Allocate `size` bytes from mimalloc's raw-OS-layer "unwrapped" path.
48///
49/// Thin wrapper around `mi_unwrapped_malloc` (include/mimalloc/memory-events.h):
50/// backed directly by `_mi_os_alloc_aligned`, never by the hooked `mi_malloc`
51/// family. Page granular, so this is not meant for hot-path/small allocations
52/// — it exists for low-level instrumentation and recursion avoidance (e.g.
53/// scratch storage for a memory-change callback that must not recursively
54/// enter mimalloc). Excluded from normal mimalloc allocation stats and from
55/// the memory-change accounting.
56///
57/// Returns a null pointer on failure (including invalid `alignment`; see
58/// `# Safety` below).
59///
60/// # Safety
61///
62/// - `alignment` must be `0` (treated as `align_of::<*const ()>()`, i.e.
63///   pointer size) or a power of two. A non-power-of-two, non-zero alignment
64///   is a validated input on the C side: `mi_unwrapped_malloc` returns a null
65///   pointer rather than invoking undefined behavior, but callers should not
66///   rely on that as anything other than a defined-failure contract — treat
67///   the alignment argument as a precondition to get right, not a value to
68///   probe.
69/// - The returned pointer, if non-null, must be passed only to
70///   [`unwrapped_free`] or [`unwrapped_realloc`] — never to `mi_free`, this
71///   crate's [`MiMalloc`] allocator, or Rust's global allocator, and vice
72///   versa (a pointer from `mi_malloc`/the Rust global allocator must never
73///   be passed to [`unwrapped_free`]/[`unwrapped_realloc`]). Mixing these
74///   families corrupts allocator-internal bookkeeping.
75/// - The memory is uninitialized; reading it before writing is undefined
76///   behavior, as with any raw allocation.
77pub unsafe fn unwrapped_malloc(size: usize, alignment: usize) -> *mut u8 {
78    unsafe { sys::mi_unwrapped_malloc(size, alignment).cast() }
79}
80
81/// Free a pointer returned by [`unwrapped_malloc`] or [`unwrapped_realloc`].
82///
83/// Thin wrapper around `mi_unwrapped_free` (include/mimalloc/memory-events.h).
84///
85/// # Safety
86///
87/// - `p` must be either a null pointer (a documented, safe no-op on the C
88///   side) or a pointer previously returned by [`unwrapped_malloc`] or
89///   [`unwrapped_realloc`] that has not already been freed.
90/// - `p` must never have come from `mi_malloc`, this crate's [`MiMalloc`]
91///   allocator, or Rust's global allocator — passing such a pointer here is
92///   undefined behavior (the "unwrapped" and normal allocation families use
93///   incompatible header layouts and are validated by a magic-number check
94///   that a foreign pointer will not satisfy).
95pub unsafe fn unwrapped_free(p: *mut u8) {
96    unsafe { sys::mi_unwrapped_free(p.cast()) }
97}
98
99/// Resize a pointer returned by [`unwrapped_malloc`] or [`unwrapped_realloc`].
100///
101/// Thin wrapper around `mi_unwrapped_realloc` (include/mimalloc/memory-events.h).
102/// If `p` is null, this behaves like [`unwrapped_malloc`]. If `new_size` is
103/// `0`, this frees `p` (like [`unwrapped_free`]) and returns a null pointer.
104/// Otherwise the existing contents are copied into a freshly allocated
105/// unwrapped block (up to `min(old payload size, new_size)` bytes) and `p` is
106/// freed; `p` must not be used again after this call, whether or not it
107/// returns null.
108///
109/// Returns a null pointer on failure (including invalid `alignment`; see
110/// [`unwrapped_malloc`]'s `# Safety` section), in which case `p` is left
111/// valid and unfreed.
112///
113/// # Safety
114///
115/// - `p` must be either a null pointer or a pointer previously returned by
116///   [`unwrapped_malloc`] or [`unwrapped_realloc`] that has not already been
117///   freed, per the same family-isolation rule as [`unwrapped_free`].
118/// - `alignment` has the same power-of-two-or-zero contract as
119///   [`unwrapped_malloc`].
120/// - After this call, `p` must not be read, written, or freed again — treat
121///   it as consumed regardless of whether the return value is null.
122pub unsafe fn unwrapped_realloc(p: *mut u8, new_size: usize, alignment: usize) -> *mut u8 {
123    unsafe { sys::mi_unwrapped_realloc(p.cast(), new_size, alignment).cast() }
124}
125
126/// Grow or shrink an allocation, zeroing any newly-exposed tail.
127///
128/// Thin wrapper around `mi_rezalloc`. This is the operation Rust's [`GlobalAlloc`]
129/// cannot express — that trait has no `grow_zeroed` — so without it a caller has to
130/// grow and then `memset` by hand, repeating work the allocator has already done, and
131/// (with zero-tracking) work it may be able to skip entirely.
132///
133/// # What is actually zeroed
134///
135/// **Not** `[old_requested_size, new_size)`. mimalloc measures from the block's old
136/// *usable* size, so the slack between what you asked for and what the block actually
137/// holds is left untouched:
138///
139/// ```text
140/// requested 64  ->  usable 80  ->  rezalloc to 70
141/// bytes [64,70) are NOT zeroed: the grow was served in place, within the old block
142/// ```
143///
144/// The guarantee is: everything past [`usable_size`] of the *original* block is zero.
145/// If you need a specific range zeroed, capture [`usable_size`] before the call and
146/// zero the remainder yourself.
147///
148/// (This is documented so precisely because a fuzz harness asserted the intuitive
149/// version and was falsified within seconds — see issue #87.)
150///
151/// # Safety
152///
153/// - `p` must be null, or a pointer from the **plain** allocation family — the global
154///   allocator, [`sys::mi_malloc`], or a previous [`rezalloc`]/[`recalloc`] — that has
155///   not been freed.
156/// - **Not interchangeable with [`unwrapped_malloc`]/[`unwrapped_realloc`].** Those
157///   place a header before the pointer, so passing one here fails the pointer check
158///   (`mi_usable_size: invalid pointer`) rather than working by accident.
159/// - After this call `p` is consumed: do not read, write, or free it again, whether or
160///   not the return value is null.
161/// - On failure a null pointer is returned and `p` is left valid and unfreed.
162pub unsafe fn rezalloc(p: *mut u8, new_size: usize) -> *mut u8 {
163    unsafe { sys::mi_rezalloc(p.cast(), new_size).cast() }
164}
165
166/// Grow or shrink an allocation to `count * size` bytes, zeroing any newly-exposed tail.
167///
168/// The [`rezalloc`] contract applies, including what is and is not zeroed. Thin wrapper
169/// around `mi_recalloc`; the element-count form exists to mirror `calloc`.
170///
171/// # Safety
172///
173/// Same contract as [`rezalloc`].
174pub unsafe fn recalloc(p: *mut u8, count: usize, size: usize) -> *mut u8 {
175    unsafe { sys::mi_recalloc(p.cast(), count, size).cast() }
176}
177
178/// Try to grow an allocation **in place**, without moving it.
179///
180/// Returns a null pointer if the block cannot be extended where it is — in which case
181/// `p` remains valid and unchanged, unlike [`rezalloc`]. Useful when moving would be
182/// more expensive than falling back to a different strategy.
183///
184/// # Safety
185///
186/// - `p` must be a pointer from this allocator that has not been freed.
187/// - Unlike [`rezalloc`], `p` is **not** consumed: on failure it is still live and must
188///   still be freed.
189pub unsafe fn expand(p: *mut u8, new_size: usize) -> *mut u8 {
190    unsafe { sys::mi_expand(p.cast(), new_size).cast() }
191}
192
193/// Bytes actually available in an allocation, which may exceed what was requested.
194///
195/// # Safety
196///
197/// `p` must be a live pointer from this allocator.
198pub unsafe fn usable_size(p: *const u8) -> usize {
199    unsafe { sys::mi_usable_size(p.cast()) }
200}
201
202/// Live per-heap -> per-page -> (optional) per-block JSON snapshot of the current
203/// subprocess (issue #269, Bun parity P4). Backs Bun's shipped `bun:jsc`
204/// `heapStats({dump:true|"blocks"}).mimallocDump`; see `src/heap-dump.c` for the JSON
205/// shape (`{"heaps":[{"seq":N,"pages":[{"id","block_size","used","reserved","thread_id"}],
206/// "blocks":[[id,size],...]}]}`, `blocks` present only when `include_blocks`).
207///
208/// Set `hash_addresses` to mix every reported address through a per-process key so a
209/// dump can be shared or diffed without exposing raw ASLR-derived pointers.
210///
211/// Best-effort under concurrent frees on the heaps being walked (mimalloc's
212/// `mi_heap_visit_blocks`/`mi_subproc_visit_heaps` contract; see the caveat on
213/// `src/heap-dump.c` and issue #78) -- never `unsafe` to call, but a heap another
214/// thread is actively freeing into may be under- or over-reported in the returned JSON.
215///
216/// Returns `None` only on allocation failure (out of memory building the JSON buffer),
217/// not for an empty subprocess.
218pub fn heap_dump_json(include_blocks: bool, hash_addresses: bool) -> Option<String> {
219    use std::ffi::CStr;
220    let ptr = unsafe { sys::mi_heap_dump_json(include_blocks, hash_addresses) };
221    if ptr.is_null() {
222        return None;
223    }
224    let json = unsafe { CStr::from_ptr(ptr) }
225        .to_string_lossy()
226        .into_owned();
227    unsafe { sys::mi_free(ptr.cast()) };
228    Some(json)
229}
230
231/// Tell mimalloc this thread is idle (issue #272, Bun parity P7a).
232///
233/// Collects this thread's pending frees, discards the free blocks inside its still-used
234/// pages, and hands the arena purge to the background scavenger thread so freed memory
235/// returns to the OS now instead of at the next allocation that happens to run a purge --
236/// which, on a genuinely idle process, is never.
237///
238/// Safe on any thread; a no-op on a thread that never allocated. Call it when the thread
239/// has nothing to do (an event loop about to block, a worker pool waiting on its queue),
240/// not on a hot path: it costs a few `madvise`/`DiscardVirtualMemory` calls.
241pub fn on_thread_idle() {
242    unsafe { sys::mi_on_thread_idle() }
243}
244
245/// Guard form of [`on_thread_idle`] for a thread that is about to BLOCK: hands this
246/// thread's heaps to the background scavenger, which does the idle work above while this
247/// thread sits in the kernel, and takes them back on drop.
248///
249/// Returns `None` when nothing was handed off (no scavenger running, this thread never
250/// allocated, or it is already parked). That case is deliberately NOT an inline sweep: a
251/// caller blocks far more often than it is truly idle. If this park is idle enough to
252/// afford the work, call [`on_thread_idle`] instead.
253///
254/// The thread must not allocate or free between the call and the drop -- that is the
255/// precondition that lets another thread rewrite its free lists -- which is why the guard
256/// is `!Send` and holds no data.
257#[must_use = "the park ends when the guard is dropped"]
258pub fn park_while_idle() -> Option<IdlePark> {
259    if unsafe { sys::mi_on_thread_idle_start() } {
260        Some(IdlePark {
261            _not_send: core::marker::PhantomData,
262        })
263    } else {
264        None
265    }
266}
267
268/// Returned by [`park_while_idle`]; ends the park when dropped.
269pub struct IdlePark {
270    // the park is per-thread state: `mi_on_thread_idle_end` must run on the parking thread
271    _not_send: core::marker::PhantomData<*const ()>,
272}
273
274impl Drop for IdlePark {
275    fn drop(&mut self) {
276        unsafe { sys::mi_on_thread_idle_end() }
277    }
278}
279
280/// Stop the background scavenger thread (issue #272).
281///
282/// It restarts on demand (the next [`park_while_idle`], or the next thread that
283/// initializes), so this is a way to quiesce it -- e.g. before a `fork`/`exec` that counts
284/// threads, or in a test -- not a way to disable it permanently. For that, set the
285/// `scavenger` option to 0 (`MIMALLOC_SCAVENGER=0`) before the first allocation.
286pub fn scavenger_stop() {
287    unsafe { sys::mi_scavenger_stop() }
288}
289
290/// What page hole purging has reclaimed, process wide (issue #272, Bun parity P7b).
291///
292/// Hole purging discards the memory of the free blocks sitting inside pages that are still
293/// in use, at each [`on_thread_idle`] / [`park_while_idle`] point -- without it a page stays
294/// fully resident until every block in it is free, so one long-lived object pins a whole
295/// 64 KiB/512 KiB page. These counters are the only way to see how much that gets back;
296/// they are deliberately not part of `mi_stats_t`, because the sweep also covers pages that
297/// no heap owns.
298///
299/// Most fields are monotonic. `purged_bytes`, `purged_blocks` and `unformed_bytes` are
300/// gauges ("right now"), and the three `ineligible_*` fields are a gauge over the LAST sweep
301/// only. Everything is zero when the `purge_holes` option is off (`MIMALLOC_PURGE_HOLES=0`).
302///
303/// ```
304/// # use mimalloc_pprof as mi;
305/// let before = mi::purge_holes_stats().purged_bytes_total;
306/// mi::on_thread_idle();
307/// let after = mi::purge_holes_stats().purged_bytes_total;
308/// assert!(after >= before);
309/// ```
310#[must_use]
311pub fn purge_holes_stats() -> sys::MiPurgeHolesStats {
312    let mut stats = sys::MiPurgeHolesStats::default();
313    unsafe { sys::mi_purge_holes_stats_get(&raw mut stats) };
314    stats
315}
316
317/// Exact DHAT v2 heap/lifetime profiling controls.
318///
319/// DHAT records every non-internal allocation from the moment [`start`] succeeds.
320/// It is intended for short diagnostic runs and tests rather than continuous production
321/// telemetry. The generated JSON opens in the standard Valgrind `dh_view.html` viewer.
322/// It is independent of sampled [`prof`] profiling and of `mi_memory_set_callbacks`.
323pub mod dhat {
324    use std::ffi::CString;
325    use std::io;
326    use std::path::Path;
327
328    use crate::sys;
329
330    /// Snapshot of exact DHAT collector state.
331    #[derive(Debug, Clone, Default, PartialEq, Eq)]
332    pub struct Stats {
333        pub enabled: bool,
334        /// True when raw-OS collector storage hit its configured budget or an internal
335        /// allocation failed. The application allocation still completed, but the
336        /// resulting profile is intentionally marked partial.
337        pub incomplete: bool,
338        pub total_bytes: u64,
339        pub total_blocks: u64,
340        pub live_bytes: u64,
341        pub live_blocks: u64,
342        pub peak_bytes: u64,
343        pub peak_blocks: u64,
344        pub dropped: u64,
345        pub internal_bytes: u64,
346    }
347
348    /// Start exact allocation/lifetime tracking. Returns `false` if it is already active.
349    pub fn start() -> bool {
350        unsafe { sys::mi_dhat_start() }
351    }
352
353    /// Stop observing allocation events. Retained records remain available to [`dump_file`]
354    /// so a caller can stop a measurement window before serializing its report.
355    pub fn stop() {
356        unsafe { sys::mi_dhat_stop() }
357    }
358
359    /// Whether exact DHAT tracking is currently active.
360    pub fn is_enabled() -> bool {
361        unsafe { sys::mi_dhat_is_enabled() }
362    }
363
364    /// Read the collector's exact counters. Returns a zero/default snapshot only if the
365    /// linked C library rejected the versioned ABI structure.
366    pub fn stats() -> Stats {
367        let mut raw: sys::mi_dhat_stats_t = unsafe { core::mem::zeroed() };
368        raw.size = core::mem::size_of::<sys::mi_dhat_stats_t>();
369        raw.version = sys::MI_DHAT_STATS_VERSION;
370        if unsafe { sys::mi_dhat_stats_get(&mut raw) } {
371            Stats {
372                enabled: raw.enabled,
373                incomplete: raw.incomplete,
374                total_bytes: raw.total_bytes,
375                total_blocks: raw.total_blocks,
376                live_bytes: raw.live_bytes,
377                live_blocks: raw.live_blocks,
378                peak_bytes: raw.peak_bytes,
379                peak_blocks: raw.peak_blocks,
380                dropped: raw.dropped,
381                internal_bytes: raw.internal_bytes,
382            }
383        } else {
384            Stats::default()
385        }
386    }
387
388    /// Serialize the current or stopped measurement window as a DHAT v2 JSON file.
389    pub fn dump_file(path: &Path) -> io::Result<()> {
390        let path = path
391            .to_str()
392            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "DHAT path is not UTF-8"))?;
393        let path = CString::new(path)
394            .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "DHAT path contains NUL"))?;
395        if unsafe { sys::mi_dhat_dump(path.as_ptr()) } {
396            Ok(())
397        } else {
398            Err(io::Error::last_os_error())
399        }
400    }
401}
402
403/// Turn on sampled heap profiling at the default sample rate.
404///
405/// Convenience entry point for wiring profiling to a command-line flag:
406///
407/// ```no_run
408/// # let args_profile_heap = true;
409/// if args_profile_heap {
410///     mimalloc_pprof::enable_heap_profiling();
411/// }
412/// ```
413///
414/// Uses the built-in default rate (one sample per ~512 KiB allocated;
415/// `MIMALLOC_PROF_SAMPLE_RATE` still overrides it). Call [`prof::start`]
416/// instead to pick a rate programmatically. Allocations made before this
417/// call — including process-startup and static initialization — are not
418/// tracked; profiles reflect steady-state behavior from this point on,
419/// which is the usual intent for an opt-in CLI switch. To capture startup
420/// as well, set `MIMALLOC_PROF=1` in the environment instead.
421///
422/// Returns `false` if profiling was already enabled (the earlier session,
423/// and its sample rate, stay active), or if the crate was built with
424/// `default-features = false`.
425pub fn enable_heap_profiling() -> bool {
426    prof::start(0)
427}
428
429/// How [`ProfConfig`] fields interact with the profiler's environment
430/// variables and `mi_option_*` settings.
431///
432/// Mirrors `mi_prof_config_mode_t` (include/mimalloc/profile.h); see that
433/// header for the full FALLBACK/OVERRIDE semantics, including the caveat
434/// that in `Override` mode `accum == false`, `dump_format == Text`, and
435/// `max_profiler_bytes == None` cannot be distinguished from "unset" and so
436/// always fall back to env-then-default rather than forcing the off/default
437/// value.
438#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
439pub enum ProfConfigMode {
440    /// Struct fields are used only where the corresponding env var / option is absent.
441    #[default]
442    Fallback,
443    /// Non-default struct fields win over env vars / options (see the caveat above).
444    Override,
445}
446
447/// Output format for [`ProfConfig::dump_at_exit`].
448///
449/// Mirrors `MI_PROF_FORMAT_TEXT` / `MI_PROF_FORMAT_PROTO` (include/mimalloc/profile.h).
450#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
451pub enum DumpFormat {
452    /// Legacy "heap profile:" text format (see [`prof::dump_to_vec`]).
453    #[default]
454    Text,
455    /// Binary pprof `profile.proto` format (see [`prof::dump_proto_to_vec`]).
456    Proto,
457}
458
459/// Ergonomic, Rust-facing sibling of `mi_prof_config_t`
460/// (include/mimalloc/profile.h) for [`enable_heap_profiling_with`].
461///
462/// Fields mirror the C struct one-for-one, but trade its 0/NULL-means-unset
463/// raw-integer conventions for `Option<T>` and enums where that reads
464/// better. `#[non_exhaustive]` + `Default` keeps future fields additive:
465/// build from `Default::default()` and set the fields you need, e.g.
466///
467/// ```
468/// use mimalloc_pprof::ProfConfig;
469/// let mut config = ProfConfig::default();
470/// config.sample_interval = Some(4096);
471/// ```
472///
473/// (Within this crate, struct-update syntax like
474/// `ProfConfig { sample_interval: Some(4096), ..Default::default() }` also
475/// works; `#[non_exhaustive]` only blocks struct-literal construction from
476/// *other* crates, so new fields stay non-breaking for them.)
477#[non_exhaustive]
478#[derive(Debug, Clone, Default)]
479pub struct ProfConfig {
480    /// See [`ProfConfigMode`].
481    pub mode: ProfConfigMode,
482    /// Average bytes between samples. `None` = env/default (512 KiB).
483    pub sample_interval: Option<usize>,
484    /// Budget (bytes) for profiler-internal persistent sampling state
485    /// (sample records, the stack intern table, interned stack entries).
486    /// `None` = unbudgeted (cap-bounded only).
487    pub max_profiler_bytes: Option<usize>,
488    /// `None` = nondeterministic.
489    pub seed: Option<u64>,
490    pub accum: bool,
491    /// `None` = default (32); compile cap 128.
492    pub max_stack_depth: Option<usize>,
493    /// Path to dump the profile to at process exit. `None` = no exit dump.
494    pub dump_at_exit: Option<PathBuf>,
495    /// Format used for the exit dump. Ignored if `dump_at_exit` is `None`.
496    pub dump_format: DumpFormat,
497}
498
499/// Turn on sampled heap profiling using a struct-based configuration.
500///
501/// Sibling of [`enable_heap_profiling`] for callers that need more than a
502/// single sample rate -- e.g. seeding the sampler, capping profiler-arena
503/// memory, or registering an exit-time dump path/format. See [`ProfConfig`]
504/// and, for the full FALLBACK/OVERRIDE semantics, `mi_prof_config_mode_t` in
505/// `include/mimalloc/profile.h`.
506///
507/// Returns `false` if profiling was already enabled (the earlier session
508/// stays active), if the crate was built with `default-features = false`, or
509/// if `config.dump_at_exit` is set but is not
510/// representable as a NUL-free C string (non-UTF-8 or an embedded NUL byte)
511/// -- in that case `mi_prof_start_ex` is never called.
512pub fn enable_heap_profiling_with(config: &ProfConfig) -> bool {
513    // `dump_at_exit_c` must outlive the `mi_prof_start_ex` call below since
514    // `raw.dump_at_exit` borrows its bytes; it does, as both live to the end
515    // of this function.
516    let dump_at_exit_c: Option<CString> = match &config.dump_at_exit {
517        Some(path) => match path.to_str().and_then(|s| CString::new(s).ok()) {
518            Some(c) => Some(c),
519            None => return false,
520        },
521        None => None,
522    };
523
524    let mut raw: sys::mi_prof_config_t = unsafe { core::mem::zeroed() };
525    raw.size = core::mem::size_of::<sys::mi_prof_config_t>();
526    raw.version = sys::MI_PROF_CONFIG_VERSION;
527    raw.mode = match config.mode {
528        ProfConfigMode::Fallback => sys::MI_PROF_CONFIG_FALLBACK,
529        ProfConfigMode::Override => sys::MI_PROF_CONFIG_OVERRIDE,
530    };
531    raw.sample_interval = config.sample_interval.unwrap_or(0);
532    raw.max_profiler_bytes = config.max_profiler_bytes.unwrap_or(0);
533    raw.seed = config.seed.unwrap_or(0);
534    raw.accum = config.accum;
535    raw.max_stack_depth = config.max_stack_depth.unwrap_or(0);
536    raw.dump_at_exit = dump_at_exit_c
537        .as_ref()
538        .map_or(core::ptr::null(), |c| c.as_ptr());
539    raw.dump_format = match config.dump_format {
540        DumpFormat::Text => sys::MI_PROF_FORMAT_TEXT,
541        DumpFormat::Proto => sys::MI_PROF_FORMAT_PROTO,
542    };
543
544    unsafe { sys::mi_prof_start_ex(&raw) }
545}
546
547/// Safe controls for mimalloc's sampled heap profiler.
548pub mod prof {
549    use core::ffi::{c_char, c_void};
550    use std::ffi::{CStr, CString};
551    use std::io;
552    use std::panic::{catch_unwind, AssertUnwindSafe};
553    use std::path::Path;
554
555    use crate::sys;
556
557    pub fn start(sample_rate: usize) -> bool {
558        unsafe { sys::mi_prof_start(sample_rate) }
559    }
560    #[doc(hidden)]
561    pub fn start_seeded(sample_rate: usize, seed: u64) -> bool {
562        unsafe { sys::mi_prof_start_seeded(sample_rate, seed) }
563    }
564    pub fn stop() {
565        unsafe { sys::mi_prof_stop() }
566    }
567    pub fn is_enabled() -> bool {
568        unsafe { sys::mi_prof_is_enabled() }
569    }
570    pub fn reset() {
571        unsafe { sys::mi_prof_reset() }
572    }
573
574    pub fn dump_file(path: &Path) -> io::Result<()> {
575        let path = path.to_str().ok_or_else(|| {
576            io::Error::new(io::ErrorKind::InvalidInput, "profile path is not UTF-8")
577        })?;
578        let path = CString::new(path).map_err(|_| {
579            io::Error::new(io::ErrorKind::InvalidInput, "profile path contains NUL")
580        })?;
581        if unsafe { sys::mi_prof_dump(path.as_ptr()) } {
582            Ok(())
583        } else {
584            Err(io::Error::last_os_error())
585        }
586    }
587
588    unsafe extern "C" fn write_cb(arg: *mut c_void, buf: *const c_char, len: usize) {
589        let out = &mut *(arg as *mut Vec<u8>);
590        out.extend_from_slice(core::slice::from_raw_parts(buf.cast::<u8>(), len));
591    }
592
593    /// Serialize the current heap profile without holding the profiler lock.
594    pub fn dump_to_vec() -> Vec<u8> {
595        let mut out = Vec::new();
596        let ok =
597            unsafe { sys::mi_prof_dump_writer(Some(write_cb), (&mut out as *mut Vec<u8>).cast()) };
598        if ok {
599            out
600        } else {
601            Vec::new()
602        }
603    }
604
605    /// Serialize the current heap profile as a binary pprof `profile.proto`
606    /// `Profile` message (see [google/pprof's `profile.proto`][proto]),
607    /// without holding the profiler lock.
608    ///
609    /// Sample values are pre-scaled the same way Go's `runtime/pprof` scales
610    /// legacy heap samples (the `protomem.go` convention: `alloc_objects`,
611    /// `alloc_space`, `inuse_objects`, `inuse_space`, each already corrected
612    /// for Poisson sampling bias rather than left for a downstream tool to
613    /// rescale). The `Mapping` table is included, so external symbolizers
614    /// need only the binary — no text parsing of a "heap profile:" header or
615    /// a `MAPPED_LIBRARIES:` section. This is the compact, machine-oriented
616    /// counterpart to [`dump_to_vec`]'s text format, intended for API and
617    /// transport use (issue #23) where a `pprof`-compatible tool consumes
618    /// the bytes directly.
619    ///
620    /// [proto]: https://github.com/google/pprof/blob/main/proto/profile.proto
621    pub fn dump_proto_to_vec() -> Vec<u8> {
622        let mut out = Vec::new();
623        let ok = unsafe {
624            sys::mi_prof_dump_proto_writer(Some(write_cb), (&mut out as *mut Vec<u8>).cast())
625        };
626        if ok {
627            out
628        } else {
629            Vec::new()
630        }
631    }
632
633    /// Write the current heap profile to `path` in `profile.proto` format.
634    ///
635    /// See [`dump_proto_to_vec`] for the format details.
636    pub fn dump_proto_file(path: &Path) -> io::Result<()> {
637        let path = path.to_str().ok_or_else(|| {
638            io::Error::new(io::ErrorKind::InvalidInput, "profile path is not UTF-8")
639        })?;
640        let path = CString::new(path).map_err(|_| {
641            io::Error::new(io::ErrorKind::InvalidInput, "profile path contains NUL")
642        })?;
643        if unsafe { sys::mi_prof_dump_proto(path.as_ptr()) } {
644            Ok(())
645        } else {
646            Err(io::Error::last_os_error())
647        }
648    }
649
650    /// Snapshot of `mi_prof_stats_get`'s counters, translated from the raw
651    /// sys struct into plain Rust types.
652    #[derive(Debug, Clone, Default)]
653    pub struct ProfStats {
654        pub enabled: bool,
655        pub accum: bool,
656        pub sample_rate: usize,
657        pub live_samples: usize,
658        pub live_bytes: usize,
659        pub accum_samples: usize,
660        pub accum_bytes: usize,
661        pub unique_stacks: usize,
662        pub arena_committed: usize,
663        pub stack_table_overflows: usize,
664        /// Count of ALL dropped samples (record-alloc failure, stack-intern
665        /// failure, including the stack-table cap); a superset of
666        /// `stack_table_overflows`, so `dropped_samples >=
667        /// stack_table_overflows` always.
668        pub dropped_samples: usize,
669        /// Allocator-level ("ground truth") counters, read from the mimalloc v3
670        /// engine's per-heap statistics at the time of the call. Every field
671        /// above is *sampled*; these are exact, so comparing them against
672        /// `live_bytes` measures the sampler's error directly -- which is what
673        /// makes an assertion on a sampled profile meaningful in a test.
674        pub heap: HeapStats,
675    }
676
677    /// Exact allocator counters accompanying a [`ProfStats`] reading.
678    ///
679    /// These come from mimalloc v3's per-heap statistics
680    /// (`mi_heap_stats_get`/`mi_subproc_stats_get`), which the v2 engine did not
681    /// expose. They are valid even when the profiler is stopped.
682    #[derive(Debug, Clone, Default)]
683    pub struct HeapStats {
684        /// Bytes currently committed from the OS.
685        pub committed: usize,
686        /// Bytes currently reserved from the OS (always `>= committed`).
687        pub reserved: usize,
688        /// Bytes the application actually requested and still holds.
689        ///
690        /// Only maintained when the C library was built with `MI_STAT >= 2`;
691        /// otherwise this is 0. Check [`HeapStats::detailed`] before using it.
692        pub malloc_requested: usize,
693        /// Live mimalloc pages.
694        pub pages: usize,
695        /// Pages abandoned by exited threads.
696        pub pages_abandoned: usize,
697        /// Live first-class heaps.
698        pub heaps: usize,
699        /// Live thread-local heaps. The main thread's statically-initialized
700        /// theap is not counted, so a single-threaded process reports 0.
701        pub theaps: usize,
702        /// Cumulative bytes purged back to the OS.
703        pub purged: usize,
704        /// Whether the C library was built with `MI_STAT >= 2` ("detailed"
705        /// statistics), which upstream enables by default only for debug
706        /// builds. [`HeapStats::malloc_requested`] is maintained only at that
707        /// level; every other field here is maintained at any level.
708        ///
709        /// Without this flag you cannot tell "the application allocated
710        /// nothing" from "this build does not track that counter".
711        pub detailed: bool,
712    }
713
714    /// Read the profiler's current counters via `mi_prof_stats_get`.
715    ///
716    /// Returns `ProfStats::default()` (all zero/false) if the call fails,
717    /// e.g. because the sys struct's `size`/`version` header does not match
718    /// what the linked mimalloc build expects.
719    pub fn stats() -> ProfStats {
720        let mut raw: sys::mi_prof_stats_t = unsafe { core::mem::zeroed() };
721        raw.size = core::mem::size_of::<sys::mi_prof_stats_t>();
722        raw.version = sys::MI_PROF_STAT_VERSION;
723        if unsafe { sys::mi_prof_stats_get(&mut raw) } {
724            ProfStats {
725                enabled: raw.enabled,
726                accum: raw.accum,
727                sample_rate: raw.sample_rate,
728                live_samples: raw.live_samples,
729                live_bytes: raw.live_bytes,
730                accum_samples: raw.accum_samples,
731                accum_bytes: raw.accum_bytes,
732                unique_stacks: raw.unique_stacks,
733                arena_committed: raw.arena_committed,
734                stack_table_overflows: raw.stack_table_overflows,
735                dropped_samples: raw.dropped_samples,
736                heap: HeapStats {
737                    committed: raw.heap_committed,
738                    reserved: raw.heap_reserved,
739                    malloc_requested: raw.heap_malloc_requested,
740                    pages: raw.heap_pages,
741                    pages_abandoned: raw.heap_pages_abandoned,
742                    heaps: raw.heap_count,
743                    theaps: raw.theap_count,
744                    purged: raw.heap_purged,
745                    detailed: raw.heap_stats_detailed,
746                },
747            }
748        } else {
749            ProfStats::default()
750        }
751    }
752
753    /// One sampled call stack, copied out of the profiler by [`samples`].
754    #[derive(Debug, Clone)]
755    pub struct Sample {
756        pub stack: Vec<usize>,
757        pub live_objects: usize,
758        pub live_bytes: usize,
759        pub accum_objects: usize,
760        pub accum_bytes: usize,
761    }
762
763    impl Sample {
764        /// Estimate the un-sampled byte volume behind this sample.
765        ///
766        /// Mirrors pprof's legacy heap-sample scaling formula
767        /// (`scaleHeapSample` in pprof's `profile/legacy_profile.go`),
768        /// which corrects for the bias a Poisson sampling process with mean
769        /// interval `sample_rate` introduces toward larger allocations.
770        pub fn estimated_bytes(&self, sample_rate: usize) -> u64 {
771            if self.live_objects == 0 || self.live_bytes == 0 {
772                return 0;
773            }
774            if sample_rate <= 1 {
775                return self.live_bytes as u64;
776            }
777            let avg = self.live_bytes as f64 / self.live_objects as f64;
778            let scale = 1.0 / (1.0 - (-avg / sample_rate as f64).exp());
779            (self.live_bytes as f64 * scale) as u64
780        }
781    }
782
783    /// Frees the snapshot handle on drop, including on unwind, so a panic
784    /// partway through collection never leaks profiler-arena memory.
785    struct SnapshotGuard(*mut sys::mi_prof_snapshot_t);
786
787    impl Drop for SnapshotGuard {
788        fn drop(&mut self) {
789            unsafe { sys::mi_prof_snapshot_free(self.0) }
790        }
791    }
792
793    unsafe extern "C" fn collect_visitor(
794        info: *const sys::mi_prof_sample_info_t,
795        arg: *mut c_void,
796    ) -> bool {
797        let result = catch_unwind(AssertUnwindSafe(|| unsafe {
798            let out = &mut *(arg as *mut Vec<Sample>);
799            let info = &*info;
800            let stack = (0..info.depth)
801                .map(|i| *info.stack.add(i) as usize)
802                .collect();
803            out.push(Sample {
804                stack,
805                live_objects: info.live_objects,
806                live_bytes: info.live_bytes,
807                accum_objects: info.accum_objects,
808                accum_bytes: info.accum_bytes,
809            });
810        }));
811        result.is_ok()
812    }
813
814    /// Collect a point-in-time copy of every live sampled stack.
815    ///
816    /// This snapshots under the profiler lock via `mi_prof_snapshot_new`,
817    /// then walks and frees the snapshot outside that lock. Using
818    /// `mi_prof_visit` directly here would run the (allocating) collection
819    /// below from inside the visitor while the profiler lock is held,
820    /// risking reentrant profiler-hook allocation and deadlock — the
821    /// reentrancy hazard the snapshot API exists to avoid (issue #2,
822    /// decisions 11-13).
823    pub fn samples() -> Vec<Sample> {
824        let snap = unsafe { sys::mi_prof_snapshot_new() };
825        if snap.is_null() {
826            return Vec::new();
827        }
828        let guard = SnapshotGuard(snap);
829        let mut out: Vec<Sample> = Vec::new();
830        unsafe {
831            sys::mi_prof_snapshot_visit(
832                guard.0,
833                collect_visitor,
834                (&mut out as *mut Vec<Sample>).cast(),
835            );
836        }
837        out
838    }
839
840    /// One loaded module (shared library or the main executable), copied out
841    /// of the OS module list by [`modules`].
842    #[derive(Debug, Clone)]
843    pub struct ModuleInfo {
844        pub path: String,
845        pub base: usize,
846        pub size: usize,
847    }
848
849    unsafe extern "C" fn modules_visitor(
850        info: *const sys::mi_prof_module_info_t,
851        arg: *mut c_void,
852    ) -> bool {
853        let result = catch_unwind(AssertUnwindSafe(|| unsafe {
854            let out = &mut *(arg as *mut Vec<ModuleInfo>);
855            let info = &*info;
856            // `info.path` is only valid for the duration of this callback (it
857            // points into OS-owned module-list storage), so it must be copied
858            // into an owned `String` right here rather than stashed for later.
859            let path = CStr::from_ptr(info.path).to_string_lossy().into_owned();
860            out.push(ModuleInfo {
861                path,
862                base: info.base,
863                size: info.size,
864            });
865        }));
866        result.is_ok()
867    }
868
869    /// Enumerate the process's loaded modules (shared libraries and the main
870    /// executable), e.g. to build pprof `Mapping` entries yourself.
871    ///
872    /// Unlike [`samples`]'s `collect_visitor`, this callback is free to
873    /// allocate: `mi_prof_modules_visit` never takes the profiler lock (the
874    /// module list is OS-owned, not part of the sampled-allocation table), so
875    /// there is no reentrant-allocation-under-the-lock hazard here.
876    pub fn modules() -> Vec<ModuleInfo> {
877        let mut out: Vec<ModuleInfo> = Vec::new();
878        unsafe {
879            sys::mi_prof_modules_visit(modules_visitor, (&mut out as *mut Vec<ModuleInfo>).cast());
880        }
881        out
882    }
883}
884
885/// Print, per size class, what hole purging leaves behind (issue #272, Bun parity P7b).
886///
887/// Read-only: it purges nothing and mutates no free list. The report goes to mimalloc's
888/// own output sink (stderr by default), not to a returned `String` — building a `String`
889/// here would allocate from inside a walk over the very free lists being reported.
890/// `mi_purge_holes_report` takes no sink argument at all; to capture the text, install a
891/// process-wide sink with C's `mi_register_output` (not bound by this crate).
892///
893/// Like the idle sweep it only covers what the calling thread may safely read: its own
894/// theaps, plus the abandoned pages of the heaps behind them. Call it right after an
895/// [`on_thread_idle`] sweep, when the numbers still describe that sweep.
896pub fn purge_holes_report() {
897    unsafe { sys::mi_purge_holes_report() }
898}
899
900/// mimalloc's `mi_option_*` settings: the runtime knobs behind every `MIMALLOC_*`
901/// environment variable.
902///
903/// Options are read once, lazily, the first time the allocator needs them, so setting one
904/// after the allocation it governs has already happened has no effect. In particular
905/// [`Opt::SCAVENGER`] and the profiler options must be set before the first allocation to
906/// matter; [`Opt::PURGE_HOLES`] and its companions are re-read per sweep and can be
907/// changed at any time.
908///
909/// ```
910/// use mimalloc_pprof::options::{self, Opt};
911/// let previous = options::get(Opt::PURGE_HOLES_MIN_INTERVAL);
912/// options::set(Opt::PURGE_HOLES_MIN_INTERVAL, 0); // sweep on every idle call
913/// mimalloc_pprof::on_thread_idle();
914/// options::set(Opt::PURGE_HOLES_MIN_INTERVAL, previous);
915/// ```
916pub mod options {
917    use core::ffi::c_long;
918
919    use crate::sys;
920
921    /// One `mi_option_t` setting.
922    ///
923    /// The associated constants name this fork's own options plus the handful of upstream
924    /// ones that interact with them; [`Opt::from_raw`] reaches any other enumerator in
925    /// [`sys`] — it range-checks against `_mi_option_last`, because the C side indexes an
926    /// array with this value and an out-of-range option would read out of bounds.
927    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
928    pub struct Opt(sys::mi_option_t);
929
930    impl Opt {
931        /// **Fork addition.** Enable the sampled profiler at process start (`MIMALLOC_PROF`).
932        pub const PROF: Self = Self(sys::mi_option_prof);
933        /// **Fork addition.** Average byte interval between profiler samples.
934        pub const PROF_SAMPLE_RATE: Self = Self(sys::mi_option_prof_sample_rate);
935        /// **Fork addition.** Maximum captured stack depth for the profiler.
936        pub const PROF_BT_MAX: Self = Self(sys::mi_option_prof_bt_max);
937        /// **Fork addition.** Keep cumulative profiler counters until [`crate::prof::reset`].
938        pub const PROF_ACCUM: Self = Self(sys::mi_option_prof_accum);
939        /// **Fork addition.** Profiler sampling PRNG seed; 0 = nondeterministic.
940        pub const PROF_SEED: Self = Self(sys::mi_option_prof_seed);
941        /// **Fork addition.** Budget in bytes for profiler-internal arena memory.
942        pub const PROF_MAX_BYTES: Self = Self(sys::mi_option_prof_max_bytes);
943        /// **Fork addition.** Enable [`crate::memory_events`] accounting
944        /// (`MIMALLOC_MEMORY_EVENTS`).
945        pub const MEMORY_EVENTS: Self = Self(sys::mi_option_memory_events);
946        /// **Fork addition, dead since #80.** Parses, but has no effect. Kept so nothing
947        /// renumbers; unrelated to [`Opt::PURGE_HOLES_EAGER_ZERO`].
948        pub const PURGE_ZEROES: Self = Self(sys::mi_option_purge_zeroes);
949        /// **Fork addition (Bun).** Run the background scavenger thread.
950        pub const SCAVENGER: Self = Self(sys::mi_option_scavenger);
951        /// **Fork addition (Bun).** Discard free blocks inside still-used pages on
952        /// [`crate::on_thread_idle`].
953        pub const PURGE_HOLES: Self = Self(sys::mi_option_purge_holes);
954        /// **Fork addition (Bun).** Zero a range before discarding it, so a mis-scoped
955        /// discard corrupts visibly. Forced on in debug builds.
956        pub const PURGE_HOLES_EAGER_ZERO: Self = Self(sys::mi_option_purge_holes_eager_zero);
957        /// **Fork addition (Bun).** Minimum milliseconds between sweeps of one thread's heaps.
958        pub const PURGE_HOLES_MIN_INTERVAL: Self = Self(sys::mi_option_purge_holes_min_interval);
959        /// **Fork addition (Bun).** Every N-th sweep walks every page; 0 disables.
960        pub const PURGE_HOLES_FULL_EVERY: Self = Self(sys::mi_option_purge_holes_full_every);
961
962        /// Upstream: milliseconds to delay purging, which the scavenger also honours.
963        pub const PURGE_DELAY: Self = Self(sys::mi_option_purge_delay);
964        /// Upstream: print statistics on process termination.
965        pub const SHOW_STATS: Self = Self(sys::mi_option_show_stats);
966        /// Upstream: print error messages.
967        pub const SHOW_ERRORS: Self = Self(sys::mi_option_show_errors);
968        /// Upstream: print verbose messages.
969        pub const VERBOSE: Self = Self(sys::mi_option_verbose);
970
971        /// Wrap a raw `mi_option_t` from [`sys`], or `None` if it is not a real option.
972        ///
973        /// The range check is load bearing: the C implementation indexes its option table
974        /// with this value, so an out-of-range option is an out-of-bounds read.
975        #[must_use]
976        pub fn from_raw(raw: sys::mi_option_t) -> Option<Self> {
977            if (0..sys::_mi_option_last).contains(&raw) {
978                Some(Self(raw))
979            } else {
980                None
981            }
982        }
983
984        /// The raw `mi_option_t` value.
985        #[must_use]
986        pub fn as_raw(self) -> sys::mi_option_t {
987            self.0
988        }
989
990        /// The C enumerator's name, e.g. `mi_option_purge_holes`.
991        #[must_use]
992        pub fn name(self) -> &'static str {
993            sys::MI_OPTIONS_IN_ORDER
994                .get(self.0 as usize)
995                .map_or("<unknown>", |(name, _)| *name)
996        }
997    }
998
999    /// Read an option's value.
1000    ///
1001    /// Note the width: mimalloc stores option values in a C `long`, which is 32-bit on
1002    /// Windows and 64-bit on Linux/macOS. Use [`get_size`] for byte counts.
1003    #[must_use]
1004    pub fn get(option: Opt) -> c_long {
1005        unsafe { sys::mi_option_get(option.as_raw()) }
1006    }
1007
1008    /// Read an option's value, clamped into `min..=max`.
1009    #[must_use]
1010    pub fn get_clamp(option: Opt, min: c_long, max: c_long) -> c_long {
1011        unsafe { sys::mi_option_get_clamp(option.as_raw(), min, max) }
1012    }
1013
1014    /// Read an option's value as a `size_t`, for options that count bytes.
1015    #[must_use]
1016    pub fn get_size(option: Opt) -> usize {
1017        unsafe { sys::mi_option_get_size(option.as_raw()) }
1018    }
1019
1020    /// Set an option's value, overriding both the default and the environment.
1021    pub fn set(option: Opt, value: c_long) {
1022        unsafe { sys::mi_option_set(option.as_raw(), value) }
1023    }
1024
1025    /// Set an option's value only if the environment did not already set it.
1026    pub fn set_default(option: Opt, value: c_long) {
1027        unsafe { sys::mi_option_set_default(option.as_raw(), value) }
1028    }
1029
1030    /// Whether a boolean option is on.
1031    #[must_use]
1032    pub fn is_enabled(option: Opt) -> bool {
1033        unsafe { sys::mi_option_is_enabled(option.as_raw()) }
1034    }
1035
1036    /// Turn a boolean option on.
1037    pub fn enable(option: Opt) {
1038        unsafe { sys::mi_option_enable(option.as_raw()) }
1039    }
1040
1041    /// Turn a boolean option off.
1042    pub fn disable(option: Opt) {
1043        unsafe { sys::mi_option_disable(option.as_raw()) }
1044    }
1045
1046    /// Turn a boolean option on or off.
1047    pub fn set_enabled(option: Opt, enabled: bool) {
1048        unsafe { sys::mi_option_set_enabled(option.as_raw(), enabled) }
1049    }
1050
1051    /// Set a boolean option's default, which the environment still overrides.
1052    pub fn set_enabled_default(option: Opt, enabled: bool) {
1053        unsafe { sys::mi_option_set_enabled_default(option.as_raw(), enabled) }
1054    }
1055
1056    /// Print every option's current value to mimalloc's output sink.
1057    ///
1058    /// Goes to the sink rather than to a returned `String` for the same reason as
1059    /// [`crate::purge_holes_report`]: capturing it would mean allocating from inside a
1060    /// callback the allocator drives.
1061    pub fn print() {
1062        unsafe { sys::mi_options_print_out(None, core::ptr::null_mut()) }
1063    }
1064
1065    /// Every option this build knows about, in C declaration order, as
1066    /// `(name, value)` pairs — including the ones without an [`Opt`] constant.
1067    #[must_use]
1068    pub fn all() -> &'static [(&'static str, sys::mi_option_t)] {
1069        sys::MI_OPTIONS_IN_ORDER
1070    }
1071}
1072
1073/// The allocator's own **exact** statistics, as opposed to the sampled numbers
1074/// [`crate::prof::stats`] reports.
1075///
1076/// This is upstream mimalloc's `mimalloc-stats.h` surface. Note what is *not* here:
1077/// hole-purging and idle-sweep gauges are **not** part of `mi_stats_t` — they live in
1078/// [`crate::purge_holes_stats`], because the sweep also covers pages that no heap owns
1079/// and `mi_stats_t` cannot grow (it is embedded in a theap, at the meta-allocator's 8 KB
1080/// block limit).
1081///
1082/// `malloc_requested` is only maintained when the C library was built with `MI_STAT >= 2`
1083/// (upstream enables that for debug builds only); a default release build reports 0 for
1084/// it and for nothing else.
1085pub mod stats {
1086    use core::ops::Deref;
1087    use std::ffi::CStr;
1088
1089    use crate::sys;
1090
1091    /// An owned copy of `mi_stats_t`, boxed because it is ~4 KB.
1092    ///
1093    /// Deref to reach every counter, e.g. `stats.committed.current`.
1094    #[derive(Clone, Debug)]
1095    pub struct Stats(Box<sys::mi_stats_t>);
1096
1097    impl Deref for Stats {
1098        type Target = sys::mi_stats_t;
1099        fn deref(&self) -> &Self::Target {
1100            &self.0
1101        }
1102    }
1103
1104    impl Stats {
1105        /// Render these counters as mimalloc's statistics JSON.
1106        ///
1107        /// Returns `None` on allocation failure. Wraps `mi_stats_as_json`.
1108        #[must_use]
1109        pub fn to_json(&self) -> Option<String> {
1110            // `mi_stats_as_json` takes a non-const pointer but only reads through it.
1111            let mut copy = self.0.clone();
1112            let ptr = unsafe { sys::mi_stats_as_json(&raw mut *copy, 0, core::ptr::null_mut()) };
1113            take_c_string(ptr)
1114        }
1115
1116        /// The raw C struct.
1117        #[must_use]
1118        pub fn as_raw(&self) -> &sys::mi_stats_t {
1119            &self.0
1120        }
1121    }
1122
1123    /// A zeroed `mi_stats_t` with its `size`/`version` header filled in, which every
1124    /// `*_stats_get` entry point checks before writing a single counter.
1125    fn empty() -> Box<sys::mi_stats_t> {
1126        let mut raw: Box<sys::mi_stats_t> = Box::new(unsafe { core::mem::zeroed() });
1127        raw.size = size_of::<sys::mi_stats_t>();
1128        raw.version = sys::MI_STAT_VERSION;
1129        raw
1130    }
1131
1132    /// Copy a `mi_malloc`-family C string out and release it with `mi_free`.
1133    fn take_c_string(ptr: *mut core::ffi::c_char) -> Option<String> {
1134        if ptr.is_null() {
1135            return None;
1136        }
1137        let owned = unsafe { CStr::from_ptr(ptr) }
1138            .to_string_lossy()
1139            .into_owned();
1140        unsafe { sys::mi_free(ptr.cast()) };
1141        Some(owned)
1142    }
1143
1144    /// Which subprocess a subprocess-scoped call refers to.
1145    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1146    pub enum Subproc {
1147        /// The process-wide default subprocess (`mi_subproc_main`).
1148        #[default]
1149        Main,
1150        /// The subprocess this thread belongs to (`mi_subproc_current`).
1151        Current,
1152    }
1153
1154    impl Subproc {
1155        fn id(self) -> sys::mi_subproc_id_t {
1156            unsafe {
1157                match self {
1158                    Self::Main => sys::mi_subproc_main(),
1159                    Self::Current => sys::mi_subproc_current(),
1160                }
1161            }
1162        }
1163    }
1164
1165    /// Statistics for the current subprocess and all its heaps, aggregated.
1166    ///
1167    /// Wraps `mi_stats_get`. Returns `None` only if the C library rejects the struct
1168    /// header, which would mean this crate's `mi_stats_t` mirror has drifted from the
1169    /// library it is linked against (`tests/t19_layout.rs` gates exactly that).
1170    #[must_use]
1171    pub fn get() -> Option<Stats> {
1172        let mut raw = empty();
1173        unsafe { sys::mi_stats_get(&raw mut *raw) }.then_some(Stats(raw))
1174    }
1175
1176    /// The same statistics as [`get`], rendered as JSON by the C library.
1177    ///
1178    /// Wraps `mi_stats_get_json`; returns `None` on allocation failure.
1179    #[must_use]
1180    pub fn json() -> Option<String> {
1181        take_c_string(unsafe { sys::mi_stats_get_json(0, core::ptr::null_mut()) })
1182    }
1183
1184    /// Print the current subprocess's statistics to mimalloc's output sink.
1185    ///
1186    /// Wraps `mi_stats_print_out(NULL, NULL)`. Use [`json`] to capture them instead:
1187    /// routing the sink through a Rust closure would allocate from inside a callback the
1188    /// allocator drives.
1189    pub fn print() {
1190        unsafe { sys::mi_stats_print_out(None, core::ptr::null_mut()) }
1191    }
1192
1193    /// The block size served by size bin `bin` (`0..=`[`sys::MI_BIN_HUGE`]), matching the
1194    /// `malloc_bins`/`page_bins` indices.
1195    #[must_use]
1196    pub fn bin_size(bin: usize) -> usize {
1197        unsafe { sys::mi_stats_get_bin_size(bin) }
1198    }
1199
1200    /// Statistics for one subprocess and all its heaps, aggregated.
1201    #[must_use]
1202    pub fn subproc_get(which: Subproc) -> Option<Stats> {
1203        let mut raw = empty();
1204        unsafe { sys::mi_subproc_stats_get(which.id(), &raw mut *raw) }.then_some(Stats(raw))
1205    }
1206
1207    /// Statistics for one subprocess **without** aggregating its heaps.
1208    #[must_use]
1209    pub fn subproc_get_exclusive(which: Subproc) -> Option<Stats> {
1210        let mut raw = empty();
1211        unsafe { sys::mi_subproc_stats_get_exclusive(which.id(), &raw mut *raw) }
1212            .then_some(Stats(raw))
1213    }
1214
1215    /// One subprocess's aggregated statistics as JSON.
1216    #[must_use]
1217    pub fn subproc_json(which: Subproc) -> Option<String> {
1218        take_c_string(unsafe {
1219            sys::mi_subproc_stats_get_json(which.id(), 0, core::ptr::null_mut())
1220        })
1221    }
1222
1223    /// Print one subprocess's aggregated statistics to mimalloc's output sink.
1224    pub fn subproc_print(which: Subproc) {
1225        unsafe { sys::mi_subproc_stats_print_out(which.id(), None, core::ptr::null_mut()) }
1226    }
1227
1228    /// Print one subprocess **and each of its heaps separately** to mimalloc's output sink.
1229    pub fn subproc_heap_print(which: Subproc) {
1230        unsafe {
1231            sys::mi_subproc_heap_stats_print_out(which.id(), None, core::ptr::null_mut());
1232        }
1233    }
1234}
1235
1236/// Opt-in allocation-change accounting and callbacks (`include/mimalloc/memory-events.h`).
1237///
1238/// Independent of the sampled profiler: this module is compiled into the C library in
1239/// every configuration, including `default-features = false`. Tracking is **off** by
1240/// default; while it is off every allocate/free/realloc pays for exactly one relaxed flag
1241/// check and nothing else.
1242///
1243/// ```
1244/// use mimalloc_pprof::{memory_events, MiMalloc};
1245///
1246/// // The counters only move for allocations that actually reach mimalloc, so this
1247/// // example is only meaningful once mimalloc is the global allocator.
1248/// #[global_allocator]
1249/// static ALLOCATOR: MiMalloc = MiMalloc;
1250///
1251/// fn main() {
1252///     memory_events::set_enabled(true);
1253///     let before = memory_events::snapshot().expect("snapshot").accum_count;
1254///     let v = vec![0_u8; 4096];
1255///     std::hint::black_box(&v);
1256///     let after = memory_events::snapshot().expect("snapshot").accum_count;
1257///     assert!(after > before);
1258///     memory_events::set_enabled(false);
1259/// }
1260/// ```
1261pub mod memory_events {
1262    use core::ffi::c_void;
1263    use std::panic::{catch_unwind, AssertUnwindSafe};
1264
1265    use crate::sys;
1266
1267    /// Which kind of change a [`Change`] describes.
1268    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1269    #[non_exhaustive]
1270    pub enum ChangeKind {
1271        /// A successful allocation.
1272        Allocate,
1273        /// A successful free.
1274        Free,
1275        /// A successful realloc, whether it grew or shrank.
1276        Resize,
1277    }
1278
1279    impl ChangeKind {
1280        fn from_raw(raw: sys::mi_memory_change_kind_t) -> Option<Self> {
1281            match raw {
1282                sys::MI_MEMORY_ALLOCATE => Some(Self::Allocate),
1283                sys::MI_MEMORY_FREE => Some(Self::Free),
1284                sys::MI_MEMORY_RESIZE => Some(Self::Resize),
1285                _ => None,
1286            }
1287        }
1288
1289        fn slot(self) -> usize {
1290            match self {
1291                Self::Allocate => sys::MI_MEMORY_ALLOCATE as usize,
1292                Self::Free => sys::MI_MEMORY_FREE as usize,
1293                Self::Resize => sys::MI_MEMORY_RESIZE as usize,
1294            }
1295        }
1296    }
1297
1298    /// One observed allocation change, copied out of `mi_memory_change_t`.
1299    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1300    pub struct Change {
1301        /// What happened.
1302        pub kind: ChangeKind,
1303        /// Tracked global live usable bytes after this operation.
1304        pub total_bytes: u64,
1305        /// Signed change in tracked live usable bytes: positive for allocation/growth,
1306        /// negative for free/shrink, zero for a same-size-class resize.
1307        pub delta_bytes: i64,
1308        /// Caller-requested size for allocate and resize; zero for free.
1309        pub request_size: u64,
1310    }
1311
1312    /// Running totals maintained while tracking is enabled.
1313    ///
1314    /// Counters are **not** reconstructed for time spent with tracking off: a total is
1315    /// exact only if tracking was enabled before the first allocation and never disabled.
1316    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1317    pub struct Snapshot {
1318        /// Tracked live usable bytes right now.
1319        pub live_bytes: u64,
1320        /// Cumulative usable bytes ever allocated.
1321        pub accum_bytes: u64,
1322        /// Tracked live allocation count right now.
1323        pub live_count: u64,
1324        /// Cumulative count of successful allocate events.
1325        pub accum_count: u64,
1326    }
1327
1328    /// Enable or disable tracking; returns the previous state.
1329    ///
1330    /// An explicit call is always authoritative over the `MIMALLOC_MEMORY_EVENTS`
1331    /// environment read: called before the first allocation it *replaces* that read;
1332    /// called after, it overrides the cached flag. Re-enabling does not reconstruct what
1333    /// happened while tracking was off.
1334    pub fn set_enabled(enabled: bool) -> bool {
1335        unsafe { sys::mi_memory_tracking_set_enabled(enabled) }
1336    }
1337
1338    /// Whether tracking is on.
1339    #[must_use]
1340    pub fn is_enabled() -> bool {
1341        unsafe { sys::mi_memory_tracking_is_enabled() }
1342    }
1343
1344    /// Read the running totals.
1345    ///
1346    /// Returns `None` only if the C library rejects the struct header, which would mean
1347    /// this crate's mirror has drifted from the library (`tests/t19_layout.rs` gates that).
1348    #[must_use]
1349    pub fn snapshot() -> Option<Snapshot> {
1350        let mut raw: sys::mi_memory_snapshot_t = unsafe { core::mem::zeroed() };
1351        raw.size = size_of::<sys::mi_memory_snapshot_t>();
1352        raw.version = sys::MI_MEMORY_SNAPSHOT_VERSION;
1353        if !unsafe { sys::mi_memory_snapshot(&raw mut raw) } {
1354            return None;
1355        }
1356        Some(Snapshot {
1357            live_bytes: raw.live_bytes,
1358            accum_bytes: raw.accum_bytes,
1359            live_count: raw.live_count,
1360            accum_count: raw.accum_count,
1361        })
1362    }
1363
1364    /// Handlers to install with [`set_callbacks`], one per [`ChangeKind`].
1365    ///
1366    /// Plain `fn` pointers rather than closures on purpose: the C side keeps the
1367    /// registration until it is replaced, so anything captured would have to outlive the
1368    /// process. Route per-instance state through a `static` (an atomic counter, a channel
1369    /// sender in a `OnceLock`) instead.
1370    #[derive(Clone, Copy, Debug, Default)]
1371    pub struct Callbacks {
1372        /// Called after each successful allocation.
1373        pub allocate: Option<fn(&Change)>,
1374        /// Called after each successful free.
1375        pub free: Option<fn(&Change)>,
1376        /// Called after each successful realloc.
1377        pub resize: Option<fn(&Change)>,
1378    }
1379
1380    impl Callbacks {
1381        fn handler(&self, kind: ChangeKind) -> Option<fn(&Change)> {
1382            match kind {
1383                ChangeKind::Allocate => self.allocate,
1384                ChangeKind::Free => self.free,
1385                ChangeKind::Resize => self.resize,
1386            }
1387        }
1388    }
1389
1390    /// The single C-ABI entry point for all three slots. `arg` is the `&'static Callbacks`
1391    /// the caller handed to [`set_callbacks`]; the kind comes out of the change record, so
1392    /// one trampoline serves every slot.
1393    unsafe extern "C" fn dispatch(change: *const sys::mi_memory_change_t, arg: *mut c_void) {
1394        if change.is_null() || arg.is_null() {
1395            return;
1396        }
1397        let raw = unsafe { &*change };
1398        let callbacks = unsafe { &*(arg as *const Callbacks) };
1399        // An unknown kind means the C enum grew: ignore it rather than guessing.
1400        let Some(kind) = ChangeKind::from_raw(raw.kind) else {
1401            return;
1402        };
1403        let Some(handler) = callbacks.handler(kind) else {
1404            return;
1405        };
1406        let change = Change {
1407            kind,
1408            total_bytes: raw.total_bytes,
1409            delta_bytes: raw.delta_bytes,
1410            request_size: raw.request_size,
1411        };
1412        // A panic must not unwind across the C frame that called us.
1413        let _ = catch_unwind(AssertUnwindSafe(|| handler(&change)));
1414    }
1415
1416    /// Install `callbacks`, replacing any previous table. Returns `false` if the C library
1417    /// refused the table.
1418    ///
1419    /// `'static` is what makes this safe: the C side keeps the pointer until the table is
1420    /// replaced or cleared, which is exactly the header's "`arg` pointers are caller-owned
1421    /// and must stay valid" requirement.
1422    ///
1423    /// Callbacks run with no allocator locks held and **may** allocate, but a hook that
1424    /// fires while another hook's callback is running on the same thread is suppressed —
1425    /// so bytes a callback itself allocates never reach the running totals. Keep them
1426    /// short, and let them return normally: a panic is caught and swallowed here, but a
1427    /// C `longjmp` out of one is unsupported.
1428    pub fn set_callbacks(callbacks: &'static Callbacks) -> bool {
1429        let mut raw = sys::mi_memory_callbacks_t {
1430            handlers: [None; sys::MI_MEMORY_CHANGE_COUNT],
1431            args: [core::ptr::null_mut(); sys::MI_MEMORY_CHANGE_COUNT],
1432        };
1433        let arg = (callbacks as *const Callbacks).cast_mut().cast::<c_void>();
1434        for kind in [ChangeKind::Allocate, ChangeKind::Free, ChangeKind::Resize] {
1435            if callbacks.handler(kind).is_some() {
1436                raw.handlers[kind.slot()] = Some(dispatch);
1437                raw.args[kind.slot()] = arg;
1438            }
1439        }
1440        unsafe { sys::mi_memory_set_callbacks(&raw const raw) }
1441    }
1442
1443    /// Remove every installed callback. Accounting (and [`snapshot`]) keeps working.
1444    pub fn clear_callbacks() -> bool {
1445        unsafe { sys::mi_memory_set_callbacks(core::ptr::null()) }
1446    }
1447
1448    unsafe extern "C" fn visit_trampoline<F>(
1449        allocation: *mut c_void,
1450        usable_size: usize,
1451        arg: *mut c_void,
1452    ) -> bool
1453    where
1454        F: FnMut(*mut u8, usize) -> bool,
1455    {
1456        let visitor = unsafe { &mut *(arg as *mut F) };
1457        catch_unwind(AssertUnwindSafe(|| visitor(allocation.cast(), usable_size))).unwrap_or(false)
1458    }
1459
1460    /// Walk the live allocations this thread may safely observe, calling `visitor` with
1461    /// each one's address and usable size. Return `false` from `visitor` to stop early.
1462    ///
1463    /// Diagnostics only. This is **not** a consistent global snapshot: it is built on
1464    /// `mi_heap_visit_blocks`, so another thread may free a reported allocation the
1465    /// instant the callback begins.
1466    ///
1467    /// # Safety
1468    ///
1469    /// - `visitor` must not allocate, free, or otherwise reenter mimalloc while the walk
1470    ///   is active — that includes anything that allocates indirectly, such as `println!`,
1471    ///   growing a `Vec`, or formatting. Collect into a fixed-size buffer, or into
1472    ///   [`crate::unwrapped_malloc`] memory, and process it after this returns.
1473    /// - `visitor` must not panic. Raising a panic allocates its payload and its message
1474    ///   through the global allocator, which reenters mimalloc in the middle of the walk
1475    ///   -- the very thing the bullet above forbids. The `catch_unwind` inside the
1476    ///   trampoline stops the unwind from crossing the C frame; it does **not** and
1477    ///   cannot prevent that allocation, which has already happened by the time it runs.
1478    ///   Report failures by setting a flag the caller reads after the walk returns.
1479    /// - The pointers handed to `visitor` must not be dereferenced, retained, or freed:
1480    ///   they may already be dead. Treat them as addresses, not as references.
1481    /// - No other thread may be freeing into the heaps being walked (the
1482    ///   `mi_heap_visit_blocks` precondition; see `include/mimalloc.h`).
1483    pub unsafe fn visit_live_allocations<F>(mut visitor: F) -> bool
1484    where
1485        F: FnMut(*mut u8, usize) -> bool,
1486    {
1487        unsafe {
1488            sys::mi_memory_visit_live_allocations(
1489                visit_trampoline::<F>,
1490                (&raw mut visitor).cast::<c_void>(),
1491            )
1492        }
1493    }
1494}
1495
1496#[cfg(test)]
1497mod tests {
1498    use super::*;
1499    use std::sync::Mutex;
1500
1501    // The profiler is process-global state, and unit tests within this
1502    // binary may run concurrently by default, so serialize everything that
1503    // starts/stops it. `unwrap_or_else` rides through a poisoned lock rather
1504    // than cascading a single panicking test into every other one.
1505    #[cfg(feature = "pprof")]
1506    static PROF_TEST_LOCK: Mutex<()> = Mutex::new(());
1507    static DHAT_TEST_LOCK: Mutex<()> = Mutex::new(());
1508
1509    #[cfg(feature = "pprof")]
1510    fn reset_profiler() {
1511        if prof::is_enabled() {
1512            prof::stop();
1513        }
1514    }
1515
1516    #[test]
1517    #[cfg(feature = "pprof")]
1518    fn enable_heap_profiling_with_default_config_starts_profiler() {
1519        let _guard = PROF_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1520        reset_profiler();
1521
1522        let config = ProfConfig::default();
1523        assert!(enable_heap_profiling_with(&config));
1524        assert!(prof::is_enabled());
1525
1526        prof::stop();
1527    }
1528
1529    #[test]
1530    #[cfg(feature = "pprof")]
1531    fn enable_heap_profiling_with_override_mode_sets_sample_interval() {
1532        let _guard = PROF_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1533        reset_profiler();
1534
1535        let config = ProfConfig {
1536            mode: ProfConfigMode::Override,
1537            sample_interval: Some(4096),
1538            ..Default::default()
1539        };
1540        assert!(enable_heap_profiling_with(&config));
1541        assert!(prof::is_enabled());
1542        assert_eq!(prof::stats().sample_rate, 4096);
1543
1544        prof::stop();
1545    }
1546
1547    #[test]
1548    #[cfg(not(feature = "pprof"))]
1549    fn heap_profiling_is_unavailable_when_compiled_out() {
1550        assert!(!enable_heap_profiling_with(&ProfConfig::default()));
1551        assert!(!prof::is_enabled());
1552    }
1553
1554    #[test]
1555    fn unwrapped_malloc_write_realloc_grow_verify_free() {
1556        unsafe {
1557            let size = 64usize;
1558            let p = unwrapped_malloc(size, 0);
1559            assert!(!p.is_null());
1560
1561            for i in 0..size {
1562                *p.add(i) = (i % 256) as u8;
1563            }
1564
1565            let new_size = 256usize;
1566            let p2 = unwrapped_realloc(p, new_size, 0);
1567            assert!(!p2.is_null());
1568
1569            for i in 0..size {
1570                assert_eq!(*p2.add(i), (i % 256) as u8);
1571            }
1572
1573            unwrapped_free(p2);
1574        }
1575    }
1576
1577    #[test]
1578    fn unwrapped_free_null_is_noop() {
1579        unsafe {
1580            unwrapped_free(core::ptr::null_mut());
1581        }
1582    }
1583
1584    #[test]
1585    fn unwrapped_malloc_rejects_non_power_of_two_alignment() {
1586        unsafe {
1587            let p = unwrapped_malloc(16, 3);
1588            assert!(p.is_null());
1589        }
1590    }
1591
1592    #[test]
1593    fn unwrapped_realloc_with_null_ptr_behaves_like_malloc() {
1594        unsafe {
1595            let p = unwrapped_realloc(core::ptr::null_mut(), 32, 0);
1596            assert!(!p.is_null());
1597            unwrapped_free(p);
1598        }
1599    }
1600
1601    #[test]
1602    fn unwrapped_realloc_with_zero_size_frees_and_returns_null() {
1603        unsafe {
1604            let p = unwrapped_malloc(32, 0);
1605            assert!(!p.is_null());
1606            let p2 = unwrapped_realloc(p, 0, 0);
1607            assert!(p2.is_null());
1608        }
1609    }
1610
1611    #[test]
1612    fn dhat_controls_report_lifecycle() {
1613        let _guard = DHAT_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1614        if dhat::is_enabled() {
1615            dhat::stop();
1616        }
1617        assert!(dhat::start());
1618        let active = dhat::stats();
1619        assert!(active.enabled);
1620        dhat::stop();
1621        assert!(!dhat::is_enabled());
1622        assert!(!dhat::stats().enabled);
1623    }
1624    #[test]
1625    fn heap_dump_json_reports_well_formed_json_with_current_heap() {
1626        // The default/main heap always has at least one live allocation by the time any
1627        // Rust test runs (the runtime itself allocates), so a pages-only dump of the
1628        // current subprocess must come back non-empty and syntactically balanced.
1629        let json = heap_dump_json(false, false).expect("heap_dump_json should not fail");
1630        assert!(json.contains("\"heaps\""));
1631        assert!(!json.contains("\"blocks\""));
1632        let opens = json.matches('{').count();
1633        let closes = json.matches('}').count();
1634        assert_eq!(opens, closes);
1635
1636        let with_blocks = heap_dump_json(true, true).expect("heap_dump_json should not fail");
1637        assert!(with_blocks.contains("\"blocks\""));
1638    }
1639}