Skip to content

Commit f8bb122

Browse files
committed
timers: improve linked list performance
By moving the linked list manipulations out of a separate module and into `lib/timers.js`, a significant performance increase is seen in the timer benchamrks. I am seeing consistent 50% improvement with a p-value of .0005 for the "breadth" benchmark with "thousands" set to 5. This is probably due to removing the overhead of calling an external function and possibly also removing some special handling that is not needed in some cases.
1 parent 105e628 commit f8bb122

File tree

2 files changed

+48
-18
lines changed

2 files changed

+48
-18
lines changed

lib/internal/linkedlist.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ exports.create = create;
1616

1717
// show the most idle item
1818
function peek(list) {
19-
if (list._idlePrev == list) return null;
19+
if (list._idlePrev === list) return null;
2020
return list._idlePrev;
2121
}
2222
exports.peek = peek;

lib/timers.js

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
'use strict';
22

33
const TimerWrap = process.binding('timer_wrap').Timer;
4-
const L = require('internal/linkedlist');
5-
const assert = require('assert');
64
const util = require('util');
75
const debug = util.debuglog('timer');
86
const kOnTimeout = TimerWrap.kOnTimeout | 0;
@@ -131,15 +129,28 @@ function insert(item, unrefed) {
131129
lists[msecs] = list = createTimersList(msecs, unrefed);
132130
}
133131

134-
L.append(list, item);
135-
assert(!L.isEmpty(list)); // list is not empty
132+
// put item at end of list
133+
if (item._idleNext) {
134+
item._idleNext._idlePrev = item._idlePrev;
135+
}
136+
if (item._idlePrev) {
137+
item._idlePrev._idleNext = item._idleNext;
138+
}
139+
140+
// items are linked with _idleNext -> (older) and _idlePrev -> (newer)
141+
// TODO: swap the linkage to match the intuitive older items at "prev"
142+
item._idleNext = list._idleNext;
143+
item._idlePrev = list;
144+
145+
// the list _idleNext points to tail (newest) and _idlePrev to head (oldest)
146+
list._idleNext._idlePrev = item;
147+
list._idleNext = item;
136148
}
137149

138150
function createTimersList(msecs, unrefed) {
139151
// Make a new linked list of timers, and create a TimerWrap to schedule
140152
// processing for the list.
141153
const list = new TimersList(msecs, unrefed);
142-
L.init(list);
143154
list._timer._list = list;
144155

145156
if (unrefed === true) list._timer.unref();
@@ -151,8 +162,8 @@ function createTimersList(msecs, unrefed) {
151162
}
152163

153164
function TimersList(msecs, unrefed) {
154-
this._idleNext = null; // Create the list with the linkedlist properties to
155-
this._idlePrev = null; // prevent any unnecessary hidden class changes.
165+
this._idleNext = this;
166+
this._idlePrev = this;
156167
this._timer = new TimerWrap();
157168
this._unrefed = unrefed;
158169
this.msecs = msecs;
@@ -168,7 +179,8 @@ function listOnTimeout() {
168179
debug('now: %d', now);
169180

170181
var diff, timer;
171-
while (timer = L.peek(list)) {
182+
while (list._idlePrev !== list) {
183+
timer = list._idlePrev;
172184
diff = now - timer._idleStart;
173185

174186
// Check if this loop iteration is too early for the next timer.
@@ -185,8 +197,17 @@ function listOnTimeout() {
185197

186198
// The actual logic for when a timeout happens.
187199

188-
L.remove(timer);
189-
assert(timer !== L.peek(list));
200+
// Remove the timer from the linked list
201+
if (timer._idleNext) {
202+
timer._idleNext._idlePrev = timer._idlePrev;
203+
}
204+
205+
if (timer._idlePrev) {
206+
timer._idlePrev._idleNext = timer._idleNext;
207+
}
208+
209+
timer._idleNext = null;
210+
timer._idlePrev = null;
190211

191212
if (!timer._onTimeout) continue;
192213

@@ -210,11 +231,9 @@ function listOnTimeout() {
210231
domain.exit();
211232
}
212233

213-
// If `L.peek(list)` returned nothing, the list was either empty or we have
214-
// called all of the timer timeouts.
215-
// As such, we can remove the list and clean up the TimerWrap C++ handle.
234+
// All of the timer timeouts, if any, have been called.
235+
// Remove the list and clean up the TimerWrap C++ handle.
216236
debug('%d list empty', msecs);
217-
assert(L.isEmpty(list));
218237
this.close();
219238

220239
// Either refedLists[msecs] or unrefedLists[msecs] may have been removed and
@@ -263,11 +282,21 @@ function listOnTimeoutNT(list) {
263282
// Re-using an existing handle allows us to skip that, so that a second `uv_run`
264283
// will return no active handles, even when running `setTimeout(fn).unref()`.
265284
function reuse(item) {
266-
L.remove(item);
285+
// Remove item from the linked list
286+
if (item._idleNext) {
287+
item._idleNext._idlePrev = item._idlePrev;
288+
}
289+
290+
if (item._idlePrev) {
291+
item._idlePrev._idleNext = item._idleNext;
292+
}
293+
294+
item._idleNext = null;
295+
item._idlePrev = null;
267296

268297
var list = refedLists[item._idleTimeout];
269298
// if empty - reuse the watcher
270-
if (list && L.isEmpty(list)) {
299+
if (list && list._idleNext === list) {
271300
debug('reuse hit');
272301
list._timer.stop();
273302
delete refedLists[item._idleTimeout];
@@ -313,7 +342,8 @@ exports.enroll = function(item, msecs) {
313342
}
314343

315344
item._idleTimeout = msecs;
316-
L.init(item);
345+
item._idleNext = item;
346+
item._idlePrev = item;
317347
};
318348

319349

0 commit comments

Comments
 (0)