Skip to content

Phase 1 — sampling engine: port mimalloc#1266, options, realloc fix, arena invariant #5

Description

@zackees

Part of #2. Blocked by: the Phase 0 issue (must be merged). Branch: feat/phase1-sampling. One PR (may split into 1a port / 1b options+API if the diff gets large). C-core only — no rust/ changes.

Objective

Port Datadog's sampling engine (microsoft/mimalloc#1266) onto our main, add runtime control + options, and fix its known realloc gap. After this phase, sampled allocations are recorded and released correctly — no stacks yet (Phase 2), no dumps yet (Phase 3).

Step-by-step

1. Study the source PR (do not skip)

gh pr diff 1266 --repo microsoft/mimalloc > 1266.diff
gh pr view 1266 --repo microsoft/mimalloc --comments

Read the whole diff and the discussion (it explains why a global hash table for free-matching was rejected — the per-page metadata list is load-bearing; keep it).

2. Apply and reconcile

git apply --3way 1266.diff will partially fail — the PR targets dev-main, which merged into main on 2026-07-20 (upstream #1337), so drift is small but real. Resolve hunk-by-hunk. The components you must end up with:

  • include/mimalloc/profile.h + src/profile.c (new files).
  • Per-thread sampling state (bytes_since_sample / next_threshold) in the thread-local data; the alloc-path callback returns the next threshold.
  • Hooks: _mi_page_malloc_zero (src/alloc.c), mi_free_block_local (src/free.c), _mi_page_thread_free_collect (src/page.c).
  • Per-page sample records: page->metadata list + has_metadata bit ⇒ free fast path pays one branch on a hot cache line.
  • TLS in_profiler reentrancy flag.
  • test/test-profile.c (extend it, see Tests).

Amalgamation trap: src/static.c is how the Rust sys crate builds the C code. Look at how static.c includes the other sources and add profile.c the same way (guarded by MI_PPROF). Forgetting this breaks Phase 4 invisibly — add it now.

3. Build flag

CMakeLists.txt: option(MI_PPROF "Enable pprof heap profiling support" ON); when ON compile src/profile.c and define MI_PPROF=1 (follow the style of existing MI_xxx option blocks). MI_PPROF=OFF must produce a build with zero behavioral diff from upstream — all hook lines compile away behind #if MI_PPROF.

4. Options (register in src/options.c + the mi_option_t enum in include/mimalloc.h; copy the pattern of guarded_sample_rate — env mapping MIMALLOC_<NAME> is automatic)

Option Default Meaning
prof 0 start profiling at process init
prof_sample_rate 524288 mean bytes between samples
prof_bt_max 32 reserved for Phase 2
prof_accum 0 reserved for Phase 3
prof_seed 0 0 = random; nonzero = deterministic (tests)

5. Public API (include/mimalloc/profile.h, exported like other mi_ functions)

bool mi_prof_start(size_t sample_rate);              // 0 => option/default; idempotent
bool mi_prof_start_seeded(size_t sample_rate, uint64_t seed);
void mi_prof_stop(void);                              // frees all sample records
bool mi_prof_is_enabled(void);
// test-only introspection (documented as unstable):
void mi_prof_debug_stats(size_t* live_records, size_t* live_bytes);

6. Sampling distribution

Exponential inter-sample intervals, mean = rate: next = (size_t)(-log(u01(rng)) * rate) with a per-thread xorshift64* RNG. Seeded deterministically from prof_seed + thread ordinal when prof_seed != 0. Clamp next to [1, 64*rate].

7. Fix the PR's known gap: in-place realloc

Trace every realloc path in src/alloc.c (mi_realloc_mi_heap_realloc_zero etc.). When a block resizes in place: if it has a sample record, update the record's size (and Phase 3 will adjust counters). When realloc moves the block, the normal free+alloc hooks already fire — verify with a test, don't assume.

8. PROJECT INVARIANT — profiler memory safety

All profiler-internal memory comes from a dedicated arena built directly on the raw OS layer (_mi_os_alloc → VirtualAlloc/mmap). Never from mi_malloc/mi_heap_*/anything that passes through the hooked allocation paths (operator new, GlobalAlloc, CRT malloc redirection). This makes profiler recursion structurally impossible instead of merely guarded; the TLS in_profiler flag stays as defense-in-depth and asserts (debug builds) that no hooked path is ever entered while set.

Concretely: a chunked arena in profile.c (_mi_os_alloc 64 KiB chunks, bump-allocate records, internal free list for reuse); mi_prof_stop() releases every chunk. Phases 2–3 must use this same arena for the intern table and dump buffers. Any future profiler allocation that can't come from the arena goes straight to _mi_os_alloc — never to the user heap.

Tests (test/test-profile.c, registered in CMakeLists next to the existing test-api pattern)

  1. Seeded run, rate 4096, 1000 × 512 B allocs → mi_prof_debug_stats live_records within [60, 190] (expected ≈125; document the math in a comment).
  2. Free everything → live_records == 0, live_bytes == 0 (pool reused, not leaked — run under MI_DEBUG_FULL).
  3. Realloc: grow in place, shrink in place, forced move (grow 16 B → 1 MiB); accounting stays consistent after each.
  4. mi_prof_stop() then more alloc/free traffic → no records created, no crash.
  5. Multithread: 4 threads × 100k alloc/free with profiling on; join; free remainder; live_records == 0.

Definition of done

  • c-unit green on all 3 OSes with MI_PPROF=ON and the OFF job (upstream tests untouched and passing)
  • test-profile passes on all 3 OSes, Release and MI_DEBUG_FULLexplicitly including both Windows toolchains (MSVC job and win-gnu/MinGW job)
  • Debug-build assertion verifies the memory-safety invariant (no hooked-path allocation while in_profiler is set)
  • MI_PPROF=OFF build has zero source-visible behavior change (hooks compiled out)
  • Disabled-at-runtime overhead = one predictable branch per alloc/free (state where the branch is in the PR description)
  • rust-native still green (static.c amalgamation intact)
  • No rust/ changes in this PR

Divergence from this plan (hook points moved, PR conflicts unresolvable, option pattern changed) → comment here first.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions