Repository navigation
profile: MI_PROFILE=1 without a profiler costs what MI_PROFILE=0 costs (but for 4 instructions in the generic path); mi_profiler_start reaches the running threads - #44
Merged
Conversation
… generic path, and little mi_malloc and mi_free compile to the same code with MI_PROFILE=0 and 1. What MI_PROFILE=1 cost without a profiler (1.46% of the instructions of a malloc/free micro-benchmark) was all in the generic path: the count of the blocks that a page handed out, at every refill of a free list (28 instructions to find out that the theap does not sample), and the checks of the sample countdown in _mi_malloc_generic. The refill code is that of MI_SAMPLE==0 again. A theap that samples counts the first page of a size class before it refills it, from _mi_malloc_generic behind one test of sample_rate. A theap that does not sample counts nothing; when it starts to, last_alloc is brought up to alloc_count on its pages so that nothing from before is counted. 0.22% is left (4 instructions per call of _mi_malloc_generic). mi_profiler_start now asks the theaps of the running threads to look at the profiler at their next generic allocation, through the limit of generic_count, which is a field of the theap now: they did so only every 1000 generic allocations, which is many megabytes of small blocks.
Jarred-Sumner
added a commit
to oven-sh/bun
that referenced
this pull request
Sep 14, 2026
Pins 58460467413a, the merge of oven-sh/mimalloc#45 on top of #44, on top of #40, #43 and #42. #44: with the profiler hooks compiled in (MI_PROFILE=1, the default) and no profiler attached, a thread paid for them in the shared page collect and extend code. A theap that does not sample now pays in the generic allocation path only, and little: hooks compiled in cost +0.2% of instructions on the allocator micro benchmark where they cost +1.5%. mi_profiler_start reaches the threads that are already running through theap->generic_fast_limit, at their next allocation that takes the generic path. #45 drops a debug assertion that raced with mi_profiler_stop. 25 M malloc + 25 M free with bun's flags: 2.2403 G instructions with main's pin, 2.2384 G with the previous commit, 2.2073 G now (-1.5% against main's pin). No header that bun includes changes.
Jarred-Sumner
added a commit
to oven-sh/bun
that referenced
this pull request
Sep 15, 2026
Pins 58460467413a, the merge of oven-sh/mimalloc#45 on top of #44, on top of #40, #43 and #42. #44: with the profiler hooks compiled in (MI_PROFILE=1, the default) and no profiler attached, a thread paid for them in the shared page collect and extend code. A theap that does not sample now pays in the generic allocation path only, and little: hooks compiled in cost +0.2% of instructions on the allocator micro benchmark where they cost +1.5%. mi_profiler_start reaches the threads that are already running through theap->generic_fast_limit, at their next allocation that takes the generic path. #45 drops a debug assertion that raced with mi_profiler_stop. 25 M malloc + 25 M free with bun's flags: 2.2403 G instructions with main's pin, 2.2384 G with the previous commit, 2.2073 G now (-1.5% against main's pin). No header that bun includes changes.
Jarred-Sumner
added a commit
to oven-sh/bun
that referenced
this pull request
Sep 15, 2026
Pins 58460467413a, the merge of oven-sh/mimalloc#45 on top of #44, on top of #40, #43 and #42. #44: with the profiler hooks compiled in (MI_PROFILE=1, the default) and no profiler attached, a thread paid for them in the shared page collect and extend code. A theap that does not sample now pays in the generic allocation path only, and little: hooks compiled in cost +0.2% of instructions on the allocator micro benchmark where they cost +1.5%. mi_profiler_start reaches the threads that are already running through theap->generic_fast_limit, at their next allocation that takes the generic path. #45 drops a debug assertion that raced with mi_profiler_stop. 25 M malloc + 25 M free with bun's flags: 2.2403 G instructions with main's pin, 2.2384 G with the previous commit, 2.2073 G now (-1.5% against main's pin). No header that bun includes changes.
Jarred-Sumner
added a commit
to oven-sh/bun
that referenced
this pull request
Sep 15, 2026
, #44, #45, #46) (#42668) ### What does this PR do? Moves `MIMALLOC_COMMIT` from `ab13501334a8` to `69050cf3ec14be4847ca3ba6c840da821fa283b0`, the head of `bun-dev3-v2`: main's pin plus six merged PRs of the fork and nothing else, and changes the two callers in bun that the last of them asks to change. `MI_FREE_USE_PAGEMAP=1` stays. - oven-sh/mimalloc#40: the sampled profiler hooks (`mi_profiler_t`) attribute a sample to the small allocation that refilled its page, keep `bytes_since_last_sample` right when `on_alloc` raises the rate, sample a new thread's heap from its first allocation, report the frees of `mi_heap_destroy`, and give `on_free` of an aligned block the pointer that `on_alloc` got. What the heap profiler in #42626 needs. - oven-sh/mimalloc#43: an aligned request only becomes a sample through the over-allocating path (with #40 alone `mi_malloc_aligned(64, 64)` could free a sampled block inside the allocator; `MI_DEBUG` asserted). - oven-sh/mimalloc#42: the purge pull-forward of an idle thread (`mi_on_thread_idle`) goes through compare-and-swap, so it cannot undo the reset that a purge pass does first thing. 12 lines in `src/arena.c`. - oven-sh/mimalloc#44: a thread that does not sample pays for the profiler hooks in the generic path only: +0.2% of instructions on the allocator micro benchmark where they cost +1.5%. - oven-sh/mimalloc#45: a debug assertion that raced with `mi_profiler_stop` is gone. - oven-sh/mimalloc#46: **free blocks in the 4 MiB pages (blocks of 96 to 512 KiB) are handed back to the OS.** Hole purging skipped these pages (their 1024 OS pages do not fit the 256-bit purge bitmap), so what a process freed in that range stayed resident for as long as one block of the page was alive: 97% of it by `mincore`. The bitmap's unit is now per page (16 KiB for a 4 MiB page), and the free blocks of a large page stay while the page is in use, which the allocator tells without a clock on the allocating side: the idle sweep counts epochs of 100 ms or more, an allocation from a large page leaves the current epoch in the page (a load, a compare, and a store once per epoch), and the blocks go when that is two epochs ago. Without that hold a `Bun.serve` with 256 KiB bodies discarded and refaulted 3 GB in 20 s. Nothing in bun attaches a profiler yet. The fast paths of `mi_malloc` and `mi_free` are untouched by all six. `include/mimalloc.h` gains one function, `mi_on_thread_idle_pending()`, and comments; `mimalloc-stats.h` is identical, `mimalloc-profile.h` differs in comments, `mimalloc/types.h` gains fields of the internal `mi_theap_t` and `mi_tld_t`. No struct or enum that bun binds changed (`src/mimalloc_sys/mimalloc.rs`, the declarations in `BunJSCEventLoop.cpp` and `epoll_kqueue.c`). #### The callers that sweep once and then block With oven-sh/mimalloc#46 ONE call of `mi_on_thread_idle()` never takes the free blocks of a large page that the thread allocated from since its previous sweep: a sweep ends one epoch at the most. The event loop is fine: `us_loop_run_bun_tick` hands the thread's heaps to mimalloc's scavenger thread across the poll (`mi_on_thread_idle_start()` / `_end()`), and the scavenger comes back for them by itself, two or three sweeps an interval apart. Two callers swept inline once and then blocked for good, and would have kept such blocks, as they do on main: - **`src/threading/ThreadPool.rs`**: a worker swept once, after its futex wait timed out at 100 ms, and then waited without a timeout. It now brackets that second wait with `mi_on_thread_idle_start()` / `mi_on_thread_idle_end()`, and sweeps inline (once, as before) only when `_start` says there is no scavenger to hand off to. Nothing runs between the two calls but `Futex::wait`, which allocates nothing; `drain_idle_events` runs before `_start`. Only the wait that follows a timed-out one is bracketed. Bracketing every park was measured and is slower (table below: `_start` is two read-modify-writes on cache lines that every parking thread shares, and it wakes the scavenger, which costs half a context switch per task), and sweeping on every park is what the comment there warns about (13% of `vite preview` requests per second). As it is, a worker that parks between tasks never reaches `_start`, and `notify()` is what it was. - **The libuv loop on Windows** (`Bun__JSC_onBeforeWait` in `BunJSCEventLoop.cpp`, from `us_loop_run` in `libuv.c`) sweeps inline at most every 100 ms and then `uv_run(UV_RUN_ONCE)` blocks. It cannot hand off: `uv_run` dispatches the completions right after its poll inside the same call (`uv__poll` at `src/win/core.c:732`, `uv__process_reqs` at `:737`), and libuv allocates from mimalloc there (`uv_replace_allocator`), so there is no point between "about to block" and "awake" that bun's code sees without a patch to libuv. It keeps the inline sweep, and while `mi_on_thread_idle_pending()` says blocks were left, `Bun__JSC_onBeforeWait` returns 100 (ms) and `us_loop_run` arms an unref'd timer for that long after the sweep, so the hook runs again: three times at the most after JS last ran, which is the bound the fork gives. The timer does not keep the loop alive. Not changed: when no scavenger runs (`MIMALLOC_SCAVENGER=0` or a purge delay of 0; bun sets neither) the fallbacks in `us_loop_run_bun_tick` and in the pool sweep inline once, as on main, and the free blocks of large pages stay. ### How did you verify your code works? Release builds of one tree (this branch on `04823d8fad67`), Linux x64: **base** = main's pin, **pin** = this pin with the callers unchanged (the first commit), **this PR** = both commits. **Test.** `test/js/bun/jsc/heapStats-mimalloc.test.ts` gains "an idle work pool thread hands back the free blocks of its large pages": a subprocess with 4 pool threads reads 480 files of 96 to 512 KiB with `fs.promises.readFile` (the buffers are allocated on the pool threads), keeps 1 in 12, collects the rest, and polls `RssAnon` while nothing gives the pool work. Anonymous memory beyond what it started with and what it keeps has to come down to 12 MB within 2.5 s. | | free and resident at the end | | --- | --- | | base | 54 to 67 MB: fails | | pin, callers unchanged | 38 to 47 MB: fails (also with `BUN_GARBAGE_COLLECTOR_LEVEL=1` and `2`, and with the system bun 1.4.1: 50 MB) | | this PR | -1 MB, reached 350 ms into the idle: passes, 10 of 10 runs | **The park and notify path of the pool** (what the `has_swept` comment protects). 100000 `await fs.promises.stat("/")` one after the other (every call notifies a worker, which runs one task and parks), the same in batches of 8, and `await randomBytes(16)`; `perf stat` of the whole process, 8 cores, 5 interleaved runs, medians (range): | user instructions per call | base | pin | this PR | | --- | --- | --- | --- | | `stat`, sequential | 8794 (8784..8828) | 8792 (8716..8827) | 8797 (8780..8816) | | `stat`, 8 at a time | 6027 (6014..6131) | 6025 (6004..6080) | 6034 (6008..6102) | | `randomBytes(16)`, sequential | 12608 (12513..12629) | 12581 (12522..12609) | 12589 (12582..12627) | Context switches per call are 3.96 to 3.97, 0.51 to 0.53 and 3.97 in all three, kernel instructions per call overlap (41.6 k..49.9 k sequential), wall time is within the spread of the machine. Bracketing EVERY park instead (a build with a switch for it, 200000 calls, 4 interleaved runs), against the same build with the handoff after a timed-out wait only: | | after a timed-out wait (this PR) | every park | | --- | --- | --- | | `stat` sequential: user / kernel instructions, context switches per call | 8565..8601 / 45.6 k..46.7 k / 3.95..3.99 | 9193..9335 (+7%) / 49.6 k..50.7 k (+9%) / 4.2..4.55 | | `stat` 8 at a time | 5635..5659 / 8.8 k..9.3 k / 0.53..0.55 | 5771..5960 (+3%) / 10.1 k..11.6 k (+13%) / 0.63..0.75 | | `randomBytes(16)` sequential | 12145..12283 / 43.2 k..45.5 k / 3.97..3.99 | 12815..12893 (+5%) / 48.2 k..51.2 k (+11%) / 4.41..4.5 | | wall time, `stat` sequential | 11.6 s..12.7 s | 12.4 s..13.5 s (+6 to 8%) | **Allocator micro benchmarks** (mimalloc alone with bun's flags: clang 21, `-O3 -march=nehalem`, C++, `MI_FREE_USE_PAGEMAP`; user-mode instructions, the runs of one build equal to within 100): | 25 M `mi_malloc` + 25 M `mi_free` of 16..1039 bytes | instructions | | --- | --- | | before the upstream sync (`707d90be`) | 2.0731 G | | main's pin (`ab135013`) | 2.0568 G | | with oven-sh/mimalloc#40, #43, #42 (`3aa0ac9b`) | 2.0556 G (-0.06%) | | with #44, #45 (`58460467`) | 2.0305 G (-1.28%) | | this pin (`69050cf3`) | **2.0305 G (-1.28%)**, the same as `58460467` to within 400 instructions | | this pin with `MI_PROFILE=0` (not what bun builds) | 2.0244 G | (The absolute numbers are 8% below the ones this description had before: the benchmark's own object file is built differently here. All rows are from one recipe.) | 20 M `mi_malloc(128 KiB)` + `mi_free`, the generic path into a large page | instructions per pair | | --- | --- | | main's pin | 230.6 | | `58460467` | 193.6 | | this pin | 206.6 (+13.0 for the epoch stamp and its gate; -10% against main's pin) | **bun** (5 interleaved runs pinned to 8 cores; user-mode instructions and peak RSS, medians): | | base | pin | this PR | | --- | --- | --- | --- | | `bun -e 1` | 10.405 M, 27.0 MB | 10.410 M (+0.04%), 27.0 MB | 10.412 M (+0.06%), 27.0 MB | | `Bun.serve` hello, per request (20000 sequential `fetch`) | 52071, 49.0 MB | 51129 (-1.8%), 50.6 MB | 51361 (-1.4%; the spread between runs is 3%), 50.1 MB | | `Bun.serve` start and stop alone | 12.180 M | 12.179 M | 12.182 M (+0.01%) | | `JSON.stringify` + `JSON.parse` of a 200-user object, 3000 times | 5105.20 M, 44.0 MB | 5104.58 M, 44.0 MB | 5104.00 M (-0.02%), 43.5 MB | | string concat + `Buffer.from` + `split`, 300 x 5000 | 1900 M, 59.6 MB | 1912 M (+0.6%; the spread between its runs is 6.5%) , 60.5 MB | 1897 M (-0.2%), 59.7 MB | | idle after start (`Bun.sleep`), RSS / anonymous from `smaps_rollup`, binaries read into the page cache first | 28.42 MB / 3.14 | 28.45 MB / 3.05 | 28.45 MB / 3.05 | Nothing is slower by more than 0.1% outside the spread of its own runs. **The large-buffer server of oven-sh/mimalloc#46 at bun level** (`Bun.serve` that reads 256 KiB request bodies and answers 90 to 270 KB of JSON; 16 connections for 15 s, then idle; RSS / anonymous from `smaps_rollup`): | at idle | base | pin | this PR | | --- | --- | --- | --- | | 40 bodies kept, 2 s idle | 74.7 / 36.2 MB | 54.3 / 15.9 MB | 54.3 / 15.9 MB (**-20.4 MB**) | | 40 bodies kept, 15 s idle | 74.7 / 36.2 MB | 54.2 / 15.8 MB | 54.2 / 15.9 MB | | nothing kept, 15 s idle | 47.0 / 8.6 MB | 43.6 / 5.3 MB | 43.7 / 5.5 MB (-3.3 MB) | | at the end of the load, before the idle (40 kept) | 74.4 / 36.4 MB | 73.7 / 35.7 MB | 73.9 / 35.9 MB | Under sustained load (the same server, 20 s, 3 interleaved rounds; instructions and page faults of the server process from `perf stat -p`): | | requests | user instr / request | kernel instr / request | page faults / request | | --- | --- | --- | --- | --- | | base | 83.6 k .. 92.7 k | 283.9 k .. 284.5 k | 119.4 k .. 126.6 k | 0.34 .. 0.53 | | pin | 87.0 k .. 91.4 k | 284.0 k .. 284.1 k | 118.8 k .. 127.4 k | 0.50 .. 0.64 | | this PR | 80.4 k .. 91.4 k | 284.0 k .. 284.7 k | 120.9 k .. 125.3 k | 0.51 .. 0.86 | Requests and instructions per request are the same; the buffers stay while the server is busy and go when it is not. **Tests with the release build of this PR** - `test/js/bun/spawn/spawn-pipe-leak.test.ts` 25 of 25 runs, `test/js/bun/jsc/heapStats-mimalloc.test.ts` (with the new test) and `test/js/bun/gc/gc-controller-cadence.test.ts` 10 of 10 each. - 66 test files, 2165 tests pass: `require-cache`, `serve.test.ts`, all of `test/js/web/workers`, `test/js/node/worker_threads`, `test/js/node/child_process` and `test/js/node/fs` (the pool-heavy ones), `spawn-noread-leak`, `bun-file`, `bun-write`, `bundler_edgecase`, `esbuild/splitting`. The two failures need a non-root user (the root-range port in `serve.test.ts`, EPERM after dropping privileges in `child_process.test.ts`) and fail the same way with the base build. - The wider list of 113 files (bake dev, bundler, fetch, http, streams, sqlite, install, resolve, shell and others): 2485 tests pass; the two failures are in `resolve.test.ts`, want a directory that root cannot read, and fail the same way with the base build. - The new test fails with `USE_SYSTEM_BUN=1` (bun 1.4.1: 50 MB) and with the first commit alone (38 to 47 MB), and passes with both. - No debug or ASAN build was made for this revision of the PR (the earlier revisions ran `heapStats-mimalloc`, `gc-controller-cadence` and `worker.test.ts` on one at `58460467`); the new test skips itself under ASAN, where malloc is not mimalloc. **Not run here:** macOS and Windows. The Windows half of the second commit (`BunJSCEventLoop.cpp` under `OS(WINDOWS)`, `libuv.c`, `libuv.h`) was compiled by nothing on this machine: the C++ function was syntax-checked with `OS(WINDOWS)` forced on against stubs, `cargo check --target x86_64-pc-windows-msvc` passes for `bun_uws_sys`, `bun_threading` and `bun_mimalloc_sys` (the `WindowsLoop` mirror gained the field), and the rest is for CI. The fork's CI passed 23 of 23 jobs at each of the six merges (Linux x64/arm64 with ASAN, UBSAN, TSAN and guarded builds, the `MI_FREE_USE_PAGEMAP` build that matches bun's configuration, macOS, Windows x64/arm64, FreeBSD).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Where
MI_PROFILE=1costs something without a profilerBun's flags (C++20,
-O3 -march=nehalem,MI_FREE_USE_PAGEMAP=1), clang 21, a malloc/free micro-benchmark (50 M iterations over 256 slots, 16 to 1039 bytes, 25 Mmi_mallocof which 1.23 M reach_mi_malloc_generic), user instructions:MI_PROFILE=0MI_PROFILE=1, no profilerbun-dev3-v2mi_mallocandmi_freecompile to the same instructions with and withoutMI_PROFILE(only the address of__mi_page_mapdiffers), andcallgrindcounts the same for them and formain: 421,582,951 and 576,011,818 in both builds. All of the 32.9 M instructions are in the generic path:mi_page_update_sample_countdown: 25.3 M. It runs when a free list is refilled (898,081 times, 28 instructions): find the theap of the page, leave ifsample_rate == 0, movelast_allocup._mi_malloc_generic: 7.6 M, 6 instructions per call: the check of the countdown before, and ofsample_rateand the countdown aftermi_page_queue_find_free.What a theap that does not sample runs now
mi_page_update_sample_countdownis what it is withMI_SAMPLE==0: the reset whenalloc_countgets to0x8000. The blocks that the fast path took from a page are charged to the sample countdown bymi_theap_count_page_allocs, which only a theap that samples calls: for the first page of the size class beforemi_page_queue_find_freerefills it (mi_theap_refill_is_sample, out of line), and as before inmi_theap_page_update_stats. (At the end ofmi_malloc_generic_fallbackevery theap calls it: that is where the block of a sample comes from, withsample_rateset aside by_mi_malloc_generic_no_sample;last_allochas to move up there or the block is counted a second time.) If that uses up the countdown, or it was used up already, the allocation in progress goes tomi_malloc_generic_fallback, which takes the sample as it did (or returns NULL forMI_MALLOC_GENERIC_BLOCK_START)._mi_malloc_genericthat is one test,theap->sample_rate != 0, beforemi_page_queue_find_free.last_allocup either. When its rate goes from 0 to something (_mi_theap_update_sample_rate),_mi_theap_sync_sample_countssetslast_alloc = alloc_counton its pages: what they handed out before is not for the first period.MI_SAMPLE_COUNTDOWN_MAX(_mi_theap_init, and_mi_theap_update_sample_ratewhen the rate goes to 0), not -1 or 0: nothing in the generic path puts it right any more, andalloc-aligned.casksmi_theap_should_samplebefore its naturally aligned path.The 4 instructions per generic call that are left: that test (2), the limit below (1, clang does not fold the relaxed atomic load into the compare), and one that the compiler adds around them. Without any: a theap that samples has a limit of 0 and takes another path than the fast one of
_mi_malloc_generic, which then has no code for sampling at all (+0.056%, the load of the limit). That path runs the head of_mi_malloc_genericfor nothing, and an attached profiler cost 2% more in an allocation benchmark of Bun: not taken.mi_profiler_startreaches the threads that are runningA theap looked at its profiler every 1000 generic allocations (
mi_malloc_generic_admin): with small blocks that is many megabytes aftermi_profiler_start. Now:_mi_malloc_generictakes its fast path while++generic_count < theap->generic_fast_limit(a field,MI_GENERIC_FAST_LIMIT= 1000, instead of the constant).mi_profiler_startstores -1 there for the theaps (not the detached ones, which hold meta data) of the heaps that have the profiler, in the current and the main sub-process, underheaps_lockandtheaps_lock. Their next generic allocation goes tomi_malloc_generic_fallback, which calls_mi_theap_update_profiling(the code that was inmi_malloc_generic_admin, which calls it too). That sets the limit to 0 while it looks and back to 1000 with a compare-and-swap: a request that comes in while it looks stays.mi_profiler_startruns leaves the request with itself if the profiler is enabled once it is on the list of its heap and its own look did not see that.profile_sample_countdown), as_mi_theap_initdoes: with guarded sampling on, the first guarded sample point was a profiler sample for a period that nobody requested._mi_theap_update_profilingdoes not start a detached theap:mi_process_theap_metais on the list of the main heap, andmi_malloc_generic_adminwould have started to sample it after 1000 generic allocations ("no sampling for meta data",init.c)._mi_malloc_generic_no_sample(which setssample_rateaside and puts it back) a start or stop is put off to the next generic allocation (it leaves the request for it: when every allocation is a sample,mi_malloc_generic_admincan get there inside of it every time, and ami_profiler_stopwas never seen)._mi_theap_malloc_profiledlooks as well when it finds the profiler stopped._mi_malloc_generic_no_sampleon a theap that does not sample sets nothing aside and just allocates.MI_SAMPLE_COUNTDOWN_MAXinstead of 0.With a profiler attached
A running profiler costs a bit more than before. Same micro-benchmark with a profiler at 512 KiB (26,900 samples): 2.87383 G before, 2.88457 G (+0.37%). An allocation benchmark of Bun (3 M
ArrayBuffers, JIT on, user instructions, average of 5): without a profile 6.0253 G to 6.0138 G; with one at 512 KiB 6.1229 G to 6.1653 G, which is 2.5% over a run without a profile where it was 1.6%. That is paid only while a profile runs; what is saved is paid by everyone all the time. Sampled over requested bytes for allocations of 12,000 to 200,000 bytes at 128 KiB: 1.021, 1.022, 1.098, 1.012, 1.000, as before.test-profileprints the same figures as before within a few percent (release: small share 0.585 to 0.620 for 0.699, 511 to 493 samples of aligned allocations for 553).Tests
test_profiler_start_with_running_threads: four threads allocate (250,000mi_malloceach, 16 to 1039 bytes, 256 slots), a fifth starts a profiler at 256 KiB; each thread gets its first sample after 264 K to 333 K bytes of what it requests from then on (bounds: half a period to two). Without the request: none within 16 periods on two threads, 3 and 4 MiB on the others. Without_mi_theap_sync_sample_counts: after 546 to 14,439 bytes.test_profiler_aligned_late_startnow gets all 3000 samples (2002 before: the first 998 allocations were before the theap looked).ctestwith the flags and environment of.github/workflows/test.yaml: Release, DebugMI_DEBUG=FULL, Secure, Debug and Release as C++, ASAN, UBSAN, TSAN, Debug and ReleaseMI_GUARDED=FULL, Debug and ReleaseMI_FREE_USE_PAGEMAP=ON, Debug and ReleaseMI_SECURE=FULL, TLS pthreads, SIMD, ReleaseMI_PROFILE=OFF, DebugMI_DEBUG=FULLwithMI_GUARDED=OFF(coarse sampling with asserts).Seen on the way, not changed
test_profiler_start_with_running_threadsfails when it is run by hand withMIMALLOC_GUARDED_SAMPLE_RATEbelow its rate in aMI_GUARDEDbuild (ctestsets 0 fortest-profile): the profiler gets its samples early there, before and after this change (_mi_theap_malloc_sampledtakes all bytes since the last profiler sample offprofile_sample_countdownat every guarded sample point, see the notes of #43).The block of a sample below
MI_SMALL_MAX_OBJ_SIZEis counted a second time when its page is next counted (it comes from the fast path of_mi_malloc_genericinside_mi_malloc_generic_no_sample, wherelast_allocdoes not move): 3% for allocations of 2000 bytes at 128 KiB, as before.