Conversation
Bump the mimalloc pin to oven-sh/mimalloc#31. `mi_heap_destroy` frees the `mi_heap_t` and `mi_arena_pages_t` of the heap with `_mi_free_subproc_safe`, which never collected the page it freed into. mimalloc abandons a page once it is full, and a block freed into an abandoned page without a collect is stranded: the page stays abandoned and resident for good. Once enough heaps are alive at the same time to fill a page with their meta data, every destroyed heap leaks about 150 KiB. Bun creates one heap per `bun_alloc::Arena`, for example one per `Bun.Transpiler`, and the GC destroys them in batches. A release build leaked about 150 KiB of RSS per `new Bun.Transpiler({ define })` with no bound (10,000 instances: 1.45 GB). The test destroys 200 heaps per round and checks that the abandoned page count and RSS stay flat across rounds.
This was referenced Sep 1, 2026
Collaborator
Author
|
Status
|
Collaborator
Author
|
Closing in favor of #41253 and #41261.
|
robobun
added a commit
that referenced
this pull request
Sep 3, 2026
Each new Bun.Transpiler({ define }) owns a mimalloc heap. When hundreds
of these heaps die in one GC, mi_heap_destroy frees their meta data into
full, abandoned pages. Before oven-sh/mimalloc#31 it did not collect
those pages, so each page stayed abandoned and resident.
The test creates 200 instances per round for 4 rounds. It checks that
heapStats().mimalloc.pages_abandoned.current and RSS do not grow between
rounds. With the mimalloc pin on main, the abandoned count grows by 28
per round.
The test comes from #41100.
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.
Draft until oven-sh/mimalloc#31 merges. The pin points at the head of that PR so CI builds with the fix. Once it merges into
bun-dev3-v2, the pin moves to the merge commit and this PR leaves draft. Only the pin and the test change.Problem
new Bun.Transpiler({ define })leaks about 150 KiB of RSS per instance when 100 or more instances die in one GC. There is no bound: 10,000 instances leave 1.45 GB, and an idle event loop does not give it back.bun_alloc::Arena,JSTranspiler.rs:952).mi_heap_destroyfrees the heap's meta data (about 150 KiB) with_mi_free_subproc_safe(heap.c:232), which never collects the page it frees into (free.c:280). In a full, abandoned page the block is stranded for good.Fix
_mi_free_subproc_safecollects when the page belongs to the current sub-process. A free across sub-processes keeps the old behavior. The fork PR carries a C test for it.mi_freetakes this path for a block of another thread: claim the page, fold its thread-free list intoused, then free, reclaim, or re-map it.test/js/bun/jsc/heapStats-mimalloc.test.ts(84 stranded pages on main, 0 with the fix, debug and release). Also the transpiler and bundler suites,test/js/bun/util/,process.test.js, and the fork'sctestsuite. Self-reviewed: 7 concerns raised, 7 addressed (see Notes).Background
bun_alloc::Arenais one heap:mi_heap_newon creation,mi_heap_destroyon drop. The heap's own meta data lives in pages of the main heap.heapStats().mimalloc.pages_abandoned.currentcounts abandoned pages. A stranded page stays abandoned, so the test pins the leak with an exact count.Notes
Repro on the released binary (
bun 1.4.1),define: { A: '"x"' }, RSS per instance afterBun.gc(true)and 1.5 s idle:28 live heaps fill one 4 MiB page with
mi_arena_pages_tblocks, so a batch of 100 or more finalizers strands almost every heap. The residual with 3000 instances alive at once (about 9 KiB per instance, 25 MB) is proportional to the peak live count, not to the total, and is the same with and without the fix. It is not part of this change.Stats from
heapStats().mimallocover 4 rounds of 200 instances (the test's fixture), release build:Over 10 rounds the fixed build plateaus at 102 MB; the unfixed one reaches 315 MB (+29 MB per round).
A C harness against Bun's release
static.c.oshows the mimalloc side alone: 100 live heaps with one 200-byte block each, created and destroyed in rounds, grow RSS by 150 KiB per destroyed heap without the fix (10 rounds: +146 MB) and plateau at +21 MB with it. 20 live heaps never fill a page and do not leak either way.Upstream used a plain
mi_freefor the heap meta data until the sub-process work (microsoft/mimalloc 360d7967b8). Sub-processes (mi_subproc_new) own separate arenas and heaps._mi_free_subproc_safeexists so that a sub-process teardown can free a block in the pages of another sub-process without collecting a page there. Bun has one sub-process, so every heap teardown now collects. Upstreamdev3still has the same code.The self-review raised: the
.patchcarrier that maintainers turned down before (#38168, #38199), so this is a pin bump like #40409 and #40138; anexport const MIMALLOC_COMMITthat broke theprocess.versionstest; a hash-keyedworkarounds.tsentry that would fail every future pin bump; a read ofpage->heapon an unowned page against the rule atmi_page_heap(the fork PR now derives the sub-process from the page's arena); and the overlap with #40130, which is closed as superseded. All addressed in this shape.Related PRs: #40130 was an earlier draft of the same mimalloc change, as a
.patch. It concluded that the patch has no effect in Bun because its RSS probe (64 instances alive in a ring) did not show the growth, and it had no test. The repro here shows the growth on every run. #32384 removes the per-instance heap fromBun.Transpiler; that would hide this one trigger but leave the allocator bug for every otherbun_alloc::Arenathat dies in a batch.Nothing in this PR is under
src/, so the automated fail-before check has nothing to revert. The fail-before run is the released binary (USE_SYSTEM_BUN=1 bun test test/js/bun/jsc/heapStats-mimalloc.test.ts).Pre-existing failures seen while running the suites, unrelated to this change:
test/js/bun/util/fuzzy-wuzzy.test.tsaborts the debug build injs_valkey.rs:1289(on_valkey_unsubscribe), and four 5 s timeouts intest/js/bun/util/under debug+ASAN.no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/jsc/heapStats-mimalloc.test.ts