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); +});