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}