diff --git a/benchmark/timers/timers-cancel-unpooled.js b/benchmark/timers/timers-cancel-unpooled.js index 7b46bad719a9c3..1daf68bde0ed51 100644 --- a/benchmark/timers/timers-cancel-unpooled.js +++ b/benchmark/timers/timers-cancel-unpooled.js @@ -4,18 +4,26 @@ const assert = require('assert'); const bench = common.createBenchmark(main, { n: [1e6], + direction: ['start', 'end'] }); -function main({ n }) { +function main({ n, direction }) { const timersList = []; for (var i = 0; i < n; i++) { timersList.push(setTimeout(cb, i + 1)); } + var j; bench.start(); - for (var j = 0; j < n + 1; j++) { - clearTimeout(timersList[j]); + if (direction === 'start') { + for (j = 0; j < n; j++) { + clearTimeout(timersList[j]); + } + } else { + for (j = n - 1; j >= 0; j--) { + clearTimeout(timersList[j]); + } } bench.end(n); } diff --git a/benchmark/timers/timers-insert-unpooled.js b/benchmark/timers/timers-insert-unpooled.js index 1f1c5155a77748..232cc7c31aff16 100644 --- a/benchmark/timers/timers-insert-unpooled.js +++ b/benchmark/timers/timers-insert-unpooled.js @@ -4,18 +4,26 @@ const assert = require('assert'); const bench = common.createBenchmark(main, { n: [1e6], + direction: ['start', 'end'] }); -function main({ n }) { +function main({ direction, n }) { const timersList = []; + var i; bench.start(); - for (var i = 0; i < n; i++) { - timersList.push(setTimeout(cb, i + 1)); + if (direction === 'start') { + for (i = 1; i <= n; i++) { + timersList.push(setTimeout(cb, i)); + } + } else { + for (i = n; i > 0; i--) { + timersList.push(setTimeout(cb, i)); + } } bench.end(n); - for (var j = 0; j < n + 1; j++) { + for (var j = 0; j < n; j++) { clearTimeout(timersList[j]); } } diff --git a/benchmark/util/priority-queue.js b/benchmark/util/priority-queue.js new file mode 100644 index 00000000000000..51a696439a2864 --- /dev/null +++ b/benchmark/util/priority-queue.js @@ -0,0 +1,18 @@ +'use strict'; + +const common = require('../common'); + +const bench = common.createBenchmark(main, { + n: [1e6] +}, { flags: ['--expose-internals'] }); + +function main({ n, type }) { + const PriorityQueue = require('internal/priority_queue'); + const queue = new PriorityQueue(); + bench.start(); + for (var i = 0; i < n; i++) + queue.insert(Math.random() * 1e7 | 0); + for (i = 0; i < n; i++) + queue.shift(); + bench.end(n); +} diff --git a/lib/internal/priority_queue.js b/lib/internal/priority_queue.js new file mode 100644 index 00000000000000..cb046507a667d9 --- /dev/null +++ b/lib/internal/priority_queue.js @@ -0,0 +1,111 @@ +'use strict'; + +const kCompare = Symbol('compare'); +const kHeap = Symbol('heap'); +const kSetPosition = Symbol('setPosition'); +const kSize = Symbol('size'); + +// The PriorityQueue is a basic implementation of a binary heap that accepts +// a custom sorting function via its constructor. This function is passed +// the two nodes to compare, similar to the native Array#sort. Crucially +// this enables priority queues that are based on a comparison of more than +// just a single criteria. + +module.exports = class PriorityQueue { + constructor(comparator, setPosition) { + if (comparator !== undefined) + this[kCompare] = comparator; + if (setPosition !== undefined) + this[kSetPosition] = setPosition; + + this[kHeap] = new Array(64); + this[kSize] = 0; + } + + [kCompare](a, b) { + return a - b; + } + + insert(value) { + const heap = this[kHeap]; + let pos = ++this[kSize]; + + if (heap.length === pos) + heap.length *= 2; + + const compare = this[kCompare]; + const setPosition = this[kSetPosition]; + while (pos > 1) { + const parent = heap[pos / 2 | 0]; + if (compare(parent, value) <= 0) + break; + heap[pos] = parent; + if (setPosition !== undefined) + setPosition(parent, pos); + pos = pos / 2 | 0; + } + heap[pos] = value; + if (setPosition !== undefined) + setPosition(value, pos); + } + + peek() { + return this[kHeap][1]; + } + + percolateDown(pos) { + const compare = this[kCompare]; + const setPosition = this[kSetPosition]; + const heap = this[kHeap]; + const size = this[kSize]; + const item = heap[pos]; + + while (pos * 2 <= size) { + let childIndex = pos * 2 + 1; + if (childIndex > size || compare(heap[pos * 2], heap[childIndex]) < 0) + childIndex = pos * 2; + const child = heap[childIndex]; + if (compare(item, child) <= 0) + break; + if (setPosition !== undefined) + setPosition(child, pos); + heap[pos] = child; + pos = childIndex; + } + heap[pos] = item; + if (setPosition !== undefined) + setPosition(item, pos); + } + + removeAt(pos) { + const heap = this[kHeap]; + const size = --this[kSize]; + heap[pos] = heap[size + 1]; + heap[size + 1] = undefined; + + if (size > 0) + this.percolateDown(1); + } + + remove(value) { + const heap = this[kHeap]; + const pos = heap.indexOf(value); + if (pos < 1) + return false; + + this.removeAt(pos); + + return true; + } + + shift() { + const heap = this[kHeap]; + const value = heap[1]; + if (value === undefined) + return; + + this.removeAt(1); + + return value; + } +}; diff --git a/lib/internal/timers.js b/lib/internal/timers.js index 801fce1cc369fd..44ebc65dc1d4aa 100644 --- a/lib/internal/timers.js +++ b/lib/internal/timers.js @@ -19,7 +19,7 @@ const { // Timeout values > TIMEOUT_MAX are set to 1. const TIMEOUT_MAX = 2 ** 31 - 1; -const unrefedSymbol = Symbol('unrefed'); +const kRefed = Symbol('refed'); module.exports = { TIMEOUT_MAX, @@ -27,6 +27,7 @@ module.exports = { async_id_symbol, trigger_async_id_symbol, Timeout, + kRefed, initAsyncResource, setUnrefTimeout, validateTimerDuration @@ -50,7 +51,7 @@ function initAsyncResource(resource, type) { // Timer constructor function. // The entire prototype is defined in lib/timers.js -function Timeout(callback, after, args, isRepeat, isUnrefed) { +function Timeout(callback, after, args, isRepeat) { after *= 1; // coalesce to number or NaN if (!(after >= 1 && after <= TIMEOUT_MAX)) { if (after > TIMEOUT_MAX) { @@ -62,7 +63,6 @@ function Timeout(callback, after, args, isRepeat, isUnrefed) { after = 1; // schedule on next tick, follows browser behavior } - this._called = false; this._idleTimeout = after; this._idlePrev = this; this._idleNext = this; @@ -75,22 +75,16 @@ function Timeout(callback, after, args, isRepeat, isUnrefed) { this._repeat = isRepeat ? after : null; this._destroyed = false; - this[unrefedSymbol] = isUnrefed; + this[kRefed] = null; initAsyncResource(this, 'Timeout'); } Timeout.prototype.refresh = function() { - if (this._handle) { - // Would be more ideal with uv_timer_again(), however that API does not - // cause libuv's sorted timers data structure (a binary heap at the time - // of writing) to re-sort itself. This causes ordering inconsistencies. - this._handle.start(this._idleTimeout); - } else if (this[unrefedSymbol]) { - getTimers()._unrefActive(this); - } else { + if (this[kRefed]) getTimers().active(this); - } + else + getTimers()._unrefActive(this); return this; }; @@ -122,7 +116,7 @@ function setUnrefTimeout(callback, after, arg1, arg2, arg3) { break; } - const timer = new Timeout(callback, after, args, false, true); + const timer = new Timeout(callback, after, args, false); getTimers()._unrefActive(timer); return timer; diff --git a/lib/timers.js b/lib/timers.js index e7f13924db7e59..3d200344a4589a 100644 --- a/lib/timers.js +++ b/lib/timers.js @@ -26,16 +26,17 @@ const { setupTimers, } = process.binding('timer_wrap'); const L = require('internal/linkedlist'); +const PriorityQueue = require('internal/priority_queue'); const { async_id_symbol, trigger_async_id_symbol, Timeout, + kRefed, initAsyncResource, validateTimerDuration } = require('internal/timers'); const internalUtil = require('internal/util'); const { createPromise, promiseResolve } = process.binding('util'); -const assert = require('assert'); const util = require('util'); const { ERR_INVALID_CALLBACK } = require('internal/errors').codes; const debug = util.debuglog('timer'); @@ -55,8 +56,6 @@ const kHasOutstanding = 2; const [immediateInfo, toggleImmediateRef] = setupTimers(processImmediate, processTimers); -const kRefed = Symbol('refed'); - // HOW and WHY the timers implementation works the way it does. // // Timers are crucial to Node.js. Internally, any TCP I/O connection creates a @@ -85,20 +84,17 @@ const kRefed = Symbol('refed'); // // Object maps are kept which contain linked lists keyed by their duration in // milliseconds. -// The linked lists within also have some meta-properties, one of which is a -// TimerWrap C++ handle, which makes the call after the duration to process the -// list it is attached to. // /* eslint-disable node-core/non-ascii-character */ // // ╔════ > Object Map // ║ // ╠══ -// ║ refedLists: { '40': { }, '320': { etc } } (keys of millisecond duration) -// ╚══ ┌─────────┘ +// ║ lists: { '40': { }, '320': { etc } } (keys of millisecond duration) +// ╚══ ┌────┘ // │ // ╔══ │ -// ║ TimersList { _idleNext: { }, _idlePrev: (self), _timer: (TimerWrap) } +// ║ TimersList { _idleNext: { }, _idlePrev: (self) } // ║ ┌────────────────┘ // ║ ╔══ │ ^ // ║ ║ { _idleNext: { }, _idlePrev: { }, _onTimeout: (callback) } @@ -126,36 +122,73 @@ const kRefed = Symbol('refed'); // always be due to timeout at a later time. // // Less-than constant time operations are thus contained in two places: -// TimerWrap's backing libuv timers implementation (a performant heap-based -// queue), and the object map lookup of a specific list by the duration of -// timers within (or creation of a new list). -// However, these operations combined have shown to be trivial in comparison to -// other alternative timers architectures. +// The PriorityQueue — an efficient binary heap implementation that does all +// operations in worst-case O(log n) time — which manages the order of expiring +// Timeout lists and the object map lookup of a specific list by the duration of +// timers within (or creation of a new list). However, these operations combined +// have shown to be trivial in comparison to other timers architectures. -// Object maps containing linked lists of timers, keyed and sorted by their +// Object map containing linked lists of timers, keyed and sorted by their // duration in milliseconds. // -// The difference between these two objects is that the former contains timers -// that will keep the process open if they are the only thing left, while the -// latter will not. -// // - key = time in milliseconds // - value = linked list -const refedLists = Object.create(null); -const unrefedLists = Object.create(null); +const lists = Object.create(null); + +// This is a priority queue with a custom sorting function that first compares +// the expiry times of two lists and if they're the same then compares their +// individual IDs to determine which list was created first. +const queue = new PriorityQueue(compareTimersLists, setPosition); + +function compareTimersLists(a, b) { + const expiryDiff = a.expiry - b.expiry; + if (expiryDiff === 0) { + if (a.id < b.id) + return -1; + if (a.id > b.id) + return 1; + } + return expiryDiff; +} + +function setPosition(node, pos) { + node.priorityQueuePosition = pos; +} +let handle = null; +let nextExpiry = Infinity; + +let timerListId = Number.MIN_SAFE_INTEGER; +let refCount = 0; + +function incRefCount() { + if (refCount++ === 0) + handle.ref(); +} + +function decRefCount() { + if (--refCount === 0) + handle.unref(); +} + +function createHandle(refed) { + debug('initial run, creating TimerWrap handle'); + handle = new TimerWrap(); + if (!refed) + handle.unref(); +} // Schedule or re-schedule a timer. // The item must have been enroll()'d first. const active = exports.active = function(item) { - insert(item, false); + insert(item, true, TimerWrap.now()); }; // Internal APIs that need timeouts should use `_unrefActive()` instead of // `active()` so that they do not unnecessarily keep the process open. exports._unrefActive = function(item) { - insert(item, true); + insert(item, false, TimerWrap.now()); }; @@ -164,23 +197,27 @@ exports._unrefActive = function(item) { // Appends a timer onto the end of an existing timers list, or creates a new // TimerWrap backed list if one does not already exist for the specified timeout // duration. -function insert(item, unrefed, start) { +function insert(item, refed, start) { const msecs = item._idleTimeout; - if (msecs < 0 || msecs === undefined) return; - - if (typeof start === 'number') { - item._idleStart = start; - } else { - item._idleStart = TimerWrap.now(); - } + if (msecs < 0 || msecs === undefined) + return; - const lists = unrefed === true ? unrefedLists : refedLists; + item._idleStart = start; // Use an existing list if there is one, otherwise we need to make a new one. var list = lists[msecs]; if (list === undefined) { debug('no %d list was found in insert, creating a new one', msecs); - lists[msecs] = list = new TimersList(msecs, unrefed); + const expiry = start + msecs; + lists[msecs] = list = new TimersList(expiry, msecs); + queue.insert(list); + + if (nextExpiry > expiry) { + if (handle === null) + createHandle(refed); + handle.start(msecs); + nextExpiry = expiry; + } } if (!item[async_id_symbol] || item._destroyed) { @@ -188,32 +225,55 @@ function insert(item, unrefed, start) { initAsyncResource(item, 'Timeout'); } + if (refed === !item[kRefed]) { + if (refed) + incRefCount(); + else + decRefCount(); + } + item[kRefed] = refed; + L.append(list, item); - assert(!L.isEmpty(list)); // list is not empty } -function TimersList(msecs, unrefed) { +function TimersList(expiry, msecs) { this._idleNext = this; // Create the list with the linkedlist properties to this._idlePrev = this; // prevent any unnecessary hidden class changes. - this._unrefed = unrefed; + this.expiry = expiry; + this.id = timerListId++; this.msecs = msecs; - - const timer = this._timer = new TimerWrap(); - timer._list = this; - - if (unrefed === true) - timer.unref(); - timer.start(msecs); + this.priorityQueuePosition = null; } +const { _tickCallback: runNextTicks } = process; function processTimers(now) { - if (this.owner) - return unrefdHandle(this.owner, now); - return listOnTimeout(this, now); + debug('process timer lists %d', now); + nextExpiry = Infinity; + + let list, ran; + while (list = queue.peek()) { + if (list.expiry > now) + break; + if (ran) + runNextTicks(); + listOnTimeout(list, now); + ran = true; + } + + if (refCount > 0) + handle.ref(); + else + handle.unref(); + + if (list !== undefined) { + nextExpiry = list.expiry; + handle.start(Math.max(nextExpiry - TimerWrap.now(), 1)); + } + + return true; } -function listOnTimeout(handle, now) { - const list = handle._list; +function listOnTimeout(list, now) { const msecs = list.msecs; debug('timeout callback %d', msecs); @@ -226,78 +286,69 @@ function listOnTimeout(handle, now) { // Check if this loop iteration is too early for the next timer. // This happens if there are more timers scheduled for later in the list. if (diff < msecs) { - var timeRemaining = msecs - (TimerWrap.now() - timer._idleStart); - if (timeRemaining <= 0) { - timeRemaining = 1; - } - handle.start(timeRemaining); + list.expiry = timer._idleStart + msecs; + list.id = timerListId++; + queue.percolateDown(1); debug('%d list wait because diff is %d', msecs, diff); - return true; + return; } // The actual logic for when a timeout happens. - L.remove(timer); - assert(timer !== L.peek(list)); + + const asyncId = timer[async_id_symbol]; if (!timer._onTimeout) { - if (destroyHooksExist() && !timer._destroyed && - typeof timer[async_id_symbol] === 'number') { - emitDestroy(timer[async_id_symbol]); + if (timer[kRefed]) + refCount--; + timer[kRefed] = null; + + if (destroyHooksExist() && !timer._destroyed) { + emitDestroy(asyncId); timer._destroyed = true; } continue; } + emitBefore(asyncId, timer[trigger_async_id_symbol]); + tryOnTimeout(timer); + + emitAfter(asyncId); } // If `L.peek(list)` returned nothing, the list was either empty or we have // called all of the timer timeouts. - // As such, we can remove the list and clean up the TimerWrap C++ handle. + // As such, we can remove the list from the object map and the PriorityQueue. debug('%d list empty', msecs); - assert(L.isEmpty(list)); - - // Either refedLists[msecs] or unrefedLists[msecs] may have been removed and - // recreated since the reference to `list` was created. Make sure they're - // the same instance of the list before destroying. - if (list._unrefed === true && list === unrefedLists[msecs]) { - delete unrefedLists[msecs]; - } else if (list === refedLists[msecs]) { - delete refedLists[msecs]; - } - - // Do not close the underlying handle if its ownership has changed - // (e.g it was unrefed in its callback). - if (!handle.owner) - handle.close(); - return true; + // The current list may have been removed and recreated since the reference + // to `list` was created. Make sure they're the same instance of the list + // before destroying. + if (list === lists[msecs]) { + delete lists[msecs]; + queue.shift(); + } } // An optimization so that the try/finally only de-optimizes (since at least v8 // 4.7) what is in this smaller function. function tryOnTimeout(timer, start) { - timer._called = true; - const timerAsyncId = (typeof timer[async_id_symbol] === 'number') ? - timer[async_id_symbol] : null; - var threw = true; - if (timerAsyncId !== null) - emitBefore(timerAsyncId, timer[trigger_async_id_symbol]); if (start === undefined && timer._repeat) start = TimerWrap.now(); try { ontimeout(timer); - threw = false; } finally { - if (timerAsyncId !== null) { - if (!threw) - emitAfter(timerAsyncId); - if (timer._repeat) { - rearm(timer, start); - } else if (destroyHooksExist() && !timer._destroyed) { - emitDestroy(timerAsyncId); + if (timer._repeat) { + rearm(timer, start); + } else { + if (timer[kRefed]) + refCount--; + timer[kRefed] = null; + + if (destroyHooksExist() && !timer._destroyed) { + emitDestroy(timer[async_id_symbol]); timer._destroyed = true; } } @@ -305,43 +356,35 @@ function tryOnTimeout(timer, start) { } -// A convenience function for re-using TimerWrap handles more easily. -// -// This mostly exists to fix https://github.com/nodejs/node/issues/1264. -// Handles in libuv take at least one `uv_run` to be registered as unreferenced. -// Re-using an existing handle allows us to skip that, so that a second `uv_run` -// will return no active handles, even when running `setTimeout(fn).unref()`. -function reuse(item) { - L.remove(item); - - const list = refedLists[item._idleTimeout]; - // if empty - reuse the watcher - if (list !== undefined && L.isEmpty(list)) { - debug('reuse hit'); - list._timer.stop(); - delete refedLists[item._idleTimeout]; - return list._timer; - } - - return null; -} - - // Remove a timer. Cancels the timeout and resets the relevant timer properties. function unenroll(item) { // Fewer checks may be possible, but these cover everything. if (destroyHooksExist() && - typeof item[async_id_symbol] === 'number' && + item[async_id_symbol] !== undefined && !item._destroyed) { emitDestroy(item[async_id_symbol]); item._destroyed = true; } - const handle = reuse(item); - if (handle !== null) { - debug('unenroll: list empty'); - handle.close(); + L.remove(item); + + // We only delete refed lists because unrefed ones are incredibly likely + // to come from http and be recreated shortly after. + // TODO: Long-term this could instead be handled by creating an internal + // clearTimeout that makes it clear that the list should not be deleted. + // That function could then be used by http and other similar modules. + if (item[kRefed]) { + const list = lists[item._idleTimeout]; + if (list !== undefined && L.isEmpty(list)) { + debug('unenroll: list empty'); + queue.removeAt(list.priorityQueuePosition); + delete lists[list.msecs]; + } + + decRefCount(); } + item[kRefed] = null; + // if active is called later, then we want to make sure not to insert again item._idleTimeout = -1; } @@ -403,7 +446,7 @@ function setTimeout(callback, after, arg1, arg2, arg3) { break; } - const timeout = new Timeout(callback, after, args, false, false); + const timeout = new Timeout(callback, after, args, false); active(timeout); return timeout; @@ -411,7 +454,7 @@ function setTimeout(callback, after, arg1, arg2, arg3) { setTimeout[internalUtil.promisify.custom] = function(after, value) { const promise = createPromise(); - const timeout = new Timeout(promise, after, [value], false, false); + const timeout = new Timeout(promise, after, [value], false); active(timeout); return promise; @@ -430,36 +473,20 @@ function ontimeout(timer) { Reflect.apply(timer._onTimeout, timer, args); } -function rearm(timer, start = TimerWrap.now()) { +function rearm(timer, start) { // // Do not re-arm unenroll'd or closed timers. - if (timer._idleTimeout === -1) return; - - // 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; + if (timer._idleTimeout === -1) + return; - const duration = TimerWrap.now() - start; - if (duration >= timer._repeat) { - // If callback duration >= timer._repeat, - // add 1 ms to avoid blocking eventloop - insert(timer, false, start + duration - timer._repeat + 1); - } else { - insert(timer, false, start); - } - } + timer._idleTimeout = timer._repeat; + insert(timer, timer[kRefed], start); } const clearTimeout = exports.clearTimeout = function clearTimeout(timer) { if (timer && timer._onTimeout) { timer._onTimeout = null; - if (timer instanceof Timeout) { - timer.close(); // for after === 0 - } else { - unenroll(timer); - } + unenroll(timer); } }; @@ -490,7 +517,7 @@ exports.setInterval = function setInterval(callback, repeat, arg1, arg2, arg3) { break; } - const timeout = new Timeout(callback, repeat, args, true, false); + const timeout = new Timeout(callback, repeat, args, true); active(timeout); return timeout; @@ -503,73 +530,25 @@ exports.clearInterval = function clearInterval(timer) { clearTimeout(timer); }; -function unrefdHandle(timer, now) { - try { - // Don't attempt to call the callback if it is not a function. - if (typeof timer._onTimeout === 'function') { - tryOnTimeout(timer, now); - } - } finally { - // Make sure we clean up if the callback is no longer a function - // even if the timer is an interval. - if (!timer._repeat || typeof timer._onTimeout !== 'function') { - timer.close(); - } - } - - return true; -} - Timeout.prototype.unref = function() { - if (this._handle) { - this._handle.unref(); - } else if (typeof this._onTimeout === 'function') { - const now = TimerWrap.now(); - if (!this._idleStart) this._idleStart = now; - var delay = this._idleStart + this._idleTimeout - now; - if (delay < 0) delay = 0; - - // Prevent running cb again when unref() is called during the same cb - if (this._called && !this._repeat) { - unenroll(this); - return; - } - - const handle = reuse(this); - if (handle !== null) { - handle._list = undefined; - } - - this._handle = handle || new TimerWrap(); - this._handle.owner = this; - this._handle.start(delay); - this._handle.unref(); + if (this[kRefed]) { + this[kRefed] = false; + decRefCount(); } return this; }; Timeout.prototype.ref = function() { - if (this._handle) - this._handle.ref(); + if (this[kRefed] === false) { + this[kRefed] = true; + incRefCount(); + } return this; }; Timeout.prototype.close = function() { - this._onTimeout = null; - if (this._handle) { - if (destroyHooksExist() && - typeof this[async_id_symbol] === 'number' && - !this._destroyed) { - emitDestroy(this[async_id_symbol]); - this._destroyed = true; - } - - this._idleTimeout = -1; - this._handle.close(); - } else { - unenroll(this); - } + clearTimeout(this); return this; }; diff --git a/node.gyp b/node.gyp index bf768c1995cd20..038e5219bcc0fe 100644 --- a/node.gyp +++ b/node.gyp @@ -123,6 +123,7 @@ 'lib/internal/safe_globals.js', 'lib/internal/net.js', 'lib/internal/os.js', + 'lib/internal/priority_queue.js', 'lib/internal/process/esm_loader.js', 'lib/internal/process/methods.js', 'lib/internal/process/next_tick.js', diff --git a/src/timer_wrap.cc b/src/timer_wrap.cc index 88377a3e1b88cc..1a5a22c25ee037 100644 --- a/src/timer_wrap.cc +++ b/src/timer_wrap.cc @@ -62,7 +62,6 @@ class TimerWrap : public HandleWrap { env->SetProtoMethod(constructor, "hasRef", HandleWrap::HasRef); env->SetProtoMethod(constructor, "start", Start); - env->SetProtoMethod(constructor, "stop", Stop); target->Set(timerString, constructor->GetFunction()); @@ -125,32 +124,17 @@ class TimerWrap : public HandleWrap { args.GetReturnValue().Set(err); } - static void Stop(const FunctionCallbackInfo& args) { - TimerWrap* wrap; - ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); - - CHECK(HandleWrap::IsAlive(wrap)); - - int err = uv_timer_stop(&wrap->handle_); - args.GetReturnValue().Set(err); - } - static void OnTimeout(uv_timer_t* handle) { TimerWrap* wrap = static_cast(handle->data); Environment* env = wrap->env(); HandleScope handle_scope(env->isolate()); Context::Scope context_scope(env->context()); Local ret; - Local args[1]; + Local args[] = { env->GetNow() }; do { - args[0] = env->GetNow(); ret = wrap->MakeCallback(env->timers_callback_function(), 1, args) .ToLocalChecked(); - } while (ret->IsUndefined() && - !env->tick_info()->has_thrown() && - wrap->object()->Get(env->context(), - env->owner_string()).ToLocalChecked() - ->IsUndefined()); + } while (ret->IsUndefined() && !env->tick_info()->has_thrown()); } static void Now(const FunctionCallbackInfo& args) { diff --git a/test/async-hooks/test-timerwrap.setInterval.js b/test/async-hooks/test-timerwrap.setInterval.js index 058f2c9a9d14cc..eab19be1df10f0 100644 --- a/test/async-hooks/test-timerwrap.setInterval.js +++ b/test/async-hooks/test-timerwrap.setInterval.js @@ -51,6 +51,6 @@ function onexit() { hooks.disable(); hooks.sanityCheck('TIMERWRAP'); - checkInvocations(t, { init: 1, before: 3, after: 3, destroy: 1 }, + checkInvocations(t, { init: 1, before: 3, after: 3 }, 't: when process exits'); } diff --git a/test/async-hooks/test-timerwrap.setTimeout.js b/test/async-hooks/test-timerwrap.setTimeout.js index 9c873125d044b6..fcaf6bfc0aabc3 100644 --- a/test/async-hooks/test-timerwrap.setTimeout.js +++ b/test/async-hooks/test-timerwrap.setTimeout.js @@ -23,7 +23,6 @@ checkInvocations(t1, { init: 1 }, 't1: when first timer installed'); function ontimeout() { checkInvocations(t1, { init: 1, before: 1 }, 't1: when first timer fired'); - // install second timeout with same TIMEOUT to see timer wrap being reused setTimeout(onsecondTimeout, TIMEOUT); const as = hooks.activitiesOfTypes('TIMERWRAP'); assert.strictEqual(as.length, 1); @@ -31,35 +30,21 @@ function ontimeout() { 't1: when second timer installed'); } -// even though we install 3 timers we only have two timerwrap resources created -// as one is reused for the two timers with the same timeout -let t2; - function onsecondTimeout() { - let as = hooks.activitiesOfTypes('TIMERWRAP'); + const as = hooks.activitiesOfTypes('TIMERWRAP'); assert.strictEqual(as.length, 1); checkInvocations(t1, { init: 1, before: 2, after: 1 }, 't1: when second timer fired'); // install third timeout with different TIMEOUT setTimeout(onthirdTimeout, TIMEOUT + 1); - as = hooks.activitiesOfTypes('TIMERWRAP'); - assert.strictEqual(as.length, 2); - t2 = as[1]; - assert.strictEqual(t2.type, 'TIMERWRAP'); - assert.strictEqual(typeof t2.uid, 'number'); - assert.strictEqual(typeof t2.triggerAsyncId, 'number'); checkInvocations(t1, { init: 1, before: 2, after: 1 }, 't1: when third timer installed'); - checkInvocations(t2, { init: 1 }, - 't2: when third timer installed'); } function onthirdTimeout() { - checkInvocations(t1, { init: 1, before: 2, after: 2, destroy: 1 }, + checkInvocations(t1, { init: 1, before: 3, after: 2 }, 't1: when third timer fired'); - checkInvocations(t2, { init: 1, before: 1 }, - 't2: when third timer fired'); tick(2); } @@ -69,8 +54,6 @@ function onexit() { hooks.disable(); hooks.sanityCheck('TIMERWRAP'); - checkInvocations(t1, { init: 1, before: 2, after: 2, destroy: 1 }, + checkInvocations(t1, { init: 1, before: 3, after: 3 }, 't1: when process exits'); - checkInvocations(t2, { init: 1, before: 1, after: 1, destroy: 1 }, - 't2: when process exits'); } diff --git a/test/parallel/test-handle-wrap-isrefed.js b/test/parallel/test-handle-wrap-isrefed.js index e1301b57f6baeb..0b6afe83df2115 100644 --- a/test/parallel/test-handle-wrap-isrefed.js +++ b/test/parallel/test-handle-wrap-isrefed.js @@ -112,19 +112,23 @@ const dgram = require('dgram'); // timers { + const { Timer } = process.binding('timer_wrap'); + strictEqual(process._getActiveHandles().filter( + (handle) => (handle instanceof Timer)).length, 0); const timer = setTimeout(() => {}, 500); - timer.unref(); - strictEqual(Object.getPrototypeOf(timer._handle).hasOwnProperty('hasRef'), + const handles = process._getActiveHandles().filter( + (handle) => (handle instanceof Timer)); + strictEqual(handles.length, 1); + const handle = handles[0]; + strictEqual(Object.getPrototypeOf(handle).hasOwnProperty('hasRef'), true, 'timer_wrap: hasRef() missing'); - strictEqual(timer._handle.hasRef(), + strictEqual(handle.hasRef(), true); + timer.unref(); + strictEqual(handle.hasRef(), false, 'timer_wrap: unref() ineffective'); timer.ref(); - strictEqual(timer._handle.hasRef(), + strictEqual(handle.hasRef(), true, 'timer_wrap: ref() ineffective'); - timer._handle.close(common.mustCall(() => - strictEqual(timer._handle.hasRef(), - false, 'timer_wrap: not unrefed on close'))); } - // see also test/pseudo-tty/test-handle-wrap-isrefed-tty.js diff --git a/test/parallel/test-priority-queue.js b/test/parallel/test-priority-queue.js new file mode 100644 index 00000000000000..5b8f53a1766bb5 --- /dev/null +++ b/test/parallel/test-priority-queue.js @@ -0,0 +1,97 @@ +// Flags: --expose-internals +'use strict'; + +require('../common'); + +const assert = require('assert'); +const PriorityQueue = require('internal/priority_queue'); + +{ + // Checks that the queue is fundamentally correct. + const queue = new PriorityQueue(); + for (let i = 15; i > 0; i--) + queue.insert(i); + + for (let i = 1; i < 16; i++) { + assert.strictEqual(queue.peek(), i); + assert.strictEqual(queue.shift(), i); + } + + assert.strictEqual(queue.shift(), undefined); + + // Reverse the order. + for (let i = 1; i < 16; i++) + queue.insert(i); + + for (let i = 1; i < 16; i++) { + assert.strictEqual(queue.shift(), i); + } + + assert.strictEqual(queue.shift(), undefined); +} + +{ + // Checks that the queue is capable of resizing and fitting more elements. + const queue = new PriorityQueue(); + for (let i = 2048; i > 0; i--) + queue.insert(i); + + for (let i = 1; i < 2049; i++) { + assert.strictEqual(queue.shift(), i); + } + + assert.strictEqual(queue.shift(), undefined); +} + +{ + // Checks that remove works as expected. + const queue = new PriorityQueue(); + for (let i = 16; i > 0; i--) + queue.insert(i); + + const removed = [5, 10, 15]; + for (const id of removed) + assert(queue.remove(id)); + + assert(!queue.remove(100)); + assert(!queue.remove(-100)); + + for (let i = 1; i < 17; i++) { + if (removed.indexOf(i) < 0) + assert.strictEqual(queue.shift(), i); + } + + assert.strictEqual(queue.shift(), undefined); +} + +{ + // Make a max heap with a custom sort function. + const queue = new PriorityQueue((a, b) => b - a); + for (let i = 1; i < 17; i++) + queue.insert(i); + + for (let i = 16; i > 0; i--) { + assert.strictEqual(queue.shift(), i); + } + + assert.strictEqual(queue.shift(), undefined); +} + +{ + // Make a min heap that accepts objects as values, which necessitates + // a custom sorting function. In addition, add a setPosition function + // as 2nd param which provides a reference to the node in the heap + // and allows speedy deletions. + const queue = new PriorityQueue((a, b) => { + return a.value - b.value; + }, (node, pos) => (node.position = pos)); + for (let i = 1; i < 17; i++) + queue.insert({ value: i, position: null }); + + for (let i = 1; i < 17; i++) { + assert.strictEqual(queue.peek().value, i); + queue.removeAt(queue.peek().position); + } + + assert.strictEqual(queue.peek(), undefined); +} diff --git a/test/parallel/test-timers-next-tick.js b/test/parallel/test-timers-next-tick.js new file mode 100644 index 00000000000000..1db1d18c3a0d7a --- /dev/null +++ b/test/parallel/test-timers-next-tick.js @@ -0,0 +1,15 @@ +'use strict'; + +const common = require('../common'); + +// This test documents an internal implementation detail of the Timers: +// since timers of different durations are stored in separate lists, +// a nextTick queue will clear after each list of timers. While this +// behaviour is not documented it could be relied on by Node's users. + +setTimeout(common.mustCall(() => { + process.nextTick(() => { clearTimeout(t2); }); +}), 1); +const t2 = setTimeout(common.mustNotCall(), 2); + +common.busyLoop(5); diff --git a/test/parallel/test-timers-same-timeout-wrong-list-deleted.js b/test/parallel/test-timers-same-timeout-wrong-list-deleted.js index c66ba0a57efa11..3e04cb6c4cbc42 100644 --- a/test/parallel/test-timers-same-timeout-wrong-list-deleted.js +++ b/test/parallel/test-timers-same-timeout-wrong-list-deleted.js @@ -12,8 +12,6 @@ */ const common = require('../common'); -const assert = require('assert'); -const Timer = process.binding('timer_wrap').Timer; const TIMEOUT = common.platformTimeout(100); @@ -30,41 +28,9 @@ const handle1 = setTimeout(common.mustCall(function() { // erroneously deleted. If we are able to cancel the timer successfully, // the bug is fixed. clearTimeout(handle2); - - setImmediate(common.mustCall(function() { - setImmediate(common.mustCall(function() { - const activeTimers = getActiveTimers(); - - // Make sure our clearTimeout succeeded. One timer finished and - // the other was canceled, so none should be active. - assert.strictEqual(activeTimers.length, 0, 'Timers remain.'); - })); - })); }), 1); - // Make sure our timers got added to the list. - const activeTimers = getActiveTimers(); - const shortTimer = activeTimers.find(function(handle) { - return handle._list.msecs === 1; - }); - const longTimers = activeTimers.filter(function(handle) { - return handle._list.msecs === TIMEOUT; - }); - - // Make sure our clearTimeout succeeded. One timer finished and - // the other was canceled, so none should be active. - assert.strictEqual(activeTimers.length, 3, - 'There should be 3 timers in the list.'); - assert(shortTimer instanceof Timer, 'The shorter timer is not in the list.'); - assert.strictEqual(longTimers.length, 2, - 'Both longer timers should be in the list.'); - // When this callback completes, `listOnTimeout` should now look at the // correct list and refrain from removing the new TIMEOUT list which // contains the reference to the newer timer. }), TIMEOUT); - -function getActiveTimers() { - const activeHandles = process._getActiveHandles(); - return activeHandles.filter((handle) => handle instanceof Timer); -} diff --git a/test/parallel/test-timers-timeout-to-interval.js b/test/parallel/test-timers-timeout-to-interval.js index 6952f2231a7e18..6b83e2e09e34a5 100644 --- a/test/parallel/test-timers-timeout-to-interval.js +++ b/test/parallel/test-timers-timeout-to-interval.js @@ -8,5 +8,5 @@ const t = setTimeout(common.mustCall(() => { if (t._repeat) { clearInterval(t); } - t._repeat = true; + t._repeat = 1; }, 2), 1); diff --git a/test/parallel/test-timers-unref-leak.js b/test/parallel/test-timers-unref-leak.js deleted file mode 100644 index afecf7f15ce1b5..00000000000000 --- a/test/parallel/test-timers-unref-leak.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; -const common = require('../common'); - -const timeout = setTimeout(common.mustCall(), 10); -timeout.unref(); - -// Wrap `close` method to check if the handle was closed -const close = timeout._handle.close; -timeout._handle.close = common.mustCall(function() { - return close.apply(this, arguments); -}); - -// Just to keep process alive and let previous timer's handle die -setTimeout(() => {}, 50); diff --git a/test/parallel/test-timers-unref-reuse-no-exposed-list.js b/test/parallel/test-timers-unref-reuse-no-exposed-list.js deleted file mode 100644 index 33b2da2f9e214f..00000000000000 --- a/test/parallel/test-timers-unref-reuse-no-exposed-list.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -require('../common'); -const assert = require('assert'); - -const timer1 = setTimeout(() => {}, 1).unref(); -assert.strictEqual(timer1._handle._list, undefined, - 'timer1._handle._list should be undefined'); - -// Check that everything works even if the handle was not re-used. -setTimeout(() => {}, 1); -const timer2 = setTimeout(() => {}, 1).unref(); -assert.strictEqual(timer2._handle._list, undefined, - 'timer2._handle._list should be undefined');