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]https://github.com/oven-sh/mimalloc) is POSIX-only — its stack capture is
> guarded behind glibc/Apple `<execinfo.h>`.

[mimalloc](https://github.com/microsoft/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](https://github.com/google/pprof) for flame
graphs, call graphs, top reports, and profile diffs.

## Usage

```toml
[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:

```toml
[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](https://github.com/zackees/mimalloc-pprof/issues/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.

```rust
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:

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

Or profile without touching the code at all:

```sh
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:

```rust
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](https://github.com/zackees/mimalloc-pprof#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](https://github.com/zackees/mimalloc-pprof).

## License

MIT, the same as upstream mimalloc.