From 40351e8562f43f9444d5b3d0f73f424f27141c60 Mon Sep 17 00:00:00 2001 From: Jordan Baczuk Date: Wed, 7 Oct 2026 11:40:34 -0600 Subject: [PATCH] feat: count await continuations that block on their dispatch fiber Fiber.awaitContinuationBlocks counts code after an `await` that blocked on the hidden fiber the dispatcher gave it (Future.wait, Promise.await, a sync Meteor call). Without the dispatch each of these would have thrown "Can't wait without a fiber", so the count tracks code that still depends on fibers after an await. It's counted in the dispatcher's existing "job blocked" branch, once per job, so jobs that don't block and ordinary yields cost nothing extra. The count lives on the native module so every copy of fibers in a process shares it. Co-Authored-By: Claude Opus 5.5 --- fibers_async.js | 23 ++++++++- test/await-continuation-blocks.js | 86 +++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 test/await-continuation-blocks.js diff --git a/fibers_async.js b/fibers_async.js index 700d778..79be800 100644 --- a/fibers_async.js +++ b/fibers_async.js @@ -99,10 +99,24 @@ function asyncContextFrameInUse() { return inUse; } +// Code after an `await` that blocked on the fiber we gave it (Future.wait, Promise.await, a sync +// Meteor call). Without this dispatch it would have thrown "Can't wait without a fiber", so each one +// marks code that still depends on fibers after an `await`. Counted once per job, when it first +// parks; nothing is counted for the jobs that don't block. Kept on the native module, which every +// copy of this file shares (an app can load more than one). +_Fiber.__awaitContinuationBlocks = _Fiber.__awaitContinuationBlocks || 0; + function dispatchOnNewFiber(runDispatched, token) { - const fiber = Fiber(() => runDispatched(token)); + let finished = false; + const fiber = Fiber(() => { + runDispatched(token); + finished = true; + }); fiber[kHidden] = true; fiber.run(); + if (!finished) { + _Fiber.__awaitContinuationBlocks++; + } } // Runs jobs on one long-lived hidden fiber instead of a new fiber per job. After each job it keeps @@ -148,6 +162,7 @@ function reusingDispatcher(runNextDispatchable) { if (fiber.run(token) !== kIdle && dispatchFiber === fiber) { // A job blocked on this fiber; it belongs to that job now. dispatchFiber = null; + _Fiber.__awaitContinuationBlocks++; } }; } @@ -158,4 +173,10 @@ Object.defineProperty(Fiber, 'microtasksBatched', { } }) +Object.defineProperty(Fiber, 'awaitContinuationBlocks', { + get() { + return _Fiber.__awaitContinuationBlocks || 0; + } +}) + module.exports = _Fiber.Fiber = Fiber; diff --git a/test/await-continuation-blocks.js b/test/await-continuation-blocks.js new file mode 100644 index 0000000..ec0ab31 --- /dev/null +++ b/test/await-continuation-blocks.js @@ -0,0 +1,86 @@ +'use strict'; +// Fiber.awaitContinuationBlocks counts code after an `await` that blocked on the fiber the +// dispatcher ran it on: once per blocking job, never for jobs that don't block or for awaits made +// outside a fiber. Needs a node with the microtask dispatch patch; elsewhere nothing is dispatched. +var Fiber = require('fibers'); +var Future; +try { + Future = require('fibers/future'); +} catch (err) { + Future = require('future'); +} + +var enabled = Fiber.awaitDispatchEnabled !== undefined + ? Fiber.awaitDispatchEnabled + : typeof Fiber.microtasksDispatched === 'number' && process.env.FIBERS_AWAIT_DISPATCH !== '0'; +if (!enabled) { + console.log('pass'); + return; +} + +function sleep(ms) { + var future = new Future(); + setTimeout(function() { + future.return(); + }, ms); + return future.wait(); +} + +function inFiber(fn) { + return new Promise(function(resolve, reject) { + Fiber(function() { + fn().then(resolve, reject); + }).run(); + }); +} + +async function blocks(fn) { + var before = Fiber.awaitContinuationBlocks; + await fn(); + return Fiber.awaitContinuationBlocks - before; +} + +async function main() { + var cases = [ + ['awaits that never block', 0, function() { + return inFiber(async function() { + await null; + await new Promise(function(resolve) { + setTimeout(resolve, 1); + }); + }); + }], + ['one continuation blocking twice', 1, function() { + return inFiber(async function() { + await null; + sleep(1); + sleep(1); + }); + }], + ['two continuations that block', 2, function() { + return inFiber(async function() { + await null; + sleep(1); + await null; + sleep(1); + }); + }], + ['awaits outside a fiber', 0, async function() { + await null; + await new Promise(function(resolve) { + setTimeout(resolve, 1); + }); + }], + ]; + for (var ii = 0; ii < cases.length; ii++) { + var got = await blocks(cases[ii][2]); + if (got !== cases[ii][1]) { + throw new Error(cases[ii][0] + ': expected ' + cases[ii][1] + ' blocks, counted ' + got); + } + } + console.log('pass'); +} + +main().catch(function(err) { + console.log(err.stack); +});