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)
- 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).
- Free everything → live_records == 0, live_bytes == 0 (pool reused, not leaked — run under
MI_DEBUG_FULL).
- Realloc: grow in place, shrink in place, forced move (grow 16 B → 1 MiB); accounting stays consistent after each.
mi_prof_stop() then more alloc/free traffic → no records created, no crash.
- Multithread: 4 threads × 100k alloc/free with profiling on; join; free remainder; live_records == 0.
Definition of done
Divergence from this plan (hook points moved, PR conflicts unresolvable, option pattern changed) → comment here first.
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 — norust/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)
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.diffwill partially fail — the PR targetsdev-main, which merged intomainon 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).bytes_since_sample/next_threshold) in the thread-local data; the alloc-path callback returns the next threshold._mi_page_malloc_zero(src/alloc.c),mi_free_block_local(src/free.c),_mi_page_thread_free_collect(src/page.c).page->metadatalist +has_metadatabit ⇒ free fast path pays one branch on a hot cache line.in_profilerreentrancy flag.test/test-profile.c(extend it, see Tests).Amalgamation trap:
src/static.cis how the Rust sys crate builds the C code. Look at howstatic.cincludes the other sources and addprofile.cthe same way (guarded byMI_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 compilesrc/profile.cand defineMI_PPROF=1(follow the style of existingMI_xxxoption blocks).MI_PPROF=OFFmust 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+ themi_option_tenum ininclude/mimalloc.h; copy the pattern ofguarded_sample_rate— env mappingMIMALLOC_<NAME>is automatic)profprof_sample_rateprof_bt_maxprof_accumprof_seed5. Public API (
include/mimalloc/profile.h, exported like othermi_functions)6. Sampling distribution
Exponential inter-sample intervals, mean = rate:
next = (size_t)(-log(u01(rng)) * rate)with a per-thread xorshift64* RNG. Seeded deterministically fromprof_seed+ thread ordinal whenprof_seed != 0. Clampnextto[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_zeroetc.). 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 frommi_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 TLSin_profilerflag 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_alloc64 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 existingtest-apipattern)mi_prof_debug_statslive_records within [60, 190] (expected ≈125; document the math in a comment).MI_DEBUG_FULL).mi_prof_stop()then more alloc/free traffic → no records created, no crash.Definition of done
c-unitgreen on all 3 OSes withMI_PPROF=ONand theOFFjob (upstream tests untouched and passing)test-profilepasses on all 3 OSes, Release andMI_DEBUG_FULL— explicitly including both Windows toolchains (MSVC job and win-gnu/MinGW job)in_profileris set)MI_PPROF=OFFbuild has zero source-visible behavior change (hooks compiled out)rust-nativestill green (static.c amalgamation intact)rust/changes in this PRDivergence from this plan (hook points moved, PR conflicts unresolvable, option pattern changed) → comment here first.