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