Skip to content

Commit 52428c8

Browse files
apapirovskijasnell
authored andcommitted
timers: run nextTicks after each immediate and timer
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>
1 parent 20373c4 commit 52428c8

13 files changed

+162
-158
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use strict';
2+
const common = require('../common.js');
3+
4+
// The following benchmark measures setting up n * 1e6 timeouts,
5+
// as well as scheduling a next tick from each timeout. Those
6+
// then get executed on the next uv tick.
7+
8+
const bench = common.createBenchmark(main, {
9+
n: [5e4, 5e6],
10+
});
11+
12+
function main({ n }) {
13+
let count = 0;
14+
15+
// Function tracking on the hidden class in V8 can cause misleading
16+
// results in this benchmark if only a single function is used —
17+
// alternate between two functions for a fairer benchmark.
18+
19+
function cb() {
20+
process.nextTick(counter);
21+
}
22+
function cb2() {
23+
process.nextTick(counter);
24+
}
25+
function counter() {
26+
count++;
27+
if (count === n)
28+
bench.end(n);
29+
}
30+
31+
for (var i = 0; i < n; i++) {
32+
setTimeout(i % 2 ? cb : cb2, 1);
33+
}
34+
35+
bench.start();
36+
}

doc/api/process.md

+5-13
Original file line numberDiff line numberDiff line change
@@ -1461,13 +1461,11 @@ changes:
14611461
* `callback` {Function}
14621462
* `...args` {any} Additional arguments to pass when invoking the `callback`
14631463

1464-
The `process.nextTick()` method adds the `callback` to the "next tick queue".
1465-
Once the current turn of the event loop turn runs to completion, all callbacks
1466-
currently in the next tick queue will be called.
1467-
1468-
This is *not* a simple alias to [`setTimeout(fn, 0)`][]. It is much more
1469-
efficient. It runs before any additional I/O events (including
1470-
timers) fire in subsequent ticks of the event loop.
1464+
`process.nextTick()` adds `callback` to the "next tick queue". This queue is
1465+
fully drained after the current operation on the JavaScript stack runs to
1466+
completion and before the event loop is allowed to continue. As a result, it's
1467+
possible to create an infinite loop if one were to recursively call
1468+
`process.nextTick()`.
14711469

14721470
```js
14731471
console.log('start');
@@ -1542,11 +1540,6 @@ function definitelyAsync(arg, cb) {
15421540
}
15431541
```
15441542

1545-
The next tick queue is completely drained on each pass of the event loop
1546-
**before** additional I/O is processed. As a result, recursively setting
1547-
`nextTick()` callbacks will block any I/O from happening, just like a
1548-
`while(true);` loop.
1549-
15501543
## process.noDeprecation
15511544
<!-- YAML
15521545
added: v0.8.0
@@ -2162,7 +2155,6 @@ cases:
21622155
[`require()`]: globals.html#globals_require
21632156
[`require.main`]: modules.html#modules_accessing_the_main_module
21642157
[`require.resolve()`]: modules.html#modules_require_resolve_request_options
2165-
[`setTimeout(fn, 0)`]: timers.html#timers_settimeout_callback_delay_args
21662158
[`v8.setFlagsFromString()`]: v8.html#v8_v8_setflagsfromstring_flags
21672159
[Android building]: https://github.com/nodejs/node/blob/master/BUILDING.md#androidandroid-based-devices-eg-firefox-os
21682160
[Child Process]: child_process.html

lib/internal/process/next_tick.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ function setupNextTick(_setupNextTick, _setupPromises) {
2626
const [
2727
tickInfo,
2828
runMicrotasks
29-
] = _setupNextTick(_tickCallback);
29+
] = _setupNextTick(internalTickCallback);
3030

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

4141
function _tickCallback() {
42+
if (tickInfo[kHasScheduled] === 0 && tickInfo[kHasPromiseRejections] === 0)
43+
runMicrotasks();
44+
if (tickInfo[kHasScheduled] === 0 && tickInfo[kHasPromiseRejections] === 0)
45+
return;
46+
47+
internalTickCallback();
48+
}
49+
50+
function internalTickCallback() {
4251
let tock;
4352
do {
4453
while (tock = queue.shift()) {

lib/timers.js

+76-99
Original file line numberDiff line numberDiff line change
@@ -254,16 +254,17 @@ function processTimers(now) {
254254
debug('process timer lists %d', now);
255255
nextExpiry = Infinity;
256256

257-
let list, ran;
257+
let list;
258+
let ranAtLeastOneList = false;
258259
while (list = queue.peek()) {
259260
if (list.expiry > now) {
260261
nextExpiry = list.expiry;
261262
return refCount > 0 ? nextExpiry : -nextExpiry;
262263
}
263-
if (ran)
264+
if (ranAtLeastOneList)
264265
runNextTicks();
265266
else
266-
ran = true;
267+
ranAtLeastOneList = true;
267268
listOnTimeout(list, now);
268269
}
269270
return 0;
@@ -275,6 +276,7 @@ function listOnTimeout(list, now) {
275276
debug('timeout callback %d', msecs);
276277

277278
var diff, timer;
279+
let ranAtLeastOneTimer = false;
278280
while (timer = L.peek(list)) {
279281
diff = now - timer._idleStart;
280282

@@ -288,6 +290,11 @@ function listOnTimeout(list, now) {
288290
return;
289291
}
290292

293+
if (ranAtLeastOneTimer)
294+
runNextTicks();
295+
else
296+
ranAtLeastOneTimer = true;
297+
291298
// The actual logic for when a timeout happens.
292299
L.remove(timer);
293300

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

308315
emitBefore(asyncId, timer[trigger_async_id_symbol]);
309316

310-
tryOnTimeout(timer);
317+
let start;
318+
if (timer._repeat)
319+
start = getLibuvNow();
320+
321+
try {
322+
const args = timer._timerArgs;
323+
if (!args)
324+
timer._onTimeout();
325+
else
326+
Reflect.apply(timer._onTimeout, timer, args);
327+
} finally {
328+
if (timer._repeat && timer._idleTimeout !== -1) {
329+
timer._idleTimeout = timer._repeat;
330+
if (start === undefined)
331+
start = getLibuvNow();
332+
insert(timer, timer[kRefed], start);
333+
} else {
334+
if (timer[kRefed])
335+
refCount--;
336+
timer[kRefed] = null;
337+
338+
if (destroyHooksExist() && !timer._destroyed) {
339+
emitDestroy(timer[async_id_symbol]);
340+
timer._destroyed = true;
341+
}
342+
}
343+
}
311344

312345
emitAfter(asyncId);
313346
}
@@ -327,30 +360,6 @@ function listOnTimeout(list, now) {
327360
}
328361

329362

330-
// An optimization so that the try/finally only de-optimizes (since at least v8
331-
// 4.7) what is in this smaller function.
332-
function tryOnTimeout(timer, start) {
333-
if (start === undefined && timer._repeat)
334-
start = getLibuvNow();
335-
try {
336-
ontimeout(timer);
337-
} finally {
338-
if (timer._repeat) {
339-
rearm(timer, start);
340-
} else {
341-
if (timer[kRefed])
342-
refCount--;
343-
timer[kRefed] = null;
344-
345-
if (destroyHooksExist() && !timer._destroyed) {
346-
emitDestroy(timer[async_id_symbol]);
347-
timer._destroyed = true;
348-
}
349-
}
350-
}
351-
}
352-
353-
354363
// Remove a timer. Cancels the timeout and resets the relevant timer properties.
355364
function unenroll(item) {
356365
// Fewer checks may be possible, but these cover everything.
@@ -456,26 +465,6 @@ setTimeout[internalUtil.promisify.custom] = function(after, value) {
456465
exports.setTimeout = setTimeout;
457466

458467

459-
function ontimeout(timer) {
460-
const args = timer._timerArgs;
461-
if (typeof timer._onTimeout !== 'function')
462-
return Promise.resolve(timer._onTimeout, args[0]);
463-
if (!args)
464-
timer._onTimeout();
465-
else
466-
Reflect.apply(timer._onTimeout, timer, args);
467-
}
468-
469-
function rearm(timer, start) {
470-
// Do not re-arm unenroll'd or closed timers.
471-
if (timer._idleTimeout === -1)
472-
return;
473-
474-
timer._idleTimeout = timer._repeat;
475-
insert(timer, timer[kRefed], start);
476-
}
477-
478-
479468
const clearTimeout = exports.clearTimeout = function clearTimeout(timer) {
480469
if (timer && timer._onTimeout) {
481470
timer._onTimeout = null;
@@ -601,75 +590,63 @@ function processImmediate() {
601590
const queue = outstandingQueue.head !== null ?
602591
outstandingQueue : immediateQueue;
603592
var immediate = queue.head;
604-
const tail = queue.tail;
605593

606594
// Clear the linked list early in case new `setImmediate()` calls occur while
607595
// immediate callbacks are executed
608-
queue.head = queue.tail = null;
609-
610-
let count = 0;
611-
let refCount = 0;
596+
if (queue !== outstandingQueue) {
597+
queue.head = queue.tail = null;
598+
immediateInfo[kHasOutstanding] = 1;
599+
}
612600

601+
let prevImmediate;
602+
let ranAtLeastOneImmediate = false;
613603
while (immediate !== null) {
614-
immediate._destroyed = true;
604+
if (ranAtLeastOneImmediate)
605+
runNextTicks();
606+
else
607+
ranAtLeastOneImmediate = true;
615608

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

619-
count++;
617+
immediate._destroyed = true;
618+
619+
immediateInfo[kCount]--;
620620
if (immediate[kRefed])
621-
refCount++;
621+
immediateInfo[kRefCount]--;
622622
immediate[kRefed] = null;
623623

624-
tryOnImmediate(immediate, tail, count, refCount);
624+
prevImmediate = immediate;
625625

626-
emitAfter(asyncId);
626+
const asyncId = immediate[async_id_symbol];
627+
emitBefore(asyncId, immediate[trigger_async_id_symbol]);
627628

628-
immediate = immediate._idleNext;
629-
}
629+
try {
630+
const argv = immediate._argv;
631+
if (!argv)
632+
immediate._onImmediate();
633+
else
634+
Reflect.apply(immediate._onImmediate, immediate, argv);
635+
} finally {
636+
immediate._onImmediate = null;
630637

631-
immediateInfo[kCount] -= count;
632-
immediateInfo[kRefCount] -= refCount;
633-
immediateInfo[kHasOutstanding] = 0;
634-
}
638+
if (destroyHooksExist())
639+
emitDestroy(asyncId);
635640

636-
// An optimization so that the try/finally only de-optimizes (since at least v8
637-
// 4.7) what is in this smaller function.
638-
function tryOnImmediate(immediate, oldTail, count, refCount) {
639-
var threw = true;
640-
try {
641-
// make the actual call outside the try/finally to allow it to be optimized
642-
runCallback(immediate);
643-
threw = false;
644-
} finally {
645-
immediate._onImmediate = null;
646-
647-
if (destroyHooksExist()) {
648-
emitDestroy(immediate[async_id_symbol]);
641+
outstandingQueue.head = immediate = immediate._idleNext;
649642
}
650643

651-
if (threw) {
652-
immediateInfo[kCount] -= count;
653-
immediateInfo[kRefCount] -= refCount;
654-
655-
if (immediate._idleNext !== null) {
656-
// Handle any remaining Immediates after error handling has resolved,
657-
// assuming we're still alive to do so.
658-
outstandingQueue.head = immediate._idleNext;
659-
outstandingQueue.tail = oldTail;
660-
immediateInfo[kHasOutstanding] = 1;
661-
}
662-
}
644+
emitAfter(asyncId);
663645
}
664-
}
665646

666-
function runCallback(timer) {
667-
const argv = timer._argv;
668-
if (typeof timer._onImmediate !== 'function')
669-
return Promise.resolve(timer._onImmediate, argv[0]);
670-
if (!argv)
671-
return timer._onImmediate();
672-
Reflect.apply(timer._onImmediate, timer, argv);
647+
if (queue === outstandingQueue)
648+
outstandingQueue.head = null;
649+
immediateInfo[kHasOutstanding] = 0;
673650
}
674651

675652

test/message/events_unhandled_error_nexttick.out

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Error
1414
at bootstrapNodeJSCore (internal/bootstrap/node.js:*:*)
1515
Emitted 'error' event at:
1616
at process.nextTick (*events_unhandled_error_nexttick.js:*:*)
17+
at internalTickCallback (internal/process/next_tick.js:*:*)
1718
at process._tickCallback (internal/process/next_tick.js:*:*)
1819
at Function.Module.runMain (internal/modules/cjs/loader.js:*:*)
1920
at startup (internal/bootstrap/node.js:*:*)

test/message/nexttick_throw.out

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
^
55
ReferenceError: undefined_reference_error_maker is not defined
66
at *test*message*nexttick_throw.js:*:*
7+
at internalTickCallback (internal/process/next_tick.js:*:*)
78
at process._tickCallback (internal/process/next_tick.js:*:*)
89
at Function.Module.runMain (internal/modules/cjs/loader.js:*:*)
910
at startup (internal/bootstrap/node.js:*:*)

0 commit comments

Comments
 (0)