-
Notifications
You must be signed in to change notification settings - Fork 29.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
process: refactor nextTick for clarity #17738
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,47 +1,9 @@ | ||
'use strict'; | ||
|
||
// This value is used to prevent the nextTickQueue from becoming too | ||
// large and cause the process to run out of memory. When this value | ||
// is reached the nextTimeQueue array will be shortened (see tickDone | ||
// for details). | ||
const kMaxCallbacksPerLoop = 1e4; | ||
|
||
exports.setup = setupNextTick; | ||
// Will be overwritten when setupNextTick() is called. | ||
exports.nextTick = null; | ||
|
||
class NextTickQueue { | ||
constructor() { | ||
this.head = null; | ||
this.tail = null; | ||
} | ||
|
||
push(v) { | ||
const entry = { data: v, next: null }; | ||
if (this.tail !== null) | ||
this.tail.next = entry; | ||
else | ||
this.head = entry; | ||
this.tail = entry; | ||
} | ||
|
||
shift() { | ||
if (this.head === null) | ||
return; | ||
const ret = this.head.data; | ||
if (this.head === this.tail) | ||
this.head = this.tail = null; | ||
else | ||
this.head = this.head.next; | ||
return ret; | ||
} | ||
|
||
clear() { | ||
this.head = null; | ||
this.tail = null; | ||
} | ||
} | ||
|
||
function setupNextTick() { | ||
const async_wrap = process.binding('async_wrap'); | ||
const async_hooks = require('internal/async_hooks'); | ||
|
@@ -56,15 +18,47 @@ function setupNextTick() { | |
// Grab the constants necessary for working with internal arrays. | ||
const { kInit, kDestroy, kAsyncIdCounter } = async_wrap.constants; | ||
const { async_id_symbol, trigger_async_id_symbol } = async_wrap; | ||
const nextTickQueue = new NextTickQueue(); | ||
var microtasksScheduled = false; | ||
|
||
// Used to run V8's micro task queue. | ||
var _runMicrotasks = {}; | ||
// tickInfo is used so that the C++ code in src/node.cc can | ||
// have easy access to our nextTick state, and avoid unnecessary | ||
// calls into JS land. | ||
// runMicrotasks is used to run V8's micro task queue. | ||
const [ | ||
tickInfo, | ||
runMicrotasks | ||
] = process._setupNextTick(_tickCallback); | ||
|
||
// *Must* match Environment::TickInfo::Fields in src/env.h. | ||
var kIndex = 0; | ||
var kLength = 1; | ||
const kScheduled = 0; | ||
|
||
const nextTickQueue = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm curious, why make this an object literal instead of the class that existed before? I would think V8 still optimizes classes (using hidden classes) better than object literals. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AFAIK, It does the same thing with both in terms of hidden classes. (That's part of the reason The reasoning for the change is mostly that it's straight-up confusing the first time one looks at this code, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. Properly we could merge the code for the PS: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally, we wouldn't. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have very different concepts of what is ideal :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using doubly linked list, despite it being completely unnecessary for nextTick, would come with a significant performance regression and memory consumption increase... |
||
head: null, | ||
tail: null, | ||
push(data) { | ||
const entry = { data, next: null }; | ||
if (this.tail !== null) { | ||
this.tail.next = entry; | ||
} else { | ||
this.head = entry; | ||
tickInfo[kScheduled] = 1; | ||
} | ||
this.tail = entry; | ||
}, | ||
shift() { | ||
if (this.head === null) | ||
return; | ||
const ret = this.head.data; | ||
if (this.head === this.tail) { | ||
this.head = this.tail = null; | ||
tickInfo[kScheduled] = 0; | ||
} else { | ||
this.head = this.head.next; | ||
} | ||
return ret; | ||
} | ||
}; | ||
|
||
var microtasksScheduled = false; | ||
|
||
process.nextTick = nextTick; | ||
// Needs to be accessible from beyond this scope. | ||
|
@@ -73,25 +67,6 @@ function setupNextTick() { | |
// Set the nextTick() function for internal usage. | ||
exports.nextTick = internalNextTick; | ||
|
||
// This tickInfo thing is used so that the C++ code in src/node.cc | ||
// can have easy access to our nextTick state, and avoid unnecessary | ||
// calls into JS land. | ||
const tickInfo = process._setupNextTick(_tickCallback, _runMicrotasks); | ||
|
||
_runMicrotasks = _runMicrotasks.runMicrotasks; | ||
|
||
function tickDone() { | ||
if (tickInfo[kLength] !== 0) { | ||
if (tickInfo[kLength] <= tickInfo[kIndex]) { | ||
nextTickQueue.clear(); | ||
tickInfo[kLength] = 0; | ||
} else { | ||
tickInfo[kLength] -= tickInfo[kIndex]; | ||
} | ||
} | ||
tickInfo[kIndex] = 0; | ||
} | ||
|
||
const microTasksTickObject = { | ||
callback: runMicrotasksCallback, | ||
args: undefined, | ||
|
@@ -105,38 +80,27 @@ function setupNextTick() { | |
// For the moment all microtasks come from the void until the PromiseHook | ||
// API is implemented. | ||
nextTickQueue.push(microTasksTickObject); | ||
|
||
tickInfo[kLength]++; | ||
microtasksScheduled = true; | ||
} | ||
|
||
function runMicrotasksCallback() { | ||
microtasksScheduled = false; | ||
_runMicrotasks(); | ||
runMicrotasks(); | ||
|
||
if (tickInfo[kIndex] < tickInfo[kLength] || | ||
emitPendingUnhandledRejections()) { | ||
if (nextTickQueue.head !== null || emitPendingUnhandledRejections()) | ||
scheduleMicrotasks(); | ||
} | ||
} | ||
|
||
function _tickCallback() { | ||
let tock; | ||
do { | ||
while (tickInfo[kIndex] < tickInfo[kLength]) { | ||
++tickInfo[kIndex]; | ||
const tock = nextTickQueue.shift(); | ||
|
||
// CHECK(Number.isSafeInteger(tock[async_id_symbol])) | ||
// CHECK(tock[async_id_symbol] > 0) | ||
// CHECK(Number.isSafeInteger(tock[trigger_async_id_symbol])) | ||
// CHECK(tock[trigger_async_id_symbol] > 0) | ||
|
||
while (tock = nextTickQueue.shift()) { | ||
const asyncId = tock[async_id_symbol]; | ||
emitBefore(asyncId, tock[trigger_async_id_symbol]); | ||
// emitDestroy() places the async_id_symbol into an asynchronous queue | ||
// that calls the destroy callback in the future. It's called before | ||
// calling tock.callback so destroy will be called even if the callback | ||
// throws an exception that is handles by 'uncaughtException' or a | ||
// throws an exception that is handled by 'uncaughtException' or a | ||
// domain. | ||
// TODO(trevnorris): This is a bit of a hack. It relies on the fact | ||
// that nextTick() doesn't allow the event loop to proceed, but if | ||
|
@@ -152,24 +116,21 @@ function setupNextTick() { | |
Reflect.apply(callback, undefined, tock.args); | ||
|
||
emitAfter(asyncId); | ||
|
||
if (kMaxCallbacksPerLoop < tickInfo[kIndex]) | ||
tickDone(); | ||
} | ||
tickDone(); | ||
_runMicrotasks(); | ||
runMicrotasks(); | ||
emitPendingUnhandledRejections(); | ||
} while (tickInfo[kLength] !== 0); | ||
} while (nextTickQueue.head !== null); | ||
} | ||
|
||
class TickObject { | ||
constructor(callback, args, asyncId, triggerAsyncId) { | ||
constructor(callback, args, triggerAsyncId) { | ||
// this must be set to null first to avoid function tracking | ||
// on the hidden class, revisit in V8 versions after 6.2 | ||
this.callback = null; | ||
this.callback = callback; | ||
this.args = args; | ||
|
||
const asyncId = ++async_id_fields[kAsyncIdCounter]; | ||
this[async_id_symbol] = asyncId; | ||
this[trigger_async_id_symbol] = triggerAsyncId; | ||
|
||
|
@@ -203,13 +164,7 @@ function setupNextTick() { | |
args[i - 1] = arguments[i]; | ||
} | ||
|
||
// In V8 6.2, moving tickInfo & async_id_fields[kAsyncIdCounter] into the | ||
// TickObject incurs a significant performance penalty in the | ||
// next-tick-breadth-args benchmark (revisit later) | ||
++tickInfo[kLength]; | ||
nextTickQueue.push(new TickObject(callback, | ||
args, | ||
++async_id_fields[kAsyncIdCounter], | ||
nextTickQueue.push(new TickObject(callback, args, | ||
getDefaultTriggerAsyncId())); | ||
} | ||
|
||
|
@@ -238,13 +193,6 @@ function setupNextTick() { | |
|
||
if (triggerAsyncId === null) | ||
triggerAsyncId = getDefaultTriggerAsyncId(); | ||
// In V8 6.2, moving tickInfo & async_id_fields[kAsyncIdCounter] into the | ||
// TickObject incurs a significant performance penalty in the | ||
// next-tick-breadth-args benchmark (revisit later) | ||
++tickInfo[kLength]; | ||
nextTickQueue.push(new TickObject(callback, | ||
args, | ||
++async_id_fields[kAsyncIdCounter], | ||
triggerAsyncId)); | ||
nextTickQueue.push(new TickObject(callback, args, triggerAsyncId)); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this expose infinite nextTick loop problems? and / or, isn't this a breaking change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This became irrelevant when @mscdex turned this into a linked list instead of an Array. I don't really see that it does anything right now other than make sure the indexes don't overflow and since I got rid of length & index, it's no longer necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, FWIW, our benchmarks are an indirect test for this and they still pass. (If one were to remove this without removing the index & length values then the integers overflow.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought this was to prevent infinite
nextTick
recursion... I'm not sure how a linkedlist helps that? Maybe I didn't look deep enough? Not sure.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it doesn't prevent infinite recursion. Nothing does, our documentation even calls it out as an issue.
When an Array used to be used for
nextTick
, we wouldn't clear it until 1e4 runs so it could grow to near Infinite size without this condition to remove stale tick objects.Edit: This was confusingly worded. What I mean is: before this 1e4 limit was introduced, we never cleared the Array until the whole outer loop completed but this meant that
_tickCallback
could run long enough until thenextTickQueue
array was so big that it hogged all available memory. The 1e4 limit was then introduced to occasionally empty thenextTickQueue
so the process wouldn't run out of memory.With a linked list this is no longer necessary as on each
tock
it adjusts the linked list and the GC can do its job whenever it wants/needs to. The only reason that code then remained was for the cases where an integer overflow could occur due to reaching too large of an index & length.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://nodejs.org/dist/latest-v9.x/docs/api/process.html#process_process_nexttick_callback_args
Just try
const set = () => process.nextTick(set); set();
inrepl
and be prepared to force quit it. 😆There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Fishrock123 here's the commit that introduced this code apapirovski@5757642
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@apapirovski That was a very helpful explanation… on some level it’s sad to understand code only once it’s going away, but I always wondered about this. :)