0
0
mirror of https://github.com/nodejs/node.git synced 2024-12-01 16:10:02 +01:00
nodejs/lib/timers.js
Ben Noordhuis 31da9758a0 lib: don't penalize setTimeout() common case
The common case is where setTimeout() is called with two arguments,
the callback and the timeout.  Specifying optional arguments in the
parameter list forces common case calls to go through an arguments
adaptor stack frame.

PR-URL: https://github.com/iojs/io.js/pull/1221
Reviewed-By: Trevor Norris <trev.norris@gmail.com>
2015-03-20 23:42:26 +01:00

570 lines
13 KiB
JavaScript

'use strict';
const Timer = process.binding('timer_wrap').Timer;
const L = require('_linklist');
const assert = require('assert').ok;
const util = require('util');
const debug = util.debuglog('timer');
const kOnTimeout = Timer.kOnTimeout | 0;
// Timeout values > TIMEOUT_MAX are set to 1.
const TIMEOUT_MAX = 2147483647; // 2^31-1
// IDLE TIMEOUTS
//
// Because often many sockets will have the same idle timeout we will not
// use one timeout watcher per item. It is too much overhead. Instead
// we'll use a single watcher for all sockets with the same timeout value
// and a linked list. This technique is described in the libev manual:
// http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#Be_smart_about_timeouts
// Object containing all lists, timers
// key = time in milliseconds
// value = list
var lists = {};
// the main function - creates lists on demand and the watchers associated
// with them.
function insert(item, msecs) {
item._idleStart = Timer.now();
item._idleTimeout = msecs;
if (msecs < 0) return;
var list;
if (lists[msecs]) {
list = lists[msecs];
} else {
list = new Timer();
list.start(msecs, 0);
L.init(list);
lists[msecs] = list;
list.msecs = msecs;
list[kOnTimeout] = listOnTimeout;
}
L.append(list, item);
assert(!L.isEmpty(list)); // list is not empty
}
function listOnTimeout() {
var msecs = this.msecs;
var list = this;
debug('timeout callback %d', msecs);
var now = Timer.now();
debug('now: %s', now);
var diff, first, threw;
while (first = L.peek(list)) {
diff = now - first._idleStart;
if (diff < msecs) {
list.start(msecs - diff, 0);
debug('%d list wait because diff is %d', msecs, diff);
return;
} else {
L.remove(first);
assert(first !== L.peek(list));
if (!first._onTimeout) continue;
// v0.4 compatibility: if the timer callback throws and the
// domain or uncaughtException handler ignore the exception,
// other timers that expire on this tick should still run.
//
// https://github.com/joyent/node/issues/2631
var domain = first.domain;
if (domain && domain._disposed)
continue;
try {
if (domain)
domain.enter();
threw = true;
first._onTimeout();
if (domain)
domain.exit();
threw = false;
} finally {
if (threw) {
// We need to continue processing after domain error handling
// is complete, but not by using whatever domain was left over
// when the timeout threw its exception.
var oldDomain = process.domain;
process.domain = null;
process.nextTick(function() {
list[kOnTimeout]();
});
process.domain = oldDomain;
}
}
}
}
debug('%d list empty', msecs);
assert(L.isEmpty(list));
list.close();
delete lists[msecs];
}
const unenroll = exports.unenroll = function(item) {
L.remove(item);
var list = lists[item._idleTimeout];
// if empty then stop the watcher
debug('unenroll');
if (list && L.isEmpty(list)) {
debug('unenroll: list empty');
list.close();
delete lists[item._idleTimeout];
}
// if active is called later, then we want to make sure not to insert again
item._idleTimeout = -1;
};
// Does not start the time, just sets up the members needed.
exports.enroll = function(item, msecs) {
if (typeof msecs !== 'number') {
throw new TypeError('msecs must be a number');
}
if (msecs < 0 || !isFinite(msecs)) {
throw new RangeError('msecs must be a non-negative finite number');
}
// if this item was already in a list somewhere
// then we should unenroll it from that
if (item._idleNext) unenroll(item);
// Ensure that msecs fits into signed int32
if (msecs > TIMEOUT_MAX) {
msecs = TIMEOUT_MAX;
}
item._idleTimeout = msecs;
L.init(item);
};
// call this whenever the item is active (not idle)
// it will reset its timeout.
exports.active = function(item) {
var msecs = item._idleTimeout;
if (msecs >= 0) {
var list = lists[msecs];
if (!list || L.isEmpty(list)) {
insert(item, msecs);
} else {
item._idleStart = Timer.now();
L.append(list, item);
}
}
};
/*
* DOM-style timers
*/
exports.setTimeout = function(callback, after) {
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);
var length = arguments.length;
var ontimeout = callback;
switch (length) {
// fast cases
case 0:
case 1:
case 2:
break;
case 3:
ontimeout = callback.bind(timer, arguments[2]);
break;
case 4:
ontimeout = callback.bind(timer, arguments[2], arguments[3]);
break;
case 5:
ontimeout =
callback.bind(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.bind(callback, timer, args);
break;
}
timer._onTimeout = ontimeout;
if (process.domain) timer.domain = process.domain;
exports.active(timer);
return timer;
};
exports.clearTimeout = function(timer) {
if (timer && (timer[kOnTimeout] || timer._onTimeout)) {
timer[kOnTimeout] = timer._onTimeout = null;
if (timer instanceof Timeout) {
timer.close(); // for after === 0
} else {
exports.unenroll(timer);
}
}
};
exports.setInterval = function(callback, repeat, arg1, arg2, arg3) {
repeat *= 1; // coalesce to number or NaN
if (!(repeat >= 1 && repeat <= TIMEOUT_MAX)) {
repeat = 1; // schedule on next tick, follows browser behaviour
}
var args, i;
var timer = new Timeout(repeat);
var len = arguments.length - 2;
timer._onTimeout = wrapper;
timer._repeat = true;
// Initialize args once for repeated invocation of slow case below
if (len > 3) {
args = new Array(len);
for (i = 0; i < len; i++)
args[i] = arguments[i + 2];
}
if (process.domain) timer.domain = process.domain;
exports.active(timer);
return timer;
function wrapper() {
switch (len) {
// fast cases
case 0:
callback.call(this);
break;
case 1:
callback.call(this, arg1);
break;
case 2:
callback.call(this, arg1, arg2);
break;
case 3:
callback.call(this, arg1, arg2, arg3);
break;
// slow case
default:
callback.apply(this, args);
break;
}
// If callback called clearInterval().
if (timer._repeat === false) return;
// If timer is unref'd (or was - it's permanently removed from the list.)
if (this._handle) {
this._handle.start(repeat, 0);
} else {
timer._idleTimeout = repeat;
exports.active(timer);
}
}
};
exports.clearInterval = function(timer) {
if (timer && timer._repeat) {
timer._repeat = false;
clearTimeout(timer);
}
};
const Timeout = function(after) {
this._idleTimeout = after;
this._idlePrev = this;
this._idleNext = this;
this._idleStart = null;
this._onTimeout = null;
this._repeat = false;
};
Timeout.prototype.unref = function() {
if (this._handle) {
this._handle.unref();
} else if (typeof(this._onTimeout) === 'function') {
var now = Timer.now();
if (!this._idleStart) this._idleStart = now;
var delay = this._idleStart + this._idleTimeout - now;
if (delay < 0) delay = 0;
exports.unenroll(this);
this._handle = new Timer();
this._handle[kOnTimeout] = this._onTimeout;
this._handle.start(delay, 0);
this._handle.domain = this.domain;
this._handle.unref();
}
};
Timeout.prototype.ref = function() {
if (this._handle)
this._handle.ref();
};
Timeout.prototype.close = function() {
this._onTimeout = null;
if (this._handle) {
this._handle[kOnTimeout] = null;
this._handle.close();
} else {
exports.unenroll(this);
}
};
var immediateQueue = {};
L.init(immediateQueue);
function processImmediate() {
var queue = immediateQueue;
var domain, immediate;
immediateQueue = {};
L.init(immediateQueue);
while (L.isEmpty(queue) === false) {
immediate = L.shift(queue);
domain = immediate.domain;
if (domain)
domain.enter();
var threw = true;
try {
immediate._onImmediate();
threw = false;
} finally {
if (threw) {
if (!L.isEmpty(queue)) {
// Handle any remaining on next tick, assuming we're still
// alive to do so.
while (!L.isEmpty(immediateQueue)) {
L.append(queue, L.shift(immediateQueue));
}
immediateQueue = queue;
process.nextTick(processImmediate);
}
}
}
if (domain)
domain.exit();
}
// Only round-trip to C++ land if we have to. Calling clearImmediate() on an
// immediate that's in |queue| is okay. Worst case is we make a superfluous
// call to NeedImmediateCallbackSetter().
if (L.isEmpty(immediateQueue)) {
process._needImmediateCallback = false;
}
}
function Immediate() { }
Immediate.prototype.domain = undefined;
Immediate.prototype._onImmediate = undefined;
Immediate.prototype._idleNext = undefined;
Immediate.prototype._idlePrev = undefined;
exports.setImmediate = function(callback, arg1, arg2, arg3) {
var i, args;
var len = arguments.length;
var immediate = new Immediate();
L.init(immediate);
switch (len) {
// fast cases
case 0:
case 1:
immediate._onImmediate = callback;
break;
case 2:
immediate._onImmediate = function() {
callback.call(immediate, arg1);
};
break;
case 3:
immediate._onImmediate = function() {
callback.call(immediate, arg1, arg2);
};
break;
case 4:
immediate._onImmediate = function() {
callback.call(immediate, arg1, arg2, arg3);
};
break;
// slow case
default:
args = new Array(len - 1);
for (i = 1; i < len; i++)
args[i - 1] = arguments[i];
immediate._onImmediate = function() {
callback.apply(immediate, args);
};
break;
}
if (!process._needImmediateCallback) {
process._needImmediateCallback = true;
process._immediateCallback = processImmediate;
}
if (process.domain)
immediate.domain = process.domain;
L.append(immediateQueue, immediate);
return immediate;
};
exports.clearImmediate = function(immediate) {
if (!immediate) return;
immediate._onImmediate = undefined;
L.remove(immediate);
if (L.isEmpty(immediateQueue)) {
process._needImmediateCallback = false;
}
};
// Internal APIs that need timeouts should use timers._unrefActive instead of
// timers.active as internal timeouts shouldn't hold the loop open
var unrefList, unrefTimer;
function unrefTimeout() {
var now = Timer.now();
debug('unrefTimer fired');
var diff, domain, first, threw;
while (first = L.peek(unrefList)) {
diff = now - first._idleStart;
if (diff < first._idleTimeout) {
diff = first._idleTimeout - diff;
unrefTimer.start(diff, 0);
unrefTimer.when = now + diff;
debug('unrefTimer rescheudling for later');
return;
}
L.remove(first);
domain = first.domain;
if (!first._onTimeout) continue;
if (domain && domain._disposed) continue;
try {
if (domain) domain.enter();
threw = true;
debug('unreftimer firing timeout');
first._onTimeout();
threw = false;
if (domain)
domain.exit();
} finally {
if (threw) process.nextTick(unrefTimeout);
}
}
debug('unrefList is empty');
unrefTimer.when = -1;
}
exports._unrefActive = function(item) {
var msecs = item._idleTimeout;
if (!msecs || msecs < 0) return;
assert(msecs >= 0);
L.remove(item);
if (!unrefList) {
debug('unrefList initialized');
unrefList = {};
L.init(unrefList);
debug('unrefTimer initialized');
unrefTimer = new Timer();
unrefTimer.unref();
unrefTimer.when = -1;
unrefTimer[kOnTimeout] = unrefTimeout;
}
var now = Timer.now();
item._idleStart = now;
if (L.isEmpty(unrefList)) {
debug('unrefList empty');
L.append(unrefList, item);
unrefTimer.start(msecs, 0);
unrefTimer.when = now + msecs;
debug('unrefTimer scheduled');
return;
}
var when = now + msecs;
debug('unrefList find where we can insert');
var cur, them;
for (cur = unrefList._idlePrev; cur != unrefList; cur = cur._idlePrev) {
them = cur._idleStart + cur._idleTimeout;
if (when < them) {
debug('unrefList inserting into middle of list');
L.append(cur, item);
if (unrefTimer.when > when) {
debug('unrefTimer is scheduled to fire too late, reschedule');
unrefTimer.start(msecs, 0);
unrefTimer.when = when;
}
return;
}
}
debug('unrefList append to end');
L.append(unrefList, item);
};