Skip to content

Release a JSC deferred-work keep-alive on the event loop that took it - #43415

Open
robobun wants to merge 1 commit into
mainfrom
robobun/87b775f5/deferred-work-keepalive-same-loop
Open

robobun wants to merge 1 commit into
mainfrom
robobun/87b775f5/deferred-work-keepalive-same-loop

Conversation

@robobun

@robobun robobun commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Fix

  • Each release site uses the loop kind that onAddPendingWork recorded in the ticket's PendingWork.
  • Correct because the macro loop applies its queued delta on each tick. The regular loop applies it when no macro runs (macro_loop_if_not_running). Ticket::unref_keep_alive follows the same rule.
  • Verified: test/bundler/transpiler/macro-test.test.ts, three new cases. Main fails all three with leaked 1. Also test/regression/issue/39900.test.ts, atomics.test.ts, wasm-streaming.test.ts.
  • Self-reviewed: 9 concerns raised, 7 addressed. Rejected: a per-test timeout and the optional teardown hardening, see Notes.

Background

  • A VM has a regular event loop and a macro event loop. vm.event_loop points at the macro loop while a macro runs. Both use one native loop (uSockets or libuv).
  • A keep-alive is +1 on the native loop's active count. The JS thread applies it at once. Bun__VmHandle__refKeepAlive queues it on one loop, which applies it on its next tick.
  • DeferredWorkTimer is the JSC hook for work such as a wasm compile. Bun keeps the loop alive from the registration of the work until its job runs.
Notes

onAddPendingWork takes the keep-alive of an ImminentlyScheduled ticket
(WebAssembly.compile, a FinalizationRegistry cleanup) on the event loop
that is current, and records that loop in the ticket's PendingWork.
Every release queued the -1 on the regular loop. When the ticket was
registered while a macro ran, the +1 was on the macro loop.

A macro VM on a bundler or transpiler thread never ticks its regular
loop, so the -1 was never applied and the thread's native loop stayed
active. Release on the recorded loop at all four release sites.
@robobun

robobun commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. CI is green on 1860c3b (build 118219).

How I reproduced it. A macro reads getEventLoopStats().numPolls from bun:internal-for-testing, a second macro awaits WebAssembly.compile(), and a third macro reads numPolls again. I ran this in the main VM, in the VM of a transpiler thread, and in the VM of a Bun.build() worker. On main the second reading is 1 higher in all three VMs, on Linux and on Windows. With this PR the two readings are equal. The three new cases in test/bundler/transpiler/macro-test.test.ts do the same.

Relation to #40769. With #40769 alone, bun build of a file that calls an awaited-wasm macro and then a macro whose promise never settles hangs. With #40769 and this PR together, it exits 1 with macro returned a promise that never settles. One related case still hangs with both changes: a FinalizationRegistry cleanup that the GC at the end of a Bun.build() schedules on the regular loop of the worker's macro VM. This PR does not change that case. It is tracked in #43458.

@coderabbitai

coderabbitai Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: oven-sh/bun/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 313210ec-2dab-49e8-b359-74f67fbade72

📥 Commits

Reviewing files that changed from the base of the PR and between 26e7a4b and 1860c3b.

📒 Files selected for processing (3)
  • src/jsc/bindings/JSCTaskScheduler.cpp
  • src/jsc/bindings/JSCTaskScheduler.h
  • test/bundler/transpiler/macro-test.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 4 remain after this review.


Walkthrough

The task scheduler now tracks the loop associated with each keep-alive ticket and uses it during release. Macro tests cover awaited WebAssembly.compile() in main, transpiler-thread, and build-worker VMs.

Changes

Loop-specific keep-alive release

Layer / File(s) Summary
Scheduler keep-alive release
src/jsc/bindings/JSCTaskScheduler.cpp, src/jsc/bindings/JSCTaskScheduler.h
dropPendingTicketLocked returns the ticket’s BunLoopKind. Shutdown, cancellation, execution, and queued-job reclamation release keep-alives on that loop. PendingWork documentation reflects this behavior.
Macro VM validation
test/bundler/transpiler/macro-test.test.ts
Documentation covers main-VM macro routing. Concurrent tests verify event-loop cleanup and VM classification for awaited WebAssembly.compile().

Suggested reviewers: jarred-sumner

Priority: ➖ Normal

🚥 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 summarizes the main change: release deferred-work keep-alives on the event loop that acquired them.
Description check ✅ Passed The description explains the problem, fix, background, verification steps, test coverage, and known limitations. It does not use the exact template headings, but it provides the required information i…

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

@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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟣 src/jsc/bindings/JSCTaskScheduler.cpp — A macro on a transpiler pool thread that starts WebAssembly.compile() without awaiting it leaves that thread's native loop active until the next macro runs on the same thread; if none does, forever, exactly as the PR says it fixes. Line 76 folds the +1 into the native loop at once via Bun__eventLoop__refKeepAlive. The completion is posted to the macro loop, which on a pool VM only ticks inside a later macro; the -1 at line 132 then waits there too. Fix: after a macro returns on a VM whose regular loop never ticks, drain the macro loop's adopted work and fold its delta, or document that un-awaited macro work is unsupported on pool VMs.

    Extended reasoning...

    lib.ts (a non-entry import) is transpiled on a RuntimeTranspilerStore pool thread whose VM runs macros. The macro calls compile() and returns 0 without awaiting. onAddPendingWork line 63 records Macro; line 76 ref_keep_alive (event_loop.rs:1179) folds +1 into the shared native loop. Later the JSC helper thread finishes and onScheduleWorkSoon line 107 posts the job to the macro loop. macro_loop_if_not_running (event_loop.rs:688) only helps the regular loop, and a pool VM's regular loop never ticks. So the job and its -1 (line 132) run only if another macro on this thread ticks the macro loop. The dismissal says the base failed the same way; on the base the -1 targeted Regular and was stranded forever, here it is stranded until the next macro, so the exposure differs and the PR text claims balance. Population: every un-awaited macro wasm compile on pool threads; the residue is visible to getEventLoopStats and is_event_loop_alive on that VM. Remedy: have the macro runner tick the macro loop once more after the macro returns when the VM has no ticking regular loop.

    Verification: pre-existing — triggered when a macro running on a pool-thread VM (RuntimeTranspilerStore transpiling a non-entry import, or a Bun.build() worker) calls WebAssembly.compile() without awaiting it and no later async macro runs on that same thread. Mechanism verified: /home/claude/bun/src/jsc/bindings/JSCTaskScheduler.cpp:63 records Bun__VM__currentLoopKind (Macro while the macro runs, since…

@robobun

robobun commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

On the finding about work that a macro starts and does not await on a bundler or transpiler thread: the description is correct, and this PR does not change that behavior. I did not change the code for it. The PR notes now state it as a known limit.

@robobun

robobun commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 1:43 AM PT - Sep 19th, 2026

✅ @robobun, your commit 1860c3b3afa287db537420173b4c46e24035dee1 passed in Build #118219! 🎉


🧪   To try this PR locally:

bunx bun-pr 43415

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

bun-43415 --bun

This branch has not been deployed

No deployments
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