Skip to content

fix: drain the platform's fallback foreground-task queue - #50

Merged
Arshia001 merged 1 commit into
guest-heap-mandatory-allocfrom
fix/foreground-task-queue-drain
Aug 7, 2026
Merged

fix: drain the platform's fallback foreground-task queue#50
Arshia001 merged 1 commit into
guest-heap-mandatory-allocfrom
fix/foreground-task-queue-drain

Conversation

@Arshia001

Copy link
Copy Markdown
Member

V8 posts some of its own deferred foreground work -- most importantly
Heap::PostFinalizationRegistryCleanupTaskIfNeeded's cleanup task, queued
after a GC finds a JSFinalizationRegistry with dead targets -- via
v8::TaskRunner::PostNonNestableTask on the runner EdgeV8Platform hands
back for the isolate. That runner forwards to the guest's own enqueue
callback when one is bound (BindForegroundTaskTarget), but nothing
requires the guest to bind one; a guest may drive everything through
unofficial_napi_process_microtasks instead, as edgejs does. Tasks posted
with no guest target bound fall back to the stock default-platform runner
(ForegroundTaskRunner::PostTaskCommon's fallback branch), and until now
nothing ever pumped that runner's queue: the tasks were posted and then
silently never ran.

GC still correctly collected the dead targets, but the FinalizationRegistry
callbacks that were supposed to fire afterward never did. Node's own
AbortSignal/WeakRef-based cleanup (lib/internal/abort_controller.js)
depends on this, so the practical effect was a slow, unbounded per-request
leak in every long-running V8-imports (WASIX) edgejs process.

Fix

Add EdgeV8Platform::PumpPendingForegroundTasks(isolate), which drains the
fallback platform's message loop, and call it from DrainMicrotasksForEnv
alongside the existing microtask checkpoint -- the same point already
pumped reliably by every guest event-loop tick, regardless of whether that
guest ever wires up its own foreground-task hook.

Verification

  • Standalone repro: linked a bare-minimum C++ program directly against the
    same prebuilt V8 static lib, using V8's own stock
    NewDefaultPlatform() + PumpMessageLoop (no wasmer/edgejs code). Confirms
    V8 itself works fine -- 95% finalized, matching native Node -- so the bug
    is in the embedding, not V8.
  • fr-test.mjs (register 120k objects under GC pressure, standalone):
    0/120000 finalized before this fix, 118000/120000 (98%) after.
  • A matching WeakRef-only variant: unreliable before, 114000/120000 (95%)
    after.
  • edgejs regression test in chore: bump napi submodule: make GuestHeap the only V8 allocation path edgejs#136 (stacked on this PR): fails
    (0/60000) against the pre-fix bridge, passes (60000/60000) after.

Stacked on #49.

…low-up)

V8 posts some of its own deferred work -- most importantly
Heap::PostFinalizationRegistryCleanupTaskIfNeeded's cleanup task, queued
after a GC finds a JSFinalizationRegistry with dead targets -- via
v8::TaskRunner::PostNonNestableTask on the runner EdgeV8Platform hands
back for the isolate. That runner forwards to the guest's own enqueue
callback when one is bound (BindForegroundTaskTarget), but nothing
requires the guest to bind one; a guest may drive everything through
unofficial_napi_process_microtasks instead, as edgejs does. Tasks posted
with no guest target bound fall back to the stock default-platform
runner (EdgeV8Platform::ForegroundTaskRunner::PostTaskCommon's fallback
branch), and until now nothing ever pumped that runner's queue: the
tasks were posted and then silently never ran. GC still correctly
collected the dead targets, but the FinalizationRegistry callbacks that
were supposed to fire afterward never did -- confirmed via a standalone
V8 harness (same prebuilt binary, stock platform + PumpMessageLoop: 95%
finalized) against the unpatched bridge (0/120000 finalized under
identical GC pressure, edgejs's own WeakRef/FinalizationRegistry-based
AbortSignal cleanup leaking as a result).

Add EdgeV8Platform::PumpPendingForegroundTasks(isolate), which drains
the fallback platform's message loop, and call it from
DrainMicrotasksForEnv alongside the existing microtask checkpoint --
the same point already pumped reliably by every guest event-loop tick,
regardless of whether that guest ever wires up its own foreground-task
hook.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Arshia001
Arshia001 merged commit 4b4b072 into main Aug 7, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants