-
-
Notifications
You must be signed in to change notification settings - Fork 33.3k
timers: improve setTimeout/Interval performance #8661
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
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 |
---|---|---|
|
@@ -94,8 +94,8 @@ const TIMEOUT_MAX = 2147483647; // 2^31-1 | |
// | ||
// - key = time in milliseconds | ||
// - value = linked list | ||
const refedLists = {}; | ||
const unrefedLists = {}; | ||
const refedLists = Object.create(null); | ||
const unrefedLists = Object.create(null); | ||
|
||
|
||
|
||
// Schedule or re-schedule a timer. | ||
|
@@ -128,23 +128,28 @@ function insert(item, unrefed) { | |
var list = lists[msecs]; | ||
if (!list) { | ||
debug('no %d list was found in insert, creating a new one', msecs); | ||
// Make a new linked list of timers, and create a TimerWrap to schedule | ||
// processing for the list. | ||
list = new TimersList(msecs, unrefed); | ||
L.init(list); | ||
list._timer._list = list; | ||
|
||
if (unrefed === true) list._timer.unref(); | ||
list._timer.start(msecs); | ||
|
||
lists[msecs] = list; | ||
list._timer[kOnTimeout] = listOnTimeout; | ||
lists[msecs] = list = createTimersList(msecs, unrefed); | ||
} | ||
|
||
L.append(list, item); | ||
assert(!L.isEmpty(list)); // list is not empty | ||
} | ||
|
||
function createTimersList(msecs, unrefed) { | ||
|
||
// Make a new linked list of timers, and create a TimerWrap to schedule | ||
// processing for the list. | ||
const list = new TimersList(msecs, unrefed); | ||
L.init(list); | ||
list._timer._list = list; | ||
|
||
if (unrefed === true) list._timer.unref(); | ||
list._timer.start(msecs); | ||
|
||
list._timer[kOnTimeout] = listOnTimeout; | ||
|
||
return list; | ||
} | ||
|
||
function TimersList(msecs, unrefed) { | ||
this._idleNext = null; // Create the list with the linkedlist properties to | ||
this._idlePrev = null; // prevent any unnecessary hidden class changes. | ||
|
@@ -229,7 +234,7 @@ function tryOnTimeout(timer, list) { | |
timer._called = true; | ||
var threw = true; | ||
try { | ||
timer._onTimeout(); | ||
ontimeout(timer); | ||
|
||
threw = false; | ||
} finally { | ||
if (!threw) return; | ||
|
@@ -317,51 +322,76 @@ exports.enroll = function(item, msecs) { | |
*/ | ||
|
||
|
||
exports.setTimeout = function(callback, after) { | ||
exports.setTimeout = function(callback, after, arg1, arg2, arg3) { | ||
|
||
if (typeof callback !== 'function') { | ||
throw new TypeError('"callback" argument must be a function'); | ||
} | ||
|
||
after *= 1; // coalesce to number or NaN | ||
|
||
if (!(after >= 1 && after <= TIMEOUT_MAX)) { | ||
after = 1; // schedule on next tick, follows browser behaviour | ||
var len = arguments.length; | ||
|
||
var args; | ||
if (len === 3) { | ||
args = [arg1]; | ||
} else if (len === 4) { | ||
args = [arg1, arg2]; | ||
} else if (len > 4) { | ||
args = [arg1, arg2, arg3]; | ||
for (var i = 5; i < len; i++) | ||
// extend array dynamically, makes .apply run much faster in v6.0.0 | ||
args[i - 2] = arguments[i]; | ||
} | ||
|
||
var timer = new Timeout(after); | ||
var length = arguments.length; | ||
var ontimeout = callback; | ||
switch (length) { | ||
// fast cases | ||
case 1: | ||
case 2: | ||
break; | ||
case 3: | ||
ontimeout = () => callback.call(timer, arguments[2]); | ||
break; | ||
case 4: | ||
ontimeout = () => callback.call(timer, arguments[2], arguments[3]); | ||
break; | ||
case 5: | ||
ontimeout = | ||
() => callback.call(timer, arguments[2], arguments[3], arguments[4]); | ||
break; | ||
// slow case | ||
default: | ||
var args = new Array(length - 2); | ||
for (var i = 2; i < length; i++) | ||
args[i - 2] = arguments[i]; | ||
ontimeout = () => callback.apply(timer, args); | ||
break; | ||
} | ||
timer._onTimeout = ontimeout; | ||
return createSingleTimeout(callback, after, args); | ||
}; | ||
|
||
if (process.domain) timer.domain = process.domain; | ||
function createSingleTimeout(callback, after, args) { | ||
after *= 1; // coalesce to number or NaN | ||
if (!(after >= 1 && after <= TIMEOUT_MAX)) | ||
after = 1; // schedule on next tick, follows browser behaviour | ||
|
||
var timer = new Timeout(after, callback, args); | ||
if (process.domain) | ||
timer.domain = process.domain; | ||
|
||
active(timer); | ||
|
||
return timer; | ||
}; | ||
} | ||
|
||
|
||
function ontimeout(timer) { | ||
var args = timer._timerArgs; | ||
var callback = timer._onTimeout; | ||
if (!args) | ||
callback.call(timer); | ||
else { | ||
switch (args.length) { | ||
case 1: | ||
callback.call(timer, args[0]); | ||
break; | ||
case 2: | ||
callback.call(timer, args[0], args[1]); | ||
break; | ||
case 3: | ||
callback.call(timer, args[0], args[1], args[2]); | ||
break; | ||
default: | ||
callback.apply(timer, args); | ||
} | ||
} | ||
if (timer._repeat) | ||
rearm(timer); | ||
} | ||
|
||
|
||
function rearm(timer) { | ||
// If timer is unref'd (or was - it's permanently removed from the list.) | ||
if (timer._handle && timer instanceof Timeout) { | ||
|
||
timer._handle.start(timer._repeat); | ||
} else { | ||
timer._idleTimeout = timer._repeat; | ||
active(timer); | ||
} | ||
} | ||
|
||
|
||
const clearTimeout = exports.clearTimeout = function(timer) { | ||
|
@@ -376,66 +406,41 @@ const clearTimeout = exports.clearTimeout = function(timer) { | |
}; | ||
|
||
|
||
exports.setInterval = function(callback, repeat) { | ||
exports.setInterval = function(callback, repeat, arg1, arg2, arg3) { | ||
if (typeof callback !== 'function') { | ||
throw new TypeError('"callback" argument must be a function'); | ||
} | ||
|
||
repeat *= 1; // coalesce to number or NaN | ||
var len = arguments.length; | ||
var args; | ||
if (len === 3) { | ||
args = [arg1]; | ||
} else if (len === 4) { | ||
args = [arg1, arg2]; | ||
} else if (len > 4) { | ||
args = [arg1, arg2, arg3]; | ||
for (var i = 5; i < len; i++) | ||
// extend array dynamically, makes .apply run much faster in v6.0.0 | ||
args[i - 2] = arguments[i]; | ||
} | ||
|
||
return createRepeatTimeout(callback, repeat, args); | ||
}; | ||
|
||
if (!(repeat >= 1 && repeat <= TIMEOUT_MAX)) { | ||
function createRepeatTimeout(callback, repeat, args) { | ||
|
||
repeat *= 1; // coalesce to number or NaN | ||
if (!(repeat >= 1 && repeat <= TIMEOUT_MAX)) | ||
repeat = 1; // schedule on next tick, follows browser behaviour | ||
} | ||
|
||
var timer = new Timeout(repeat); | ||
var length = arguments.length; | ||
var ontimeout = callback; | ||
switch (length) { | ||
case 1: | ||
case 2: | ||
break; | ||
case 3: | ||
ontimeout = () => callback.call(timer, arguments[2]); | ||
break; | ||
case 4: | ||
ontimeout = () => callback.call(timer, arguments[2], arguments[3]); | ||
break; | ||
case 5: | ||
ontimeout = | ||
() => callback.call(timer, arguments[2], arguments[3], arguments[4]); | ||
break; | ||
default: | ||
var args = new Array(length - 2); | ||
for (var i = 2; i < length; i += 1) | ||
args[i - 2] = arguments[i]; | ||
ontimeout = () => callback.apply(timer, args); | ||
break; | ||
} | ||
timer._onTimeout = wrapper; | ||
timer._repeat = ontimeout; | ||
var timer = new Timeout(repeat, callback, args); | ||
timer._repeat = repeat; | ||
|
||
if (process.domain) | ||
timer.domain = process.domain; | ||
|
||
if (process.domain) timer.domain = process.domain; | ||
active(timer); | ||
|
||
return timer; | ||
|
||
function wrapper() { | ||
timer._repeat(); | ||
|
||
// Timer might be closed - no point in restarting it | ||
if (!timer._repeat) | ||
return; | ||
|
||
// If timer is unref'd (or was - it's permanently removed from the list.) | ||
if (this._handle) { | ||
this._handle.start(repeat); | ||
} else { | ||
timer._idleTimeout = repeat; | ||
active(timer); | ||
} | ||
} | ||
}; | ||
|
||
} | ||
|
||
exports.clearInterval = function(timer) { | ||
if (timer && timer._repeat) { | ||
|
@@ -445,19 +450,20 @@ exports.clearInterval = function(timer) { | |
}; | ||
|
||
|
||
function Timeout(after) { | ||
function Timeout(after, callback, args) { | ||
this._called = false; | ||
this._idleTimeout = after; | ||
this._idlePrev = this; | ||
this._idleNext = this; | ||
this._idleStart = null; | ||
this._onTimeout = null; | ||
this._onTimeout = callback; | ||
this._timerArgs = args; | ||
this._repeat = null; | ||
} | ||
|
||
|
||
function unrefdHandle() { | ||
this.owner._onTimeout(); | ||
ontimeout(this.owner); | ||
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. This is always better for internal workings. ALWAYS, Node community. |
||
if (!this.owner._repeat) | ||
this.owner.close(); | ||
} | ||
|
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 remember we had a long discussion about this, I think for http. Have we ever come to a conclusion whether this is a good idea?
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.
The objects here are not exposed, so it's not an issue in this case.
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.
OK. And it does improve performance? Because - at least for me - it decreases readability.
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 didn't measure it specifically, but it does remove unnecessary hash table entries. It may also help if V8 sees only numeric keys being set on the object?
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.
@fhinkel Just curious. Do you mean using a Map instead of Object.create? #7581 (comment)
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.
For me it increases readability, but please check performance - I recall there being a regression when using
.create(null)
at one point.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.
@Florian-R Thanks, that's the discussion I was thinking of. But in this case I meant {} vs .create(null).
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.
@benjamingr I'm not sure what regression you're referring to. These objects are created at startup, not during runtime, so the time to create them doesn't matter as much.
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 understand this is not about performance, but just out of curiosity, I measured the performance difference.
console.time("{}"); for (var i = 100000; i--;) { var a = {}; } console.timeEnd("{}"); var create = Object.create; console.time("create"); for (var i = 100000; i--;) { var b = create(null); } console.timeEnd("create");
{}: 5.792ms
create: 15.221ms
Probably, calling a function is heavier than {}
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.
Literals are always faster than instantiation and builder function calls, in JS and most languages.