Skip to content

[Prototype] Run await continuations from fibers on one reused hidden dispatch fiber (needs node#7) - #6

Draft
jbaczuk-qualia wants to merge 2 commits into
async-resourcefrom
jbaczuk/fiber-await-v18
Draft

jbaczuk-qualia wants to merge 2 commits into
async-resourcefrom
jbaczuk/fiber-await-v18

Conversation

@jbaczuk-qualia

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

Copy link
Copy Markdown

Summary

R&D prototype, pairs with qualialabs/node#7 (node 18 V8 patch). Makes the code after an await (and .then callbacks) that was registered inside a fiber run on a fiber, so fiber-only calls work there instead of throwing Can't wait without a fiber. Continuations run on one long-lived hidden dispatch fiber, which also runs the dispatchable jobs queued right behind each one, so a chain of awaits in fibered code costs one switch onto the fiber and one back instead of two per await. Two commits on async-resource: hidden-fiber dispatch, then the reused dispatch fiber.

File Change
src/fibers.cc SwapContext() sets the context's continuation-preserved embedder data (CPED) to a marker while a fiber runs, so V8 tags every promise reaction registered inside it. DispatchMicrotask() hands tagged jobs to a JS dispatcher; runDispatched(token) runs one. New Fiber.__setMicrotaskDispatcher, Fiber.__runNextDispatchable() and Fiber.microtasksDispatched. The V8 API is resolved with dlsym, so this binary still loads (feature off) on stock node
fibers_async.js reusingDispatcher: runs jobs on the dispatch fiber and, while it is still the dispatch fiber, keeps calling Fiber.__runNextDispatchable(). A job that blocks keeps the fiber (it's parked inside that job) and the next dispatch starts a new one, the same approach as znewsham's drain fiber in this repo's #7. A parked fiber that is later resumed never pulls in other jobs. A dispatch that arrives while the dispatch fiber itself is running (nested drain) falls back to a one-off fiber. Fiber.current reports hidden fibers as undefined; new Fiber.currentIncludingHidden returns them. Fiber.microtasksBatched counts jobs run without a switch
future.js Future.wait uses Fiber.currentIncludingHidden so it can block in a hidden fiber

Kill switches: FIBERS_AWAIT_DISPATCH=0 disables dispatch (stock behaviour); FIBERS_AWAIT_REUSE=0, or a node without v8_qualia_RunNextDispatchableMicrotask, runs each continuation on its own one-off hidden fiber.

Why hidden fibers

A first version used ordinary fibers and broke qualia's boot: Fiber.current is also used as an API mode switch ("in a fiber, return the result synchronously, otherwise return a Promise"), e.g. @znewsham/meteor-mongo-collection-hooks cursor.observeChanges. Async code after an await suddenly got a handle instead of a Promise (platform-data-subscriptions/server.js:96: handlePromise.catch is not a function). Hiding the fiber keeps every Fiber.current check answering exactly as it does today; only blocking primitives that opt in (Future.wait, Promise.await via qualialabs/shared-packages#265) see the fiber.

Testing

On the node 18 build from qualialabs/node#7, arm64 dev-qualia container:

  • Standalone checks: 11/11 pass with reuse on and off (await then fiber-only call, nested awaits, .then, AsyncLocalStorage across the hop, Promise.all overlap, errors, Fiber.current still undefined after await, a dual-mode API still returns a Promise after await, fiberless code untouched, microtask order; same 45 jobs: 27 dispatched + 18 batched with reuse, 45 dispatched without). FIBERS_AWAIT_DISPATCH=0 fails the expected 7.
  • znewsham's test-microtask-fibers.js (with Fiber.current swapped for the running fiber, poolSize = 1e9): 7/7 behaviour tests pass, including "a parked microtask does not stall other promises" and "resuming a parked fiber does not drain unrelated microtasks mid-callback"; the 100k park/resume cycle test still exceeds its 10 s timeout (~9.7 s for the cycles alone; every park needs its own fiber).
  • meteor-promise suite: 15/15, with native Promise too.
  • On stock node 18 the new binary loads with __setMicrotaskDispatcher absent and behaves as before.
  • test/: identical results on stock vs patched node (pool.js/cleanup.js segfault on both: the pthread coro_destroy bug fixed in d3260f6 on node24-pthread).
  • Local qli5 qualia boots; a startup hook doing await OrdersAsync.countDocuments() then Orders.find().count() logs both counts; fiberless code still throws Can't wait without a fiber.
one fiber per continuation (FIBERS_AWAIT_REUSE=0) reused dispatch fiber
chain of awaits in a fiber ~25–30 us 0.13 us (stock: 0.05–0.07 us)
I/O-style await in a fiber ~33 us ~28 us (stock: ~1.1 us; ~20 us is the pthread run+yield floor)
drains / awaits in code that never had a fiber unchanged unchanged

Known gaps

🤖 Generated with Claude Code

Prototype, needs the patched node (v8-microtask-dispatch.h).

While a fiber runs, its context's CPED is a marker, so every promise
reaction registered inside it (the code after `await`, `.then`
callbacks) is handed to a dispatcher that runs it in a new fiber.

Those fibers are hidden: `Fiber.current` reports undefined there, as
it does after an `await` today, so Fiber.current-based mode switches
(e.g. cursor.observeChanges returning a Promise) are unchanged.
Future.wait uses the new `Fiber.currentIncludingHidden`, so blocking
calls work. The dispatch API is resolved with dlsym, so this binary
still loads (with the feature off) on stock node.
FIBERS_AWAIT_DISPATCH=0 disables it; Fiber.microtasksDispatched counts
dispatched jobs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Instead of a new hidden fiber per dispatched job, keep one long-lived
hidden dispatch fiber. After each job it runs the dispatchable jobs
queued right behind it (Fiber.__runNextDispatchable, needs the matching
node patch), so a chain of awaits in fibered code costs one switch onto
the fiber and one back instead of two per await. A job that blocks keeps
the fiber and the next dispatch starts a new one; a fiber that was
parked and later resumed never pulls in other jobs.

FIBERS_AWAIT_REUSE=0 restores one new fiber per job.
Fiber.microtasksBatched counts jobs run without a switch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jbaczuk-qualia jbaczuk-qualia changed the title [Prototype] Run await continuations from fibers in hidden fibers (needs node#7) [Prototype] Run await continuations from fibers on one reused hidden dispatch fiber (needs node#7) Oct 9, 2026
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