Back bun_jsc::Strong with JSC's StrongSet and remove StrongRootBlock - #37842
Back bun_jsc::Strong with JSC's StrongSet and remove StrongRootBlock#37842robobun wants to merge 1 commit into
Conversation
JSC now stores Strong<> slots in StrongBlocks owned by a per-heap StrongSet (upstream ff64aee116d4, which also deletes HandleSet). Allocate bun_jsc::Strong slots from that set instead of the StrongRootBlock cells added in #35849: Bun__StrongRef__new returns the slot, Rust reads and writes it directly (the set is scanned unconditionally, so stores need no barrier), and Bun__StrongRef__delete returns it. Bun__StrongRef__set goes away. Delete StrongRootBlock, its iso subspace, the per-VM block list on JSVMClientData and the "Srb" marking constraint. heapStats() and getProtectedObjects() go back to JSC's own protected-cell walk, which covers StrongSet slots. The AbortSignal.timeout changes from #35849 are unaffected. Requires a WebKit that contains the StrongSet refactor.
|
Warning Review limit reached
Next review available in: 56 seconds Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (15)
Comment |
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Since it swaps the GC-root storage backing every bun_jsc::Strong, depends on an unlanded WebKit bump (CI is expected red), and carries a measured eden-pause trade-off at high handle counts, a maintainer sign-off is still warranted.
What was reviewed:
Strong.rsslot read/write/clear vs.JSC::Strong::set()semantics — plain store is correct (StrongSet is scanned unconditionally, no barrier).- Teardown ordering:
is_shutting_downguard still needed since StrongSet dies in ~VM beforedeinit_runtime_state. - Grepped for lingering
StrongRootBlock/StrongRefImpl/Bun__StrongRef__setreferences (none) and build-file entries. heapStats/getProtectedObjectsnow delegate to JSC's own walkers; test asserts both see 5000 armed timers and 0 after clear.
Extended reasoning...
Overview
This PR removes the custom StrongRootBlock GC-cell mechanism (added in #35849) that backed bun_jsc::Strong handles, and reverts to using JSC's own strong-handle storage — now JSC::StrongSet after upstream refactor ff64aee116d4 deleted HandleSet. Net -380 lines. Touches: Strong.rs (direct slot read/write, drops Bun__StrongRef__set FFI), StrongRef.{cpp,h} (allocate/deallocate via vm.heap.strongSet()), deletes StrongRootBlock.{h,cpp} + iso-subspace entries + the "Srb" marking constraint + JSVMClientData block-list state, BunJSCModule.h (heapStats/getProtectedObjects back to JSC's own walkers), root.h include swap, and comment-only edits in JSRef.rs/Response.rs.
Security risks
None. No user-controlled input paths; this is internal GC rooting machinery.
Level of scrutiny
High. This is the mechanism that keeps every bun_jsc::Strong-held JSValue alive across GC — timers, sockets, promises, etc. A mistake here is a use-after-GC or a leak affecting the whole runtime. Additionally:
- The PR depends on an unlanded WebKit bump (oven-sh/WebKit#404);
<JavaScriptCore/StrongSet.h>does not exist against the currently-pinned WebKit, so CI cannot validate it yet. Merging before the bump would break the build. - It carries a deliberate performance trade-off the author measured: eden STW at 1M live handles goes from ~3.8 ms to ~10.1 ms (roughly the pre-#35849
HandleSetcost). Below ~100k it's noise. This was requested by a maintainer, but the regression/simplification trade should be acknowledged by a human on the record.
Other factors
- The no-barrier store in
Impl::setmirrorsJSC::Strong::set()and is correct because the "Sh" constraint scans every StrongSet slot on every fixpoint; the PR comments this accurately. - The
is_shutting_downearly-return inImpl::destroyis retained with an updated rationale (StrongSet freed in ~VM phase C beforedeinit_runtime_statephase E) — matches the #31990 crash class. ExternTraits<Bun::StrongRef>::ExternTypechanged fromBun::StrongRefImpl*toJSC::JSValue*, consistent with the newusing StrongRef = std::unique_ptr<JSC::JSValue, StrongRefDeleter>; the Rustadoptside treats it as an opaqueNonNull<Impl>either way.- I grepped the whole repo for
StrongRootBlock,m_strongRootBlock*,StrongRefImpl, andBun__StrongRef__set— no remaining references, including build files. - The updated test asserts
protectedObjectTypeCounts.Timeout,protectedObjectCount, andgetProtectedObjects()all report 5000 armed timers and 0 after clear, and that noStrong*cell type appears inobjectTypeCounts— it fails on main (spareStrongRootBlockretained) and covers the observable contract this PR restores.
Given the WebKit dependency gating CI and the GC-critical surface, this should land with maintainer eyes rather than automated approval.
|
Status: ready for review, blocked on the WebKit bump. CI on this PR fails in every build lane with Additional verification since the description was written, using a local
|
Problem
bun_jsc::Strong(StrongRootBlock, from Strong: back bun_jsc::Strong with StrongRootBlock; free AbortSignal.timeout at wrapper GC #35849, about 470 lines), added because JSC's oldHandleSetscanned every handle on every eden GC.Strong<>slots in a per-heapStrongSet(ff64aee116d4) and deletesHandleSet, so the next WebKit bump has to touch this code anyway and bun's copy becomes a duplicate.StrongRootBlockcell still shows up inheapStats().objectTypeCounts.Fix
Strongis now one slot in the VM'sStrongSet, the storageJSC::Strong<>uses. C++ only allocates and releases the slot; Rust reads and writes it directly.StrongRootBlockand its marking constraint are deleted.StrongSetslot on every collection, so a plain store into a slot needs no barrier, the same storeJSC::Strong::set()makes.heapStats()andgetProtectedObjects()use JSC's protected-cell walks, which cover these slots.StrongSet.hdoes not exist, so CI is expected red until then. Verified on a local WebKit build with the commit cherry-picked: the updated timer-gc-roots test fails on main and passes here, and the jsc, timers, abort and worker suites pass.Background
bun_jsc::Strongis the Rust handle that pins a JS value so GC keeps it alive (an armed timer pins itsTimeoutthis way). Dropping it releases the pin, on the JS thread only.StrongSetis JSC's per-heap table of such pinned slots. It is visited whole on every collection, eden included, which is why a slot store needs no write barrier.StrongRootBlockwas a heap cell, so untouched blocks were skipped on eden; that skip is what this PR gives up.is_shutting_downearly return inStrong's drop stays: the VM, and itsStrongSet, is freed before the runtime state that still ownsStrongs, so releasing a slot during teardown would touch freed memory (the crash class in Release RuntimeState's JSC handles before tearing down the VM #31990).Original description
What does this PR do?
Removes
StrongRootBlock(added in #35849) and backsbun_jsc::Strongwith JSC's own strong-handle storage again, now that upstream JSC storesStrong<>slots inStrongBlocks owned by a per-heapStrongSet(upstream ff64aee116d4, "Introduce StrongBlock", which also deletesHandleSet).Depends on a WebKit bump that contains that refactor (the in-progress oven-sh/WebKit#404 upgrade, or any later one). Against the WebKit main currently pins,
<JavaScriptCore/StrongSet.h>does not exist, so CI on this PR is expected to be red until the bump lands; the PR should be rebased onto it. Everything below was verified against main's pin (7b763944) with ff64aee116d4 cherry-picked on top (the only conflicts were include style) and built in local WebKit mode.Requested in Slack by @Jarred-Sumner.
Changes
StrongRef.cpp/.h:Bun__StrongRef__newallocates a slot fromvm.heap.strongSet()and returns it;Bun__StrongRef__deletecallsStrongSet::deallocate. The slot pointer is the handle, as it was before Strong: back bun_jsc::Strong with StrongRootBlock; free AbortSignal.timeout at wrapper GC #35849.Bun__StrongRef__setis gone: likeJSC::Strong::set(), a store into a slot needs no barrier (the set is scanned unconditionally), soStrong.rsreads, writes and clears the slot directly and only calls into C++ to allocate and release.StrongRootBlock.{h,cpp}, its iso subspace entries, the block list / cursor / structure onJSVMClientData, and the"Srb"marking constraint.heapStats()/getProtectedObjects()go back to JSC'sprotectedObjectTypeCounts()/forEachProtectedCell(), which walk theStrongSet, so they still report everybun_jsc::Strongpinned object.root.hincludesStrongSet.hin place of the deletedHandleSet.h(the WebKit bump needs this line either way).is_shutting_downearly-return inImpl::destroystays:StrongSetdies with the heap in teardown phase C, whiledeinit_runtime_state(phase E) can still dropStrongs (the crash class described in Release RuntimeState's JSC handles before tearing down the VM #31990), so the release must still be skipped once teardown has started.AbortSignal.timeoutlifetime fix that was also part of Strong: back bun_jsc::Strong with StrongRootBlock; free AbortSignal.timeout at wrapper GC #35849 is unaffected.Net: 94 insertions, 474 deletions.
Trade-off, measured
Upstream's
StrongSet::visitAggregatevisits every slot on every collection, eden included; it does not have the generational skip thatStrongRootBlockgot from being a write-barriered cell. So this trades eden pause time at very high live-handle counts for less code and cheaper allocation. Release builds, same locally built JSC for both binaries, only the bun side differs (BUN_JSC_logGC=1, stop-the-world time summed per eden collection, 3 runs each):Strongs (armed timers)StrongRootBlock(main)The 1M number is roughly where #35849 measured the old
HandleSet(8.8 ms). Below ~100k live handles the scan is within noise.Per-handle cost (
setTimeout+clearTimeout, median of 5, two runs each):bench/snippets/set-timeout.mjs(20M)Tests
test/js/web/timers/timer-gc-roots.test.ts: the first test now checks thatheapStats().protectedObjectTypeCounts.Timeout,protectedObjectCountandgetProtectedObjects()all see 5000 armed timers and none after they are cleared, and that noStrong*cell type shows up inobjectTypeCounts. It fails on main (clearedObjectTypes: ["StrongRootBlock"], the retained spare block) and passes here; the other 8 tests in the file (callbacks reachable across GC,AbortSignal.timeoutlifetime) pass unchanged.Also run against the local StrongSet build with no failures attributable to this change:
test/js/bun/jsc/,test/js/web/timers/,test/js/web/abort/,worker.test.ts,worker-terminate-lifetime.test.ts,worker_threads.test.ts, andserve.test.ts/fetch.test.ts/socket.test.ts(the latter have the same sandbox-networking failures on the main binary built the same way).