Skip to content

Commit

Permalink
timers: run nextTicks after each immediate and timer
Browse files Browse the repository at this point in the history
In order to better match the browser behaviour, run nextTicks (and
subsequently the microtask queue) after each individual Timer and
Immediate, rather than after the whole list is processed. The
current behaviour is somewhat of a performance micro-optimization
and also partly dictated by how timer handles were implemented.

PR-URL: #22842
Fixes: #22257
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Gus Caplan <me@gus.host>
Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
  • Loading branch information
apapirovski authored and Trott committed Oct 18, 2018
1 parent 4e9e0d3 commit e7af983
Show file tree
Hide file tree
Showing 13 changed files with 162 additions and 158 deletions.
36 changes: 36 additions & 0 deletions benchmark/timers/timers-timeout-nexttick.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict';
const common = require('../common.js');

// The following benchmark measures setting up n * 1e6 timeouts,
// as well as scheduling a next tick from each timeout. Those
// then get executed on the next uv tick.

const bench = common.createBenchmark(main, {
n: [5e4, 5e6],
});

function main({ n }) {
let count = 0;

// Function tracking on the hidden class in V8 can cause misleading
// results in this benchmark if only a single function is used —
// alternate between two functions for a fairer benchmark.

function cb() {
process.nextTick(counter);
}
function cb2() {
process.nextTick(counter);
}
function counter() {
count++;
if (count === n)
bench.end(n);
}

for (var i = 0; i < n; i++) {
setTimeout(i % 2 ? cb : cb2, 1);
}

bench.start();
}
18 changes: 5 additions & 13 deletions doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -1461,13 +1461,11 @@ changes:
* `callback` {Function}
* `...args` {any} Additional arguments to pass when invoking the `callback`

The `process.nextTick()` method adds the `callback` to the "next tick queue".
Once the current turn of the event loop turn runs to completion, all callbacks
currently in the next tick queue will be called.

This is *not* a simple alias to [`setTimeout(fn, 0)`][]. It is much more
efficient. It runs before any additional I/O events (including
timers) fire in subsequent ticks of the event loop.
`process.nextTick()` adds `callback` to the "next tick queue". This queue is
fully drained after the current operation on the JavaScript stack runs to
completion and before the event loop is allowed to continue. As a result, it's
possible to create an infinite loop if one were to recursively call
`process.nextTick()`.

```js
console.log('start');
Expand Down Expand Up @@ -1542,11 +1540,6 @@ function definitelyAsync(arg, cb) {
}
```

The next tick queue is completely drained on each pass of the event loop
**before** additional I/O is processed. As a result, recursively setting
`nextTick()` callbacks will block any I/O from happening, just like a
`while(true);` loop.

## process.noDeprecation
<!-- YAML
added: v0.8.0
Expand Down Expand Up @@ -2162,7 +2155,6 @@ cases:
[`require()`]: globals.html#globals_require
[`require.main`]: modules.html#modules_accessing_the_main_module
[`require.resolve()`]: modules.html#modules_require_resolve_request_options
[`setTimeout(fn, 0)`]: timers.html#timers_settimeout_callback_delay_args
[`v8.setFlagsFromString()`]: v8.html#v8_v8_setflagsfromstring_flags
[Android building]: https://github.com/nodejs/node/blob/master/BUILDING.md#androidandroid-based-devices-eg-firefox-os
[Child Process]: child_process.html
Expand Down
11 changes: 10 additions & 1 deletion lib/internal/process/next_tick.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function setupNextTick(_setupNextTick, _setupPromises) {
const [
tickInfo,
runMicrotasks
] = _setupNextTick(_tickCallback);
] = _setupNextTick(internalTickCallback);

// *Must* match Environment::TickInfo::Fields in src/env.h.
const kHasScheduled = 0;
Expand All @@ -39,6 +39,15 @@ function setupNextTick(_setupNextTick, _setupPromises) {
process._tickCallback = _tickCallback;

function _tickCallback() {
if (tickInfo[kHasScheduled] === 0 && tickInfo[kHasPromiseRejections] === 0)
runMicrotasks();
if (tickInfo[kHasScheduled] === 0 && tickInfo[kHasPromiseRejections] === 0)
return;

internalTickCallback();
}

function internalTickCallback() {
let tock;
do {
while (tock = queue.shift()) {
Expand Down
175 changes: 76 additions & 99 deletions lib/timers.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,16 +254,17 @@ function processTimers(now) {
debug('process timer lists %d', now);
nextExpiry = Infinity;

let list, ran;
let list;
let ranAtLeastOneList = false;
while (list = queue.peek()) {
if (list.expiry > now) {
nextExpiry = list.expiry;
return refCount > 0 ? nextExpiry : -nextExpiry;
}
if (ran)
if (ranAtLeastOneList)
runNextTicks();
else
ran = true;
ranAtLeastOneList = true;
listOnTimeout(list, now);
}
return 0;
Expand All @@ -275,6 +276,7 @@ function listOnTimeout(list, now) {
debug('timeout callback %d', msecs);

var diff, timer;
let ranAtLeastOneTimer = false;
while (timer = L.peek(list)) {
diff = now - timer._idleStart;

Expand All @@ -288,6 +290,11 @@ function listOnTimeout(list, now) {
return;
}

if (ranAtLeastOneTimer)
runNextTicks();
else
ranAtLeastOneTimer = true;

// The actual logic for when a timeout happens.
L.remove(timer);

Expand All @@ -307,7 +314,33 @@ function listOnTimeout(list, now) {

emitBefore(asyncId, timer[trigger_async_id_symbol]);

tryOnTimeout(timer);
let start;
if (timer._repeat)
start = getLibuvNow();

try {
const args = timer._timerArgs;
if (!args)
timer._onTimeout();
else
Reflect.apply(timer._onTimeout, timer, args);
} finally {
if (timer._repeat && timer._idleTimeout !== -1) {
timer._idleTimeout = timer._repeat;
if (start === undefined)
start = getLibuvNow();
insert(timer, timer[kRefed], start);
} else {
if (timer[kRefed])
refCount--;
timer[kRefed] = null;

if (destroyHooksExist() && !timer._destroyed) {
emitDestroy(timer[async_id_symbol]);
timer._destroyed = true;
}
}
}

emitAfter(asyncId);
}
Expand All @@ -327,30 +360,6 @@ function listOnTimeout(list, now) {
}


// An optimization so that the try/finally only de-optimizes (since at least v8
// 4.7) what is in this smaller function.
function tryOnTimeout(timer, start) {
if (start === undefined && timer._repeat)
start = getLibuvNow();
try {
ontimeout(timer);
} finally {
if (timer._repeat) {
rearm(timer, start);
} else {
if (timer[kRefed])
refCount--;
timer[kRefed] = null;

if (destroyHooksExist() && !timer._destroyed) {
emitDestroy(timer[async_id_symbol]);
timer._destroyed = true;
}
}
}
}


// Remove a timer. Cancels the timeout and resets the relevant timer properties.
function unenroll(item) {
// Fewer checks may be possible, but these cover everything.
Expand Down Expand Up @@ -456,26 +465,6 @@ setTimeout[internalUtil.promisify.custom] = function(after, value) {
exports.setTimeout = setTimeout;


function ontimeout(timer) {
const args = timer._timerArgs;
if (typeof timer._onTimeout !== 'function')
return Promise.resolve(timer._onTimeout, args[0]);
if (!args)
timer._onTimeout();
else
Reflect.apply(timer._onTimeout, timer, args);
}

function rearm(timer, start) {
// Do not re-arm unenroll'd or closed timers.
if (timer._idleTimeout === -1)
return;

timer._idleTimeout = timer._repeat;
insert(timer, timer[kRefed], start);
}


const clearTimeout = exports.clearTimeout = function clearTimeout(timer) {
if (timer && timer._onTimeout) {
timer._onTimeout = null;
Expand Down Expand Up @@ -601,75 +590,63 @@ function processImmediate() {
const queue = outstandingQueue.head !== null ?
outstandingQueue : immediateQueue;
var immediate = queue.head;
const tail = queue.tail;

// Clear the linked list early in case new `setImmediate()` calls occur while
// immediate callbacks are executed
queue.head = queue.tail = null;

let count = 0;
let refCount = 0;
if (queue !== outstandingQueue) {
queue.head = queue.tail = null;
immediateInfo[kHasOutstanding] = 1;
}

let prevImmediate;
let ranAtLeastOneImmediate = false;
while (immediate !== null) {
immediate._destroyed = true;
if (ranAtLeastOneImmediate)
runNextTicks();
else
ranAtLeastOneImmediate = true;

const asyncId = immediate[async_id_symbol];
emitBefore(asyncId, immediate[trigger_async_id_symbol]);
// It's possible for this current Immediate to be cleared while executing
// the next tick queue above, which means we need to use the previous
// Immediate's _idleNext which is guaranteed to not have been cleared.
if (immediate._destroyed) {
outstandingQueue.head = immediate = prevImmediate._idleNext;
continue;
}

count++;
immediate._destroyed = true;

immediateInfo[kCount]--;
if (immediate[kRefed])
refCount++;
immediateInfo[kRefCount]--;
immediate[kRefed] = null;

tryOnImmediate(immediate, tail, count, refCount);
prevImmediate = immediate;

emitAfter(asyncId);
const asyncId = immediate[async_id_symbol];
emitBefore(asyncId, immediate[trigger_async_id_symbol]);

immediate = immediate._idleNext;
}
try {
const argv = immediate._argv;
if (!argv)
immediate._onImmediate();
else
Reflect.apply(immediate._onImmediate, immediate, argv);
} finally {
immediate._onImmediate = null;

immediateInfo[kCount] -= count;
immediateInfo[kRefCount] -= refCount;
immediateInfo[kHasOutstanding] = 0;
}
if (destroyHooksExist())
emitDestroy(asyncId);

// An optimization so that the try/finally only de-optimizes (since at least v8
// 4.7) what is in this smaller function.
function tryOnImmediate(immediate, oldTail, count, refCount) {
var threw = true;
try {
// make the actual call outside the try/finally to allow it to be optimized
runCallback(immediate);
threw = false;
} finally {
immediate._onImmediate = null;

if (destroyHooksExist()) {
emitDestroy(immediate[async_id_symbol]);
outstandingQueue.head = immediate = immediate._idleNext;
}

if (threw) {
immediateInfo[kCount] -= count;
immediateInfo[kRefCount] -= refCount;

if (immediate._idleNext !== null) {
// Handle any remaining Immediates after error handling has resolved,
// assuming we're still alive to do so.
outstandingQueue.head = immediate._idleNext;
outstandingQueue.tail = oldTail;
immediateInfo[kHasOutstanding] = 1;
}
}
emitAfter(asyncId);
}
}

function runCallback(timer) {
const argv = timer._argv;
if (typeof timer._onImmediate !== 'function')
return Promise.resolve(timer._onImmediate, argv[0]);
if (!argv)
return timer._onImmediate();
Reflect.apply(timer._onImmediate, timer, argv);
if (queue === outstandingQueue)
outstandingQueue.head = null;
immediateInfo[kHasOutstanding] = 0;
}


Expand Down
1 change: 1 addition & 0 deletions test/message/events_unhandled_error_nexttick.out
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Error
at bootstrapNodeJSCore (internal/bootstrap/node.js:*:*)
Emitted 'error' event at:
at process.nextTick (*events_unhandled_error_nexttick.js:*:*)
at internalTickCallback (internal/process/next_tick.js:*:*)
at process._tickCallback (internal/process/next_tick.js:*:*)
at Function.Module.runMain (internal/modules/cjs/loader.js:*:*)
at startup (internal/bootstrap/node.js:*:*)
Expand Down
1 change: 1 addition & 0 deletions test/message/nexttick_throw.out
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
^
ReferenceError: undefined_reference_error_maker is not defined
at *test*message*nexttick_throw.js:*:*
at internalTickCallback (internal/process/next_tick.js:*:*)
at process._tickCallback (internal/process/next_tick.js:*:*)
at Function.Module.runMain (internal/modules/cjs/loader.js:*:*)
at startup (internal/bootstrap/node.js:*:*)
Expand Down
Loading

0 comments on commit e7af983

Please sign in to comment.