Repository navigation
os: no address hint for OS allocations above 1 GiB - #34
Merged
Jarred-Sumner merged 1 commit intoSep 16, 2026
Merged
Conversation
robobun
added a commit
to oven-sh/bun
that referenced
this pull request
Sep 6, 2026
Bumps oven-sh/mimalloc from 6a64e1b to cc71a21 (oven-sh/mimalloc#34). Each WebAssembly.Memory reserves 4 GiB of address space through mi_malloc_aligned. mimalloc placed every huge allocation at a new hinted address, and each new 512 MiB region of the 2-level page map cost a 4 KiB sub-map that is never freed. The fork commit skips the hint above 1 GiB so the OS reuses the range that was just unmapped. Fixes #41459
This was referenced Sep 6, 2026
A block above arena_max_object_size is mapped from the OS and unmapped again on free. _mi_os_get_aligned_hint gave each of those mappings an address from a counter that only moves forward, so each one landed in a 512 MiB part of the address space that the page map had not seen before. The 2-level page map commits a 64 KiB sub-map for each such part and keeps it until the process ends: 64 KiB committed and 4 KiB resident per alloc/free cycle. JavaScriptCore reserves 4 GiB this way for every WebAssembly memory (oven-sh/bun#41459). Return no hint above 1 GiB, as mimalloc v2 did. The OS then gives the range that was just unmapped to the next mapping of that size, and the sub-map is used again. Upstream relaxed the v2 cap in 82c1688 (microsoft#1290). v3 only has a cap of 32 GiB, in secure mode. test-huge-cycle counts the 512 MiB parts in which 6 huge blocks start: 6 without the cap, 2 with it.
robobun
force-pushed
the
no-hint-for-huge-os-allocs
branch
from
September 16, 2026 02:27
cc71a21 to
7b4eb15
Compare
robobun
added a commit
to oven-sh/bun
that referenced
this pull request
Sep 16, 2026
Bumps oven-sh/mimalloc from 2014682 to 7b4eb15, the head of oven-sh/mimalloc#34. That is bun-dev3-v2 (which adds oven-sh/mimalloc#52 over the current pin) plus one commit. Each WebAssembly.Memory reserves 4 GiB of address space through mi_malloc_aligned. mimalloc placed every huge allocation at a new hinted address, and each new 512 MiB region of the 2-level page map cost a 64 KiB sub-map, 4 KiB of it resident, that is never freed. The fork commit skips the hint above 1 GiB so the OS reuses the range that was just unmapped. Fixes #41459
robobun
added a commit
to oven-sh/bun
that referenced
this pull request
Sep 16, 2026
… no address hint for OS allocations above 1 GiB Bumps oven-sh/mimalloc from 2014682 to 3a13a5f. - os: no address hint for OS allocations above 1 GiB (oven-sh/mimalloc#34) - page: a collect that is not forced leaves a large page with no block in use to the sweeps (oven-sh/mimalloc#52) Each WebAssembly.Memory reserves 4 GiB of address space through mi_malloc_aligned. mimalloc placed every huge allocation at a new hinted address, and each new 512 MiB region of the 2-level page map cost a 4 KiB sub-map that is never freed. Skipping the hint above 1 GiB lets the OS reuse the range that was just unmapped. Fixes #41459
robobun
added a commit
to oven-sh/bun
that referenced
this pull request
Sep 30, 2026
…its schedule The event loop hands the heaps of its thread to mimalloc's scavenger around every poll (us_loop_run_bun_tick). Each hand-off woke the scavenger: a futex wake on the loop thread and a turn of the scavenger, which found that the thread was not due. mimalloc paced the sweeps of a thread (100 ms) and not its wakes. Bump mimalloc to oven-sh/mimalloc#57 (which contains oven-sh/mimalloc#56). The scavenger puts a thread on its schedule when it passes a park of that thread over inside its window, and a park of a thread on the schedule issues no wake. The call in the event loop does not change. - packages/bun-usockets: the comment at the hand-off said that it costs a compare-and-swap per tick. It says what it costs now, and why a tick whose poll cannot block hands off too. - src/mimalloc_sys: the same for the doc comment of mi_on_thread_idle_start. - test/js/bun/jsc/heapStats-mimalloc.test.ts: 500 turns of a 1 ms timer put the scavenger thread to sleep 14 to 32 times. It was once for each turn. - bench/snippets/event-loop-turn.mjs: CPU time and sleeps of the threads for one turn of the loop, by the kind of turn. The pin also carries oven-sh/mimalloc#34, which is on bun-dev3-v2.
robobun
added a commit
to oven-sh/bun
that referenced
this pull request
Sep 30, 2026
…its schedule The event loop hands the heaps of its thread to mimalloc's scavenger around every poll (us_loop_run_bun_tick). Each hand-off woke the scavenger: a futex wake on the loop thread and a turn of the scavenger, which found that the thread was not due. mimalloc paced the sweeps of a thread (100 ms) and not its wakes. Bump mimalloc to oven-sh/mimalloc#57 (which contains oven-sh/mimalloc#56). The scavenger puts a thread on its schedule when it passes a park of that thread over inside its window, and a park of a thread on the schedule issues no wake. The call in the event loop does not change. - packages/bun-usockets: the comment at the hand-off said that it costs a compare-and-swap per tick. It says what it costs now, and why a tick whose poll cannot block hands off too. - src/mimalloc_sys: the same for the doc comment of mi_on_thread_idle_start. - test/js/bun/jsc/heapStats-mimalloc.test.ts: 500 turns of a 1 ms timer put the scavenger thread to sleep 15 to 26 times. It was once for each turn. - bench/snippets/event-loop-turn.mjs: CPU time and sleeps of the threads for one turn of the loop, by the kind of turn. The pin also carries oven-sh/mimalloc#34, which is on bun-dev3-v2.
robobun
added a commit
to oven-sh/bun
that referenced
this pull request
Oct 2, 2026
…its schedule The event loop hands the heaps of its thread to mimalloc's scavenger around every poll (us_loop_run_bun_tick). Each hand-off woke the scavenger: a futex wake on the loop thread and a turn of the scavenger, which found that the thread was not due. mimalloc paced the sweeps of a thread (100 ms) and not its wakes. Bump mimalloc to oven-sh/mimalloc#57 (which contains oven-sh/mimalloc#56). The scavenger puts a thread on its schedule when it passes a park of that thread over inside its window, and a park of a thread on the schedule issues no wake. The call in the event loop does not change. - packages/bun-usockets: the comment at the hand-off said that it costs a compare-and-swap per tick. It says what it costs now, and why a tick whose poll cannot block hands off too. - src/mimalloc_sys: the same for the doc comment of mi_on_thread_idle_start. - test/js/bun/jsc/heapStats-mimalloc.test.ts: 500 turns of a 1 ms timer put the scavenger thread to sleep 15 to 26 times. It was once for each turn. - bench/snippets/event-loop-turn.mjs: CPU time and sleeps of the threads for one turn of the loop, by the kind of turn. The pin also carries oven-sh/mimalloc#34, which is on bun-dev3-v2.
robobun
added a commit
to oven-sh/bun
that referenced
this pull request
Oct 2, 2026
…its schedule The event loop hands the heaps of its thread to mimalloc's scavenger around every poll (us_loop_run_bun_tick). Each hand-off woke the scavenger: a futex wake on the loop thread and a turn of the scavenger, which found that the thread was not due. mimalloc paced the sweeps of a thread (100 ms) and not its wakes. Bump mimalloc to oven-sh/mimalloc#57 (which contains oven-sh/mimalloc#56). The scavenger puts a thread on its schedule when it passes a park of that thread over inside its window, and a park of a thread on the schedule issues no wake. The call in the event loop does not change. - packages/bun-usockets: the comment at the hand-off said that it costs a compare-and-swap per tick. It says what it costs now, and why a tick whose poll cannot block hands off too. - test/js/bun/jsc/heapStats-mimalloc.test.ts: 500 turns of a 1 ms timer put the scavenger thread to sleep 15 to 26 times. It was once for each turn. - bench/snippets/event-loop-turn.mjs: CPU time and sleeps of the threads for one turn of the loop, by the kind of turn. The pin also carries oven-sh/mimalloc#34, which is on bun-dev3-v2.
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.
Problem
new WebAssembly.Memoryin bun 1.4.x on Linux leaks about 4 KiB of RSS that is never returned (Everynew WebAssembly.Memoryleaks ~4 KB of RSS that GC never returns (1.4.0 through 1.4.3-canary; 1.3.14 is flat) bun#41459). Bun 1.3.14, on mimalloc v2 and libpas, was flat. A release build of bun main with the current pin (2014682) still ends the issue's repro at +85 MB after 20000 memories.mi_malloc_aligned. Blocks abovearena_max_object_sizeare mapped from the OS directly, and_mi_os_get_aligned_hintgives each one an address from a counter that only moves forward. Each new 512 MiB region of the 2-level page map allocates a 64 KiB sub-map (mi_page_map_alloc_submap_at), and sub-maps are only freed in_mi_page_map_unsafe_destroy. 64 KiB of committed memory, 4 KiB of it resident, stays behind per alloc/free cycle.Fix
_mi_os_get_aligned_hintfor sizes above 1 GiB. The OS then picks the address and hands back the range that was just unmapped, so the same sub-map is reused.test/test-huge-cycle.c: it allocates and frees 6 blocks abovemi_arena_max_object_size()(and above 1 GiB), two at a time, and counts the 512 MiB regions in which a block starts. Without the cap: 6 regions. With it: 2. Checked locally in Release, Release C++, Debug, Debug FULL, Secure, Guarded, and with Bun's defines (MI_FREE_USE_PAGEMAP=1). It is left out of sanitizer builds: a debug build reads every fresh block back byte by byte, which is too slow for blocks above 1 GiB under a sanitizer.Verification
src/static.cwith Bun's defines (MI_STATIC_LIB,MI_BUILD_RELEASE,MI_FREE_USE_PAGEMAP=1,MI_DEFAULT_ALLOW_THP=0), 2000 cycles ofmi_malloc_aligned(4 GiB + 64 KiB, 4096)+mi_free. Before: +9168 kB RSS and +129024 kB committed (64.5 KiB per cycle). After: +0 kB, the same address every cycle. With 200 blocks live at a time over 20000 cycles: +152 kB RSS. 1 GiB blocks show no growth either way.ctestpasses here (35 of 35, Release).