process: improve nextTick() performance

PR-URL: https://github.com/nodejs/node/pull/13446
Reviewed-By: Refael Ackermann <refack@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com>
Reviewed-By: Evan Lucas <evanlucas@me.com>
This commit is contained in:
Brian White 2017-06-03 23:28:57 -04:00
parent 1e2905f46a
commit 460ee75f7e
No known key found for this signature in database
GPG Key ID: 606D7358F94DA209
3 changed files with 97 additions and 45 deletions

View File

@ -2,7 +2,7 @@
var common = require('../common.js'); var common = require('../common.js');
var bench = common.createBenchmark(main, { var bench = common.createBenchmark(main, {
millions: [2] millions: [12]
}); });
process.maxTickDepth = Infinity; process.maxTickDepth = Infinity;

View File

@ -1,7 +1,7 @@
'use strict'; 'use strict';
var common = require('../common.js'); var common = require('../common.js');
var bench = common.createBenchmark(main, { var bench = common.createBenchmark(main, {
millions: [2] millions: [12]
}); });
process.maxTickDepth = Infinity; process.maxTickDepth = Infinity;

View File

@ -10,6 +10,42 @@ exports.setup = setupNextTick;
// Will be overwritten when setupNextTick() is called. // Will be overwritten when setupNextTick() is called.
exports.nextTick = null; exports.nextTick = null;
class NextTickQueue {
constructor() {
this.head = null;
this.tail = null;
this.length = 0;
}
push(v) {
const entry = { data: v, next: null };
if (this.length > 0)
this.tail.next = entry;
else
this.head = entry;
this.tail = entry;
++this.length;
}
shift() {
if (this.length === 0)
return;
const ret = this.head.data;
if (this.length === 1)
this.head = this.tail = null;
else
this.head = this.head.next;
--this.length;
return ret;
}
clear() {
this.head = null;
this.tail = null;
this.length = 0;
}
}
function setupNextTick() { function setupNextTick() {
const async_wrap = process.binding('async_wrap'); const async_wrap = process.binding('async_wrap');
const async_hooks = require('async_hooks'); const async_hooks = require('async_hooks');
@ -27,7 +63,7 @@ function setupNextTick() {
const { kInit, kBefore, kAfter, kDestroy, kAsyncUidCntr, kInitTriggerId } = const { kInit, kBefore, kAfter, kDestroy, kAsyncUidCntr, kInitTriggerId } =
async_wrap.constants; async_wrap.constants;
const { async_id_symbol, trigger_id_symbol } = async_wrap; const { async_id_symbol, trigger_id_symbol } = async_wrap;
var nextTickQueue = []; var nextTickQueue = new NextTickQueue();
var microtasksScheduled = false; var microtasksScheduled = false;
// Used to run V8's micro task queue. // Used to run V8's micro task queue.
@ -55,27 +91,29 @@ function setupNextTick() {
function tickDone() { function tickDone() {
if (tickInfo[kLength] !== 0) { if (tickInfo[kLength] !== 0) {
if (tickInfo[kLength] <= tickInfo[kIndex]) { if (tickInfo[kLength] <= tickInfo[kIndex]) {
nextTickQueue = []; nextTickQueue.clear();
tickInfo[kLength] = 0; tickInfo[kLength] = 0;
} else { } else {
nextTickQueue.splice(0, tickInfo[kIndex]);
tickInfo[kLength] = nextTickQueue.length; tickInfo[kLength] = nextTickQueue.length;
} }
} }
tickInfo[kIndex] = 0; tickInfo[kIndex] = 0;
} }
const microTasksTickObject = {
callback: runMicrotasksCallback,
args: undefined,
domain: null,
[async_id_symbol]: 0,
[trigger_id_symbol]: 0
};
function scheduleMicrotasks() { function scheduleMicrotasks() {
if (microtasksScheduled) if (microtasksScheduled)
return; return;
const tickObject =
new TickObject(runMicrotasksCallback, undefined, null);
// For the moment all microtasks come from the void until the PromiseHook // For the moment all microtasks come from the void until the PromiseHook
// API is implemented. // API is implemented.
tickObject[async_id_symbol] = 0; nextTickQueue.push(microTasksTickObject);
tickObject[trigger_id_symbol] = 0;
nextTickQueue.push(tickObject);
tickInfo[kLength]++; tickInfo[kLength]++;
microtasksScheduled = true; microtasksScheduled = true;
@ -86,8 +124,9 @@ function setupNextTick() {
_runMicrotasks(); _runMicrotasks();
if (tickInfo[kIndex] < tickInfo[kLength] || if (tickInfo[kIndex] < tickInfo[kLength] ||
emitPendingUnhandledRejections()) emitPendingUnhandledRejections()) {
scheduleMicrotasks(); scheduleMicrotasks();
}
} }
function _combinedTickCallback(args, callback) { function _combinedTickCallback(args, callback) {
@ -133,7 +172,8 @@ function setupNextTick() {
function _tickCallback() { function _tickCallback() {
do { do {
while (tickInfo[kIndex] < tickInfo[kLength]) { while (tickInfo[kIndex] < tickInfo[kLength]) {
const tock = nextTickQueue[tickInfo[kIndex]++]; ++tickInfo[kIndex];
const tock = nextTickQueue.shift();
const callback = tock.callback; const callback = tock.callback;
const args = tock.args; const args = tock.args;
@ -174,7 +214,8 @@ function setupNextTick() {
function _tickDomainCallback() { function _tickDomainCallback() {
do { do {
while (tickInfo[kIndex] < tickInfo[kLength]) { while (tickInfo[kIndex] < tickInfo[kLength]) {
const tock = nextTickQueue[tickInfo[kIndex]++]; ++tickInfo[kIndex];
const tock = nextTickQueue.shift();
const callback = tock.callback; const callback = tock.callback;
const domain = tock.domain; const domain = tock.domain;
const args = tock.args; const args = tock.args;
@ -210,45 +251,48 @@ function setupNextTick() {
} while (tickInfo[kLength] !== 0); } while (tickInfo[kLength] !== 0);
} }
function TickObject(callback, args, domain) { class TickObject {
this.callback = callback; constructor(callback, args, asyncId, triggerAsyncId) {
this.domain = domain; this.callback = callback;
this.args = args; this.args = args;
this[async_id_symbol] = -1; this.domain = process.domain || null;
this[trigger_id_symbol] = -1; this[async_id_symbol] = asyncId;
} this[trigger_id_symbol] = triggerAsyncId;
function setupInit(tickObject, triggerAsyncId) {
tickObject[async_id_symbol] = ++async_uid_fields[kAsyncUidCntr];
tickObject[trigger_id_symbol] = triggerAsyncId || initTriggerId();
if (async_hook_fields[kInit] > 0) {
emitInit(tickObject[async_id_symbol],
'TickObject',
tickObject[trigger_id_symbol],
tickObject);
} }
} }
// `nextTick()` will not enqueue any callback when the process is about to
// exit since the callback would not have a chance to be executed.
function nextTick(callback) { function nextTick(callback) {
if (typeof callback !== 'function') if (typeof callback !== 'function')
throw new errors.TypeError('ERR_INVALID_CALLBACK'); throw new errors.TypeError('ERR_INVALID_CALLBACK');
// on the way out, don't bother. it won't get fired anyway.
if (process._exiting) if (process._exiting)
return; return;
var args; var args;
if (arguments.length > 1) { switch (arguments.length) {
args = new Array(arguments.length - 1); case 1: break;
for (var i = 1; i < arguments.length; i++) case 2: args = [arguments[1]]; break;
args[i - 1] = arguments[i]; case 3: args = [arguments[1], arguments[2]]; break;
case 4: args = [arguments[1], arguments[2], arguments[3]]; break;
default:
args = new Array(arguments.length - 1);
for (var i = 1; i < arguments.length; i++)
args[i - 1] = arguments[i];
} }
var obj = new TickObject(callback, args, process.domain || null); const asyncId = ++async_uid_fields[kAsyncUidCntr];
setupInit(obj, null); const triggerAsyncId = initTriggerId();
const obj = new TickObject(callback, args, asyncId, triggerAsyncId);
nextTickQueue.push(obj); nextTickQueue.push(obj);
tickInfo[kLength]++; ++tickInfo[kLength];
if (async_hook_fields[kInit] > 0)
emitInit(asyncId, 'TickObject', triggerAsyncId, obj);
} }
// `internalNextTick()` will not enqueue any callback when the process is
// about to exit since the callback would not have a chance to be executed.
function internalNextTick(triggerAsyncId, callback) { function internalNextTick(triggerAsyncId, callback) {
if (typeof callback !== 'function') if (typeof callback !== 'function')
throw new TypeError('callback is not a function'); throw new TypeError('callback is not a function');
@ -259,17 +303,25 @@ function setupNextTick() {
return; return;
var args; var args;
if (arguments.length > 2) { switch (arguments.length) {
args = new Array(arguments.length - 2); case 2: break;
for (var i = 2; i < arguments.length; i++) case 3: args = [arguments[2]]; break;
args[i - 2] = arguments[i]; case 4: args = [arguments[2], arguments[3]]; break;
case 5: args = [arguments[2], arguments[3], arguments[4]]; break;
default:
args = new Array(arguments.length - 2);
for (var i = 2; i < arguments.length; i++)
args[i - 2] = arguments[i];
} }
var obj = new TickObject(callback, args, process.domain || null); const asyncId = ++async_uid_fields[kAsyncUidCntr];
setupInit(obj, triggerAsyncId); const obj = new TickObject(callback, args, asyncId, triggerAsyncId);
nextTickQueue.push(obj);
++tickInfo[kLength];
if (async_hook_fields[kInit] > 0)
emitInit(asyncId, 'TickObject', triggerAsyncId, obj);
// The call to initTriggerId() was skipped, so clear kInitTriggerId. // The call to initTriggerId() was skipped, so clear kInitTriggerId.
async_uid_fields[kInitTriggerId] = 0; async_uid_fields[kInitTriggerId] = 0;
nextTickQueue.push(obj);
tickInfo[kLength]++;
} }
} }