Skip to content

[Prototype] Node 24: let node-fibers run await continuations on a fiber (port of #7 + #9) - #10

Draft
jbaczuk-qualia wants to merge 1 commit into
custom-v24-pthreadfrom
jbaczuk/fiber-await-v24
Draft

jbaczuk-qualia wants to merge 1 commit into
custom-v24-pthreadfrom
jbaczuk/fiber-await-v24

Conversation

@jbaczuk-qualia

@jbaczuk-qualia jbaczuk-qualia commented Oct 2, 2026 •

Copy link
Copy Markdown

Summary

Port of the node 18 fiber-await prototype (#7 + #9) to node 24.21.0 / V8 13.6, stacked on #6 (custom-v24-pthread; merge #6 first). Lets node-fibers run the code after an await (and .then callbacks) registered inside a fiber on a hidden fiber, and batch runs of them on one fiber. Pairs with qualialabs/node-fibers#9. One commit, V8 only:

File Change
deps/v8/include/v8-microtask-dispatch.h (new, identical to #7/#9) SetMicrotaskDispatchCallback, RunDispatchedMicrotask, RunNextDispatchableMicrotask and unmangled v8_qualia_* entry points for dlsym
deps/v8/src/builtins/builtins-microtask-queue-gen.cc Before a fulfill/reject reaction job whose continuation-preserved embedder data (CPED) is set, call Runtime::kDispatchMicrotask; skip the job if the embedder took it
deps/v8/src/runtime/runtime-promise.cc, runtime.h Same logic as node 18: private one-job queue run through the RunMicrotasks builtin, plus batching of the dispatchable jobs queued right behind

What differs from node 18

  • API renames (Tagged<>, DirectHandle, RETURN_FAILURE_IF_EXCEPTION, new TryRunMicrotasks signature): mechanical.
  • CPED lives on the isolate. V8 13's MicrotaskQueue::RunMicrotasks already saves and restores the caller's CPED around a drain, so unlike [Prototype] Node 18: let node-fibers run await continuations on a fiber (V8 microtask dispatch) #7 the builtin needs no CPED change; the private one-job runner does the same save/clear/restore.
  • AsyncContextFrame shares the slot. Node 24 keeps AsyncLocalStorage state in CPED unless started with --no-async-context-frame. node-fibers only enables dispatch with the flag (fibers needs it on node 24 anyway); the tag it uses is true, which lives in read-only space, so the per-archived-thread GC visit of the CPED root (the unapplied 4172f779 fix) can't corrupt it.
  • Forward risk: if a future node drops --no-async-context-frame, tagging through CPED stops being possible and this would need its own field on promise reactions (a larger V8 patch).

Testing

Built incrementally on the custom-v24-pthread tree (arm64, Docker), with qualialabs/node-fibers#9, all under --no-async-context-frame:

  • Behaviour checks (fiber-await/test.js on node-fibers jbaczuk/fiber-await-v24-stats): 11/11 with reuse on and off; dispatch off fails the expected 7, same as Node 24.21.0 for CORO_PTHREAD fibers (custom-v24-pthread): three node patches, no V8 changes #6's stock build.
  • node-fibers test/: all pass. znewsham's scenarios from node-fibers#7 (with the running fiber): 9/9, including the 100k park/resume test. meteor-promise suite with shared-packages#265: 15/15 (both promise implementations).
  • GC stress: behaviour checks pass under --stress-scavenge=20 and --stress-compaction.
  • fiber-await/bench.js: fiberless awaits 0.03–0.04 us (stock Node 24.21.0 for CORO_PTHREAD fibers (custom-v24-pthread): three node patches, no V8 changes #6: 0.03), chained awaits in a fiber 0.10 us, I/O-style awaits ~28 us (the pthread run()+yield() floor is ~22 us), drains unchanged.
  • GDC on qli5 locally (qualia branch jbaczuk/node-24-gdc-fiber-await, on top of qualia#56473): boots; a startup hook that awaits and then makes a sync Mongo call works on a hidden fiber; fiberless code still throws Can't wait without a fiber.

🤖 Generated with Claude Code

…node 24)

Port of the node 18 fiber-await prototype (#7 + #9) to
V8 13.6:

- The microtask runner offers fulfill/reject reaction jobs whose
  continuation-preserved embedder data (CPED) is set to an embedder
  callback (include/v8-microtask-dispatch.h, Runtime_DispatchMicrotask).
- RunDispatchedMicrotask runs a taken job through a private one-job
  queue with the same CPED save/clear/restore MicrotaskQueue::
  RunMicrotasks does around a drain (V8 13 keeps CPED on the isolate).
- RunNextDispatchableMicrotask lets the embedder run the dispatchable
  jobs queued right behind one it is running, without switching stacks.
- Unmangled v8_qualia_* entry points for dlsym.

Unlike V8 10, V8 13 already restores the caller's CPED after a drain,
so the builtin needs no CPED change beyond the dispatch hook. Node 24's
AsyncContextFrame uses the same CPED slot; the embedder (node-fibers)
only enables dispatch under --no-async-context-frame.

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.

1 participant