mimalloc-pprof 1.0.0

mimalloc global allocator with pprof-compatible sampled heap profiling
Documentation

mimalloc-pprof

mimalloc with native pprof-compatible heap profiling — on Windows, Linux, and macOS alike.

The one mimalloc heap profiler that runs natively on Windows. Upstream mimalloc has no profiler at all, and the only other known implementation (Bun's) is POSIX-only — its stack capture is guarded behind glibc/Apple <execinfo.h>.

mimalloc as a Rust global allocator, with pprof-compatible sampled heap profiling built in. Windows is a first-class target alongside Linux and macOS.

Dumps open directly in google/pprof for flame graphs, call graphs, top reports, and profile diffs.

Usage

[dependencies]
mimalloc-pprof = { version = "1", features = ["pprof"] }

[profile.release]
debug = "line-tables-only"
strip = false

Enable instrumentation (1.0: everything is opt-in)

default = []. The default build is a plain fast allocator: no profiler, no accounting hooks, no diagnostics, no DHAT, no owner gate — and its malloc/free fast path is byte-identical to upstream mimalloc, checked on the instruction stream by ci/check_fastpath_identity.py. Name what you want:

Feature C define What it builds in Cost when built in but idle
pprof MI_PPROF=1 sampled pprof heap profiling +3.4 instructions per malloc/free pair, 28.9 KB
memory-events MI_MEMEVT=1 allocation-change accounting and callbacks +9–13 instructions per pair, 4.0 KB
diagnostics MI_DIAGNOSTICS=1 heap snapshot + live-heap JSON dump 0 instructions, 25.7 KB
dhat (implies memory-events) MI_DHAT=1 the exact DHAT v2 observer 0 instructions, 9.8 KB
owner-gate MI_OWNER_GATE=1 purge_all can sweep every thread +95 instructions per pair
full all of the above everything

To restore what 0.11.x built by default, and more:

[dependencies]
mimalloc-pprof = { version = "1", features = ["full"] }

The whole API is present in every configuration. A subsystem that was not built in is still there as a stub that reports itself off — prof::start and enable_heap_profiling return false, dhat::start returns false, memory_events::set_enabled returns false, heap_dump_json returns None, heap_snapshot_to_file returns Err — so no caller needs a #[cfg] and default-features = false never fails to compile anything.

Cargo features are additive and unified across the dependency graph: if any crate in your build enables one of these, the C library is built with it and your build pays for it too. cargo tree -e features shows who asked.

owner-gate is the one that changes allocator behaviour rather than only adding observation: every allocator call takes a per-thread gate so purge_all can sweep every thread's heap, not only threads parked in park_while_idle (#366). Measured at +206% cycles on a tight malloc/free loop. Without it purge_all reports running threads as pending (PurgeStatus::Partial), which is a normal outcome, not an error.

use mimalloc_pprof::{prof, MiMalloc};
use std::path::Path;

#[global_allocator]
static ALLOCATOR: MiMalloc = MiMalloc;

fn main() -> std::io::Result<()> {
    assert!(prof::start(0), "profiler already running"); // 0 = default, ~512 KiB

    let retained = vec![0_u8; 1024 * 1024];
    prof::dump_file(Path::new("heap.prof"))?;            // dump while still live
    std::hint::black_box(&retained);

    prof::stop();
    Ok(())
}

Then:

pprof -http=:0 ./target/release/my_app heap.prof

Or profile without touching the code at all:

MIMALLOC_PROF=1 MIMALLOC_PROF_DUMP_AT_EXIT=heap.prof ./my_app

Versions

crate engine
1.x — current mimalloc-pprof = { version = "1", features = ["pprof"] } mimalloc v3
0.12.x mimalloc-pprof = { version = "0.12", features = ["pprof"] } mimalloc v3
0.11.x mimalloc-pprof = "0.11" mimalloc v3
0.8.x — previous mimalloc-pprof = "0.8" mimalloc v2

1.0 keeps the opt-in defaults introduced in 0.12: default = [], so a mimalloc-pprof = "1" dependency with no features is the allocator alone. Add features = ["pprof"] for sampled profiling or features = ["full"] for every instrumentation subsystem. The API remains available as inert stubs when a feature is absent.

The profiler API, environment variables, and output formats remain compatible with 0.12.

1.x is recommended. It has strictly more test coverage, per-heap allocator statistics, and fixes two upstream mimalloc bugs that 0.8.x still carries — including an unbounded memory leak on Windows/MinGW where every exiting thread leaked its heap and pages (23.5 GB at 100 stress iterations, versus flat after the fix). Note that upstream mimalloc v3 is itself still a pre-release branch.

Exact statistics alongside sampled ones

On 0.11.x and later, prof::stats() carries the allocator's exact counters next to the sampled ones. A sampled profile alone cannot tell you whether it under-counted; comparing the two measures the sampling error directly:

let s = mimalloc_pprof::prof::stats();
println!(
    "sampled live: {} bytes in {} samples; allocator committed: {}, requested: {}",
    s.live_bytes, s.live_samples, s.heap.committed, s.heap.malloc_requested,
);

The full API

Everything mimalloc-pprof exposes, grouped and shown next to its C counterpart, is in one table in the repository README: API surface. It is not duplicated here — one table, kept honest by ci/check_rust_surface.py, which fails the build if a C export or an mi_option_t enumerator has no Rust binding.

The short version. Safe wrappers, all at the crate root unless noted:

Module What it covers
prof sampled pprof profiling: start/stop, text and profile.proto dumps, stats(), samples(), modules()
dhat exact DHAT v2 profiling: start/stop, stats(), dump_file() (needs the dhat feature)
stats the allocator's exact counters: get(), json(), print(), bin_size(), and the subprocess-scoped forms
memory_events allocation-change accounting: set_enabled, snapshot, set_callbacks, visit_live_allocations (needs the memory-events feature)
options every mi_option_t, including the fourteen this fork adds (Opt::PROF, Opt::SCAVENGER, Opt::PURGE_HOLES, …)
crate root MiMalloc, heap_dump_json / heap_snapshot_to_file (need the diagnostics feature), on_thread_idle, park_while_idle, scavenger_stop, purge_all/purge_all_ex, purge_holes_stats, purge_holes_report, rezalloc/recalloc/expand, unwrapped_malloc/_free/_realloc

mimalloc_pprof::sys holds the raw unsafe extern "C" declarations and the #[repr(C)] struct mirrors behind all of the above. The mirrors are checked field-by-field against the C compiler's own layout on every build (tests/t19_layout.rs), because mi_option_t is positional and a mirror that drifted would silently set a different option than the caller named.

Platforms and cross-compilation

The crate vendors mimalloc as a single amalgamated C translation unit with no autotools or CMake step, so it builds wherever cc-rs can reach a C compiler — including cross-compiled builds, in every direction.

Windows ARM64 needs one adjustment, and the crate makes it itself. In plain C mode mimalloc models C11 atomics with a deprecated MSVC Interlocked wrapper whose _acq/_rel ARM64 intrinsics clang-cl does not declare, so a cargo-xwin cross build of aarch64-pc-windows-msvc cannot compile it. Since 0.9.3 the build script selects clang's C11 stdatomic implementation for that target instead.

That choice is deliberately made in build.rs rather than left to the caller: CFLAGS is process-global for every cc-rs build script in a build, so a consumer trying to fix this from the outside — for example with CFLAGS=-TP to force mimalloc's C++ atomics path — also changes the language mode of every other native dependency in the graph. Nothing outside this crate should have to know how its atomics are selected.

Notes

  • On Linux and macOS, keep frame pointers for reliable stack walking: rustflags = ["-Cforce-frame-pointers=yes"] in .cargo/config.toml. Windows x64 uses unwind information instead — keep the PDB.
  • Do not link a second mimalloc into the same process; this crate vendors its own.

Full documentation, the C API, and the upstream-bug details are in the repository README.

License

MIT, the same as upstream mimalloc.