Skip to content

Run one V8 fallback foreground task per event-loop checkpoint - #117

Merged
syrusakbary merged 2 commits into
mainfrom
fix/v8-checkpoint-fairness
Oct 25, 2026
Merged

syrusakbary merged 2 commits into
mainfrom
fix/v8-checkpoint-fairness

Conversation

@syrusakbary

Copy link
Copy Markdown
Member

Summary

A V8 foreground task can post its successor while it runs (incremental marking, minor GC jobs). EdgeV8Platform::PumpPendingForegroundTasks drained the fallback queue until it was empty, so such a chain could keep one event-loop checkpoint busy, stalling the guest's event loop and streamed responses during GC. This ports the fix from #101 onto main: each checkpoint runs at most one ready fallback task, and reports when it ran one so the runtime takes another turn instead of waiting.

The fallback queue is the one used when no enqueue hook is bound. That is every V8 foreground task under the WASIX bridge, which attaches empty hooks. Native EdgeJS binds the hook, so it rarely reaches this path.

#101 was part 9 of the split of #84 and was closed with its stack. The production change is unchanged from #101 (author Christoph Herzog, kept as the first commit). In #85, the same change cut p95 for a streamed Astro dashboard on Edge from 1,109 ms to 84 ms.

Changes

  • v8/src/edge_v8_platform.cc, .h: PumpPendingForegroundTasks runs one task (return v8::platform::PumpMessageLoop(...) instead of the while loop) and returns whether it ran one.
  • v8/src/unofficial_napi.cc: when a task ran, unofficial_napi_event_loop_checkpoint reports host_tasks_admitted | pending_provider_work. EdgeJS reads host_tasks_admitted as "don't wait on libuv this turn". Without it, EdgeJS stops with "Provider reported pending work without admitting a host task" once its uv loop is idle.
  • include/unofficial_napi.h: documents what the admitted bit means for a synchronous provider (new in this port).
  • tests/src/v8_foreground_checkpoint_test.cc: a standalone process posts a four-task self-reposting chain and checks that each pump advances it by one. The test called EdgeV8Platform::Create(true), a signature from fix(v8): bound foreground work at each JavaScript checkpoint #101's stack; it now calls Create() (new in this port).

Testing

  • make test-napi: 98/98 pass. test_35_promise and foreground_checkpoint_fairness pass 50 repeats.
  • The fairness test fails with the old while loop (the first pump runs all four tasks) and passes with this change.
  • EdgeJS main (18b0ce9e) built against napi main, with and without this change and Fix use-after-free when handing V8 foreground tasks to the enqueue hook #113 together, on macOS arm64:
EdgeJS check napi main with this change and #113
Native make test-only 1749/1749 1749/1749
WASIX make test-wasix-v8-only 1676/1676 1675/1676: one TLS test, which then passed 24/24 on both
test-intl, test-lang, test-wasix-v8-process pass pass
Astro SSR framework app (V8 WASIX) pass pass
GC-pressure streaming probe (V8 WASIX), 4 runs 3.9–4.1 s 3.5–3.7 s; FinalizationRegistry callbacks fire at the same rate

Notes for reviewers

🤖 Generated with Claude Code

theduke and others added 2 commits October 25, 2026 13:08
The test called EdgeV8Platform::Create(true), a signature from the split
stack it came from; main's Create() takes no arguments.

The header now says what the admitted bit means for a synchronous
provider: it reports a provider task it ran, together with pending
provider work, in either checkpoint mode, and runs at most one such task
per checkpoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@syrusakbary
syrusakbary merged commit dfc50c7 into main Oct 25, 2026
10 checks passed
syrusakbary added a commit to wasmerio/edgejs that referenced this pull request Oct 25, 2026
Advance the napi submodule from a65e9cb8 to NAPI main at dfc50c75.

- wasmerio/napi#113 stops the V8 provider from touching a foreground
  task after handing it to Edge's enqueue hook. Native Edge crashed in
  PostTaskCommon (SIGSEGV/SIGBUS) when the hook refused a task during
  env cleanup, mostly in vm and worker_threads tests at exit.
- wasmerio/napi#117 runs at most one fallback foreground task per event
  loop checkpoint, so self-reposting GC tasks no longer stall the V8
  WASIX event loop.

The bump also brings wasmerio/napi#87, #88, #90, #91, #92, #103 and
#104. Public header changes are comments only.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
syrusakbary added a commit that referenced this pull request Oct 27, 2026
Brings in the V8 foreground-task fixes (#113, #117) and QuickJS
termination (#118, #119).

Conflicts were in the QuickJS provider, where this branch moved
runtime-wide state onto the agent and removed the switchable current
context:
- napi_env.cc: main's runtime interrupt handler with this branch's
  constructor (agent and fixed context). Teardown keeps both the agent's
  value clearing and the interrupt detach, using the env's context.
- napi_function.cc, js_native_api_quickjs.cc: main's termination checks,
  without the napi_env_context_scope__ this branch removed.
- last_exception_is_uncatchable() asks the agent, which now holds the
  pending exception.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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