Repository navigation
Let idle FTL code age out once the heap goes quiet (WebKit#557/#558) - #41297
Conversation
…-sh/WebKit#557) No-Verification-Needed: dependency version bump only
No-Verification-Needed: dependency version bump only
… age out idle FTL code in them, and only in them The full collections issued after BUN_IDLE_GC_SECONDS of heap quiet now go through JSC__VM__collectAsyncIdle, which sets GCRequest::isIdle (oven-sh/WebKit#558). JSC lets FTL code and counter-less DFG code go only in such a collection, so a program that forces Bun.gc() on a timer while running hot code keeps its optimized code. test: with eager JIT TTLs, the idle collections take a 40-function FTL workload from ~90 FunctionCodeBlocks to a handful; BUN_IDLE_GC_SECONDS=0 plus Bun.gc(true) every 0.7 s leaves them in place.
No-Verification-Needed: dependency version bump only
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review. WalkthroughThe PR updates the WebKit revision and adds an idle-specific asynchronous garbage collection path. The garbage collection controller uses this path for idle collections. New tests compare FTL code aging after idle and explicit collections. ChangesIdle garbage collection
Possibly related PRs
Merge Risk: ⚪ Minimal · up to Idle garbage collection can age eligible optimized code after quiet periods to reduce retained memory, while explicitly forced collections preserve that code. No unresolved merge-readiness risk remains. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Full details: Description checkExplanation The description explains the WebKit update, Bun changes, expected behavior, measured memory improvements, and verification tests. It uses different headings from the template, but it provides the required change and verification information in sufficient detail. Comment |
No-Verification-Needed: test-only assertion order
… leak
Problem
=======
JSC's internal allocator (libpas) leaks ~7KB per Bun.$ invocation on the
1.4.0 line. Repro (`for (i<500000) await $`echo hello``) grows RSS
~69MB per 10k iterations, linear, never converging — ~3.5GB after 500k
iterations, OOM around 430k under a 3GB limit. Control loops (plain
await, Bun.spawn) hold flat RSS on the same unfixed binary, so the leak
is Bun.$-specific.
Root cause
==========
Upstream fixed this in the 1.4.0 → 1.4.1 window by building WebKit with
USE_MIMALLOC=ON / USE_EXTERNAL_MIMALLOC=ON ("JSC allocates through Bun's
mimalloc, not libpas") and bumping the mimalloc pin. This PR ports that
allocator state, the OHOS CI wiring for it, and the engine adaptations
the new WebKit source requires.
Changes
=======
Allocator state (matches the upstream 1.4.1 pins):
- scripts/build/deps/webkit.ts: WEBKIT_VERSION → 6119947592b6; local
WebKit cmake gains USE_MIMALLOC / USE_EXTERNAL_MIMALLOC (non-ASAN)
- scripts/build/deps/mimalloc.ts: MIMALLOC_COMMIT → 6a64e1ba
OHOS CI gates (build-github, full-test, container-test, build-rust,
release): WEBKIT_REF → the same pin. The gates build WebKit from source
via --webkit=local; the old pins predate the USE_MIMALLOC cmake option,
which made the switch a silent no-op there. ohos-release.yml's old
default ref named a branch that does not exist upstream, so its checkout
silently fell back to a runner-local WebKit checkout.
Engine adaptations required by the new WebKit source (ported from the
upstream 1.4.0..1.4.1 upgrade commits 11fb730 / a92d84e /
561641b):
- EncodeURIComponent.cpp includes <wtf/HexNumber.h> itself (the engine
no longer provides it transitively)
- GlobalObjectMethodTable::moduleLoaderFetch gains a trailing
`const String& referrer` parameter; ZigGlobalObject and the bake
implementation/call sites updated
- the @newPromiseCapability private builtin was removed upstream: the
six call sites in node:events / node:util / node:dgram /
node:_http_server switch to $newPromise plus the resolve/reject
WithFirstResolvingFunctionCallCheck variants (the old form fails at
module-load time, which compilation does not catch)
- relaxAdoptionRequirement was removed from the engine: drop all 10 call
sites (FetchHeaders create paths, ScriptExecutionContext constructors)
- VM::heap.collectAsync takes a CollectionScope::Full parameter: the
JSC__VM__collectAsync FFI (C++ wrapper, header decl, Rust decl) gains
`bool full`; the Rust wrapper passes false, preserving incremental
collection behavior
OHOS gate build fixes (from the OHOS branches' post-1.4.1 experience):
- FindThreads' pthread_cancel probe does not compile on OHOS musl and
its fallback invents a nonexistent -lpthreads that kills the final jsc
link: define pthread_cancel to nothing for cfg.ohos
- ICU_INCLUDE_DIR passed explicitly (the new engine's cmake needs it)
- nested WebKit build serialized via a new NestedCmakeBuild.parallel
option (generated-header copy rules race on mounted filesystems)
Verification
============
- Same-codebase release binaries reproduce both sides: 1.4.0-line builds
(this fork's linux-x64/windows-x64 and upstream 1.4.0) grow to ~3.5GB
RSS after 500k spawns; upstream 1.4.1/1.4.2 hold flat at ~47MB (100k
iterations). The repro fails on unfixed builds.
- Ported regression tests pin the migrated promise paths and the
JS-visible engine changes.
Follow-ups
==========
- The bun-webkit formula must be rebuilt at 6119947592b6 for the
OHOS_WEBKIT_ROOT prebuilt path to pick up USE_MIMALLOC.
- The GC-cadence commits (oven-sh#41083/oven-sh#41297) ride with the next routine
upstream merge.
… leak
Problem
=======
JSC's internal allocator (libpas) leaks ~7KB per Bun.$ invocation on the
1.4.0 line. Repro (`for (i<500000) await $`echo hello``) grows RSS
~69MB per 10k iterations, linear, never converging — ~3.5GB after 500k
iterations, OOM around 430k under a 3GB limit. Control loops (plain
await, Bun.spawn) hold flat RSS on the same unfixed binary, so the leak
is Bun.$-specific.
Root cause
==========
Upstream fixed this in the 1.4.0 → 1.4.1 window by building WebKit with
USE_MIMALLOC=ON / USE_EXTERNAL_MIMALLOC=ON ("JSC allocates through Bun's
mimalloc, not libpas") and bumping the mimalloc pin. This PR ports that
allocator state, the OHOS CI wiring for it, and the engine adaptations
the new WebKit source requires.
Changes
=======
Allocator state (matches the upstream 1.4.1 pins):
- scripts/build/deps/webkit.ts: WEBKIT_VERSION → 6119947592b6; local
WebKit cmake gains USE_MIMALLOC / USE_EXTERNAL_MIMALLOC (non-ASAN)
- scripts/build/deps/mimalloc.ts: MIMALLOC_COMMIT → 6a64e1ba
OHOS CI gates (build-github, full-test, container-test, build-rust,
release): WEBKIT_REF → the same pin. The gates build WebKit from source
via --webkit=local; the old pins predate the USE_MIMALLOC cmake option,
which made the switch a silent no-op there. ohos-release.yml's old
default ref named a branch that does not exist upstream, so its checkout
silently fell back to a runner-local WebKit checkout.
Engine adaptations required by the new WebKit source (ported from the
upstream 1.4.0..1.4.1 upgrade commits 11fb730 / a92d84e /
561641b):
- EncodeURIComponent.cpp includes <wtf/HexNumber.h> itself (the engine
no longer provides it transitively)
- GlobalObjectMethodTable::moduleLoaderFetch gains a trailing
`const String& referrer` parameter; ZigGlobalObject and the bake
implementation/call sites updated
- the @newPromiseCapability private builtin was removed upstream: the
six call sites in node:events / node:util / node:dgram /
node:_http_server switch to $newPromise plus the resolve/reject
WithFirstResolvingFunctionCallCheck variants (the old form fails at
module-load time, which compilation does not catch)
- relaxAdoptionRequirement was removed from the engine: drop all 10 call
sites (FetchHeaders create paths, ScriptExecutionContext constructors)
- VM::heap.collectAsync takes a CollectionScope::Full parameter: the
JSC__VM__collectAsync FFI (C++ wrapper, header decl, Rust decl) gains
`bool full`; the Rust wrapper passes false, preserving incremental
collection behavior
OHOS gate build fixes (from the OHOS branches' post-1.4.1 experience):
- FindThreads' pthread_cancel probe does not compile on OHOS musl and
its fallback invents a nonexistent -lpthreads that kills the final jsc
link: define pthread_cancel to nothing for cfg.ohos
- ICU_INCLUDE_DIR passed explicitly (the new engine's cmake needs it)
- nested WebKit build serialized via a new NestedCmakeBuild.parallel
option (generated-header copy rules race on mounted filesystems)
Verification
============
- Same-codebase release binaries reproduce both sides: 1.4.0-line builds
(this fork's linux-x64/windows-x64 and upstream 1.4.0) grow to ~3.5GB
RSS after 500k spawns; upstream 1.4.1/1.4.2 hold flat at ~47MB (100k
iterations). The repro fails on unfixed builds.
- Ported regression tests pin the migrated promise paths and the
JS-visible engine changes.
Follow-ups
==========
- The bun-webkit formula must be rebuilt at 6119947592b6 for the
OHOS_WEBKIT_ROOT prebuilt path to pick up USE_MIMALLOC.
- The GC-cadence commits (oven-sh#41083/oven-sh#41297) ride with the next routine
upstream merge.
… leak
Problem
=======
JSC's internal allocator (libpas) leaks ~7KB per Bun.$ invocation on the
1.4.0 line. Repro (`for (i<500000) await $`echo hello``) grows RSS
~69MB per 10k iterations, linear, never converging — ~3.5GB after 500k
iterations, OOM around 430k under a 3GB limit. Control loops (plain
await, Bun.spawn) hold flat RSS on the same unfixed binary, so the leak
is Bun.$-specific.
Root cause
==========
Upstream fixed this in the 1.4.0 → 1.4.1 window by building WebKit with
USE_MIMALLOC=ON / USE_EXTERNAL_MIMALLOC=ON ("JSC allocates through Bun's
mimalloc, not libpas") and bumping the mimalloc pin. This PR ports that
allocator state, the OHOS CI wiring for it, and the engine adaptations
the new WebKit source requires.
Changes
=======
Allocator state (matches the upstream 1.4.1 pins):
- scripts/build/deps/webkit.ts: WEBKIT_VERSION → 6119947592b6; local
WebKit cmake gains USE_MIMALLOC / USE_EXTERNAL_MIMALLOC (non-ASAN)
- scripts/build/deps/mimalloc.ts: MIMALLOC_COMMIT → 6a64e1ba
OHOS CI gates (build-github, full-test, container-test, build-rust,
release): WEBKIT_REF → the same pin. The gates build WebKit from source
via --webkit=local; the old pins predate the USE_MIMALLOC cmake option,
which made the switch a silent no-op there. ohos-release.yml's old
default ref named a branch that does not exist upstream, so its checkout
silently fell back to a runner-local WebKit checkout.
Engine adaptations required by the new WebKit source (ported from the
upstream 1.4.0..1.4.1 upgrade commits 11fb730 / a92d84e /
561641b):
- EncodeURIComponent.cpp includes <wtf/HexNumber.h> itself (the engine
no longer provides it transitively)
- GlobalObjectMethodTable::moduleLoaderFetch gains a trailing
`const String& referrer` parameter; ZigGlobalObject and the bake
implementation/call sites updated
- the @newPromiseCapability private builtin was removed upstream: the
six call sites in node:events / node:util / node:dgram /
node:_http_server switch to $newPromise plus the resolve/reject
WithFirstResolvingFunctionCallCheck variants (the old form fails at
module-load time, which compilation does not catch)
- relaxAdoptionRequirement was removed from the engine: drop all 10 call
sites (FetchHeaders create paths, ScriptExecutionContext constructors)
- VM::heap.collectAsync takes a CollectionScope::Full parameter: the
JSC__VM__collectAsync FFI (C++ wrapper, header decl, Rust decl) gains
`bool full`; the Rust wrapper passes false, preserving incremental
collection behavior
OHOS gate build fixes (from the OHOS branches' post-1.4.1 experience):
- FindThreads' pthread_cancel probe does not compile on OHOS musl and
its fallback invents a nonexistent -lpthreads that kills the final jsc
link: define pthread_cancel to nothing for cfg.ohos
- ICU_INCLUDE_DIR passed explicitly (the new engine's cmake needs it)
- nested WebKit build serialized via a new NestedCmakeBuild.parallel
option (generated-header copy rules race on mounted filesystems)
Verification
============
- Same-codebase release binaries reproduce both sides: 1.4.0-line builds
(this fork's linux-x64/windows-x64 and upstream 1.4.0) grow to ~3.5GB
RSS after 500k spawns; upstream 1.4.1/1.4.2 hold flat at ~47MB (100k
iterations). The repro fails on unfixed builds.
- Ported regression tests pin the migrated promise paths and the
JS-visible engine changes.
Follow-ups
==========
- The bun-webkit formula must be rebuilt at 6119947592b6 for the
OHOS_WEBKIT_ROOT prebuilt path to pick up USE_MIMALLOC.
- The GC-cadence commits (oven-sh#41083/oven-sh#41297) ride with the next routine
upstream merge.
CI round 3 (infra): the pre-sign step failed writing bun-signed
(EACCES) - a stale root-owned signed output from an earlier run blocks
the runner-user write on the persistent self-hosted workspace. Remove
the stale signed file before signing.
… leak
Problem
=======
JSC's internal allocator (libpas) leaks ~7KB per Bun.$ invocation on the
1.4.0 line. Repro (`for (i<500000) await $`echo hello``) grows RSS
~69MB per 10k iterations, linear, never converging — ~3.5GB after 500k
iterations, OOM around 430k under a 3GB limit. Control loops (plain
await, Bun.spawn) hold flat RSS on the same unfixed binary, so the leak
is Bun.$-specific.
Root cause
==========
Upstream fixed this in the 1.4.0 → 1.4.1 window by building WebKit with
USE_MIMALLOC=ON / USE_EXTERNAL_MIMALLOC=ON ("JSC allocates through Bun's
mimalloc, not libpas") and bumping the mimalloc pin. This PR ports that
allocator state, the OHOS CI wiring for it, and the engine adaptations
the new WebKit source requires.
Changes
=======
Allocator state (matches the upstream 1.4.1 pins):
- scripts/build/deps/webkit.ts: WEBKIT_VERSION → 6119947592b6; local
WebKit cmake gains USE_MIMALLOC / USE_EXTERNAL_MIMALLOC (non-ASAN)
- scripts/build/deps/mimalloc.ts: MIMALLOC_COMMIT → 6a64e1ba
OHOS CI gates (build-github, full-test, container-test, build-rust,
release): WEBKIT_REF → the same pin. The gates build WebKit from source
via --webkit=local; the old pins predate the USE_MIMALLOC cmake option,
which made the switch a silent no-op there. ohos-release.yml's old
default ref named a branch that does not exist upstream, so its checkout
silently fell back to a runner-local WebKit checkout.
Engine adaptations required by the new WebKit source (ported from the
upstream 1.4.0..1.4.1 upgrade commits 11fb730 / a92d84e /
561641b):
- EncodeURIComponent.cpp includes <wtf/HexNumber.h> itself (the engine
no longer provides it transitively)
- GlobalObjectMethodTable::moduleLoaderFetch gains a trailing
`const String& referrer` parameter; ZigGlobalObject and the bake
implementation/call sites updated
- the @newPromiseCapability private builtin was removed upstream: the
six call sites in node:events / node:util / node:dgram /
node:_http_server switch to $newPromise plus the resolve/reject
WithFirstResolvingFunctionCallCheck variants (the old form fails at
module-load time, which compilation does not catch)
- relaxAdoptionRequirement was removed from the engine: drop all 10 call
sites (FetchHeaders create paths, ScriptExecutionContext constructors)
- VM::heap.collectAsync takes a CollectionScope::Full parameter: the
JSC__VM__collectAsync FFI (C++ wrapper, header decl, Rust decl) gains
`bool full`; the Rust wrapper passes false, preserving incremental
collection behavior
OHOS gate build fixes (from the OHOS branches' post-1.4.1 experience):
- FindThreads' pthread_cancel probe does not compile on OHOS musl and
its fallback invents a nonexistent -lpthreads that kills the final jsc
link: define pthread_cancel to nothing for cfg.ohos
- ICU_INCLUDE_DIR passed explicitly (the new engine's cmake needs it)
- nested WebKit build serialized via a new NestedCmakeBuild.parallel
option (generated-header copy rules race on mounted filesystems)
Verification
============
- Same-codebase release binaries reproduce both sides: 1.4.0-line builds
(this fork's linux-x64/windows-x64 and upstream 1.4.0) grow to ~3.5GB
RSS after 500k spawns; upstream 1.4.1/1.4.2 hold flat at ~47MB (100k
iterations). The repro fails on unfixed builds.
- Ported regression tests pin the migrated promise paths and the
JS-visible engine changes.
Follow-ups
==========
- The bun-webkit formula must be rebuilt at 6119947592b6 for the
OHOS_WEBKIT_ROOT prebuilt path to pick up USE_MIMALLOC.
- The GC-cadence commits (oven-sh#41083/oven-sh#41297) ride with the next routine
upstream merge.
CI round 3 (infra): the pre-sign step failed writing bun-signed
(EACCES) - docker cp restores container ownership (root), and on the
persistent self-hosted workspace the build/release-ohos directory ends
up root-owned, so the runner-user pre-sign cannot create the signed
output. Reclaim ownership (sudo chown, chmod fallback) right after the
docker cp.
What
WebKit →
40e43a82a755(oven-sh/WebKit#557 + #558).That change lets JSC age out FTL code (and DFG code compiled without tier-up checks) once the mutator has gone quiet. Such code has no execution counter, so since #41083 / WebKit#542 it never aged, and each block pinned its baseline alternative plus every baseline CodeBlock it inlined. Now a full collection lets it go once ≤ 1 MB has been allocated since the previous full collection looked at it for ≥ 30 s — which is exactly what the idle collections
GarbageCollectionControlleralready schedules (BUN_IDLE_GC_SECONDS=10,65,65) produce. Busy processes are unaffected: their full collections are allocation-driven and always renew the lease.Bun side:
GarbageCollectionControllernow issues those idle full collections throughJSC__VM__collectAsyncIdle, which tags the request (GCRequest::isIdle, WebKit#558). JSC lets such code go only in a tagged collection, so a program that forcesBun.gc()on a timer while running hot, non-allocating code keeps its optimized code; allocation-paced and memory-pressure collections never drop it either.Numbers
express / hono with a library-heavy handler (ajv, zod, markdown-it+highlight.js, js-yaml, protobufjs, graphql, handlebars, sucrase, …), 5,000-request burst at c=50, then idle. Same Bun commit, WebKit#557 option off vs on, 3 runs each:
FunctionCodeBlocks (express)Nothing changes at +45 s (before the second idle collection) or under load; a 2-minute sustained-load run compiled the same number of FTL functions with the option on and off.
For context, idle +240 s RSS across versions on the same apps (5 runs, medians): express 1.3.14 245 / 1.4.0 224 / canary 192 → ~171 with this; hono 228 / 207 / 173 → ~152; elysia 230 / 203 / 170 → ~149; next 339 / 296 / 244 → ~223.
Tests
test/js/bun/gc/gc-controller-cadence.test.tsgains two cases (eager JIT TTLs so it runs in seconds): the idle collections take a 40-function FTL workload from ~90FunctionCodeBlocks to a handful;BUN_IDLE_GC_SECONDS=0plusBun.gc(true)every 0.7 s leaves them in place. Both fail on current bun. The engine side isJSTests/stress/codeblock-aging-ftl-idle*.jsin WebKit#557/#558.