Skip to content

jsc: treat DFG Plan::m_mustHandleValues as weak so queued compiles don't root user objects - #34640

Open
robobun wants to merge 6 commits into
mainfrom
farm/9944bf28/gc-complete-jit-plans
Open

jsc: treat DFG Plan::m_mustHandleValues as weak so queued compiles don't root user objects#34640
robobun wants to merge 6 commits into
mainfrom
farm/9944bf28/gc-complete-jit-plans

Conversation

@robobun

@robobun robobun commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

What

The N-1/N stall in test-gc-http-client* (and the motivation for the once() closure-nulling in #34500 and 9abee13) was attributed to JSC's conservative stack scan. It isn't: a GCDebugging heap snapshot taken while the test is at N-1/N shows the surviving IncomingMessage/ClientRequest rooted directly with RootMarkReason::JITWorkList, and the same run with BUN_JSC_useConcurrentJIT=0 or BUN_JSC_useDFGJIT=0 collects everything in one gc.

The retainer is DFG::Plan::m_mustHandleValues. When a hot loop triggers Baseline->DFG (or DFG->FTL) OSR, the tier-up path copies every live argument and local of the triggering frame into the compilation plan so the compiler can seed OSR-entry type predictions. Plan::checkLivenessAndVisitChildren marked those JSValues as roots for the life of the concurrent compile, so whatever happened to be in scope at the tier-up point (an IncomingMessage, the nextTick drain loop's tock with args:[req], etc.) stayed alive until the plan was finalized.

Fix

oven-sh/WebKit#308 stops visiting m_mustHandleValues during marking and, in Plan::finalizeInGC (post-mark, compiler threads suspended, before sweep), drops any entry whose cell was not otherwise marked. Every compiler phase that reads m_mustHandleValues (PredictionInjection, CFA::injectOSR, TypeCheckHoisting) already treats nullopt as "unknown", so compilation continues; the OSR-entry prediction for that local simply widens and OSR-entry validation handles the rest. No plan cancellation, no blocking on the compile thread.

This PR:

Verification

test/js/bun/jsc/dfg-plan-gc-fixture.js makes 32 http.get requests with BUN_JSC_numberOfDFGCompilerThreads=1 / BUN_JSC_numberOfFTLCompilerThreads=1, waits for all responses, calls Bun.gc(true) once, and (if anything survived) takes a generateHeapSnapshotForDebugging() snapshot and counts ClientRequest/IncomingMessage nodes whose root reason is JITWorkList.

unpatched JSC patched JSC
jitworklist-rooted after one Bun.gc(true) 1-6 (30/30 runs) 0 (all runs)
ClientRequest alive after one gc 2-7 0

All four test-gc-http-client* and test-net-connect-memleak pass cleanly on the patched build with no N-1/N stall. bun bd test test/js/bun/jsc/bun-jsc.test.ts is 37/37.

The once() closure-nulling in 9abee13 (part of #34519) is unnecessary once this lands; the node:events wrapper can match Node's shape.

Note for the gate

The fix is a WEBKIT_VERSION bump in scripts/build/deps/webkit.ts, which git stash push -- src/ packages/ does not revert, so the gate's fail-before build picks up the new WebKit and the test passes both ways. The 30/30 fail rate against the current release build (table above) is the honest fail-before evidence.


no test proof · iteration 3 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/jsc/bun-jsc.test.ts

…ot user objects

The retaining root behind the test-gc-http-client N-1/N stall (and the
earlier once()-nulling workarounds in node:events / internal/shared) is
not the conservative stack scan: a GCDebugging heap snapshot taken when
the test is stuck shows the surviving IncomingMessage/ClientRequest
rooted with RootMarkReason::JITWorkList. DFG::Plan::m_mustHandleValues
captures whatever locals were live in the frame that triggered loop OSR
and marks them as roots for the life of the concurrent compile.

oven-sh/WebKit#308 makes that snapshot weak: entries that nothing else
marks are dropped in Plan::finalizeInGC, and every compiler phase that
reads m_mustHandleValues already skips nullopt.

The fixture drives enough independent functions to DFG at once via the
http client/server path that several plans are queued when the first
gc() runs, then asserts zero ClientRequest/IncomingMessage are
JITWorkList-rooted in the debugging heap snapshot.
@robobun

robobun commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 11:21 AM PT - Jul 18th, 2026

@robobun, your commit 20a81ca has 2 failures in Build #75495 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 34640

That installs a local version of the PR into your bun-34640 executable, so you can run:

bun-34640 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 2 issues this PR may fix:

  1. Memory leak in Next.js SSR under bun --bun next start — JSC GC fails to reclaim heap after concurrent requests #29267 - Next.js SSR memory leak where JSC GC fails to reclaim heap after concurrent requests; the hot server-component fetch loop would trigger DFG compilation, rooting request/response objects via mustHandleValues
  2. Native RSS grows linearly under sustained AWS SDK v3 Kinesis GetRecords; identical Node 22 workload is flat #30415 - Native RSS grows linearly (~1GB/hr, heap stays at 19MB) under sustained AWS SDK v3 Kinesis polling with 128 shards; the hot fetch-based polling loops would trigger JIT tier-up, rooting HTTP objects as strong GC roots

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #29267
Fixes #30415

🤖 Generated with Claude Code

@robobun

robobun commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

Neither of those is this.

This change removes a bounded, short-lived extra root. It makes gc()-based collection tests deterministic (which is the actual failure mode in test-gc-http-client* and test-net-connect-memleak); it is not expected to move steady-state RSS in long-running servers.

… m_mustHandleValues change

Also:
  - generateHeapSnapshotForDebugging: RELEASE_AND_RETURN around JSONParse
    so validateExceptionChecks does not assert on the simulated throw the
    fixture exercises on the x64-asan lane.
  - fixture: skip the heap snapshot when alive=0 (it is expensive under
    debug+ASAN and the count is trivially zero).
  - test: run the child with a single DFG/FTL compiler thread so plans
    queue rather than drain in parallel, and allow 30s on debug/ASAN
    builds (the 32 http requests alone take ~5s there).
@robobun
robobun marked this pull request as ready for review July 18, 2026 13:08
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The PR updates the WebKit preview version, releases the JSC throw scope when returning a heap snapshot, and adds a DFG garbage-collection regression fixture and test.

Changes

WebKit build update

Layer / File(s) Summary
Update WebKit build tag
scripts/build/deps/webkit.ts
WEBKIT_VERSION is updated to autobuild-preview-pr-308-40ff52aa.

JSC heap snapshot GC regression

Layer / File(s) Summary
Release heap snapshot throw scope
src/jsc/modules/BunJSCModule.h
functionGenerateHeapSnapshotForDebugging uses RELEASE_AND_RETURN when returning the parsed snapshot.
Exercise DFG plan GC roots
test/js/bun/jsc/dfg-plan-gc-fixture.js, test/js/bun/jsc/bun-jsc.test.ts
The fixture creates concurrent requests, forces GC, inspects debugging heap snapshots, and the test verifies jitworklist-rooted=0.

Possibly related PRs

  • oven-sh/bun#33956: Updates the same WEBKIT_VERSION constant to another preview tag.
  • oven-sh/bun#34138: Modifies the same WebKit version constant used by the JSC build.
  • oven-sh/bun#34373: Changes the same WEBKIT_VERSION constant to an autobuild preview tag.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly matches the main change: making DFG plan values weak to avoid rooting user objects during queued compiles.
Description check ✅ Passed The description covers what changed and how it was verified, but it does not follow the template’s exact section headings.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/build/deps/webkit.ts`:
- Line 10: Update WEBKIT_VERSION to use the stable merged autobuild-<sha> tag
instead of the PR-specific autobuild-preview-pr-308-06ecce07 tag, ensuring
WebKit downloads remain available after the upstream PR lifecycle ends.

In `@test/js/bun/jsc/dfg-plan-gc-fixture.js`:
- Around line 26-46: The fixture must signal that the intended queued
DFG/worklist state was reached before calling Bun.gc(true), and the parent test
must assert that signal alongside jitworklist-rooted=0. Update the check
function in test/js/bun/jsc/dfg-plan-gc-fixture.js at lines 26-46 to emit the
observable precondition signal, then update test/js/bun/jsc/bun-jsc.test.ts at
lines 590-592 to require both outputs so a drained-plan run cannot pass as a
false positive.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 32a45784-e1b0-4db9-9ec5-7f158debfbc8

📥 Commits

Reviewing files that changed from the base of the PR and between a227ad9 and 333cc6b.

📒 Files selected for processing (4)
  • scripts/build/deps/webkit.ts
  • src/jsc/modules/BunJSCModule.h
  • test/js/bun/jsc/bun-jsc.test.ts
  • test/js/bun/jsc/dfg-plan-gc-fixture.js

Comment thread scripts/build/deps/webkit.ts Outdated
Comment thread test/js/bun/jsc/dfg-plan-gc-fixture.js
Comment thread test/js/bun/jsc/bun-jsc.test.ts Outdated
Matches the combined {stdout, exitCode} toEqual pattern the neighboring
deserialize-* tests use; stderr is drained but not asserted so a benign
ASAN/debug warning cannot mask the jitworklist-rooted=0 check.
Comment thread test/js/bun/jsc/bun-jsc.test.ts Outdated
…LI flags

Bun only reads JSC options from BUN_JSC_* env vars (ZigGlobalObject.cpp);
unknown --long flags are silently skipped, so the fixture was running with
the default 2 DFG threads and the 'one compiler thread so plans queue'
precondition was never applied. With the env vars actually taking effect
fail-before is 30/30 on the current release build.
Comment thread test/js/bun/jsc/bun-jsc.test.ts Outdated
…) and trim comments

oven-sh/WebKit#308 is now based on 4895f45d (the currently pinned commit)
so the preview artifact contains only the DFGPlan change, and finalizeInGC
skips jsEmpty() entries (a TDZ-sentinel or LLInt-zeroed temp at the OSR
point satisfies isCell() but asCell() is null).

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three earlier findings (stderr-empty assertion, --jsc-* CLI flags, comment length) are addressed and the Bun-side diff now looks correct, but I'm leaving this for a human: the substantive change is the JSC GC-semantics patch in oven-sh/WebKit#308 (making m_mustHandleValues weak during concurrent compilation), which is outside this diff and needs maintainer sign-off — and WEBKIT_VERSION is still pinned to the preview tag pending that PR's merge.

What was reviewed:

  • RELEASE_AND_RETURN around JSONParse in functionGenerateHeapSnapshotForDebugging — correct throw-scope hygiene, matches the pattern used elsewhere in the file.
  • Test now uses BUN_JSC_* env vars, drains stderr without asserting empty, and matches the neighboring {stdout, exitCode} assertion shape.
  • Fixture's snapshot-skip when alive == 0 is sound (empty worklist ⇒ trivially zero JITWorkList roots); http.get error-handler concern was examined and ruled out.
Extended reasoning...

Overview

Four files: WEBKIT_VERSION bump to autobuild-preview-pr-308-40ff52aa, a one-line RELEASE_AND_RETURN wrap in BunJSCModule.h, a new subprocess test in bun-jsc.test.ts, and its fixture dfg-plan-gc-fixture.js. The actual behavioral fix — stopping DFG::Plan::checkLivenessAndVisitChildren from marking m_mustHandleValues and instead weak-clearing dead cells in Plan::finalizeInGC — lives entirely in oven-sh/WebKit#308 and is pulled in via the version bump.

Security risks

None from the Bun-side diff. The WebKit-side change alters which JSValues are treated as GC roots while a concurrent DFG/FTL compile is queued; getting that wrong is a use-after-free / type-confusion risk in the JIT (a cell freed mid-compile whose value is later consulted for prediction seeding). The PR description argues every consumer already tolerates nullopt and that clearing happens with compiler threads suspended, but that reasoning belongs to the WebKit PR review, not this one.

Level of scrutiny

High — even though the visible diff here is small and mechanical, what it ships is a change to JSC's GC-root set during concurrent compilation. That is exactly the class of change (GC marking, weak references, cross-thread lifetime) REVIEW.md flags as most-blocked. A maintainer who can read the WebKit-side diff should approve this.

Other factors

  • All three findings from my earlier passes were fixed (a9a337a, c9a03e2, 20a81ca) and verified in the current diff.
  • WEBKIT_VERSION is still a preview tag; the PR description says it will be swapped for the merged autobuild-<sha> once WebKit#308 lands, so this is effectively blocked on that anyway.
  • The regression test's assertion is monotone (as the author argued in-thread), so pass-after should be stable; fail-before is 30/30 against the release build per the author's measurement.

@robobun

robobun commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

CI on 20a81ca (build #75495): test/js/bun/jsc/bun-jsc.test.ts passed on every lane, and the earlier small-list-grow/bundler_splitting/require-cache segfaults are gone (fixed by the jsEmpty() guard in oven-sh/WebKit#308 40ff52aa).

Remaining red is unrelated: worker-transfer-terminate-stress / test-worker-message-port-transfer-terminate (the known intermittent terminate-during-MessagePort-transfer ASAN assertion, pre-existing on main and tracked separately), plus four retry-passed flakes on the install/fetch/repl/http-agent tests.

Ready for review. Once oven-sh/WebKit#308 merges I'll swap WEBKIT_VERSION from the preview tag to the merged sha.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant