Skip to content

Commit 67963c8

Browse files
committed
timers: greatly improve code comments
Describes the How and Why of the timers implementation, as well as adding comments in spots that should allow for an easier understanding about what is going on. The timers implementation is very efficient, at a cost. That cost is readable understandability, and this aims to improve that. PR-URL: #4007 Reviewed-By: Rod Vagg <[email protected]> Reviewed-By: Trevor Norris <[email protected]> Reviewed-By: Julien Gilli <[email protected]> Reviewed-By: Chris Dickinson <[email protected]>
1 parent 60f8c1a commit 67963c8

File tree

1 file changed

+105
-8
lines changed

1 file changed

+105
-8
lines changed

lib/timers.js

+105-8
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,90 @@ const kOnTimeout = TimerWrap.kOnTimeout | 0;
1010
// Timeout values > TIMEOUT_MAX are set to 1.
1111
const TIMEOUT_MAX = 2147483647; // 2^31-1
1212

13-
// IDLE TIMEOUTS
13+
14+
// HOW and WHY the timers implementation works the way it does.
15+
//
16+
// Timers are crucial to Node.js. Internally, any TCP I/O connection creates a
17+
// timer so that we can time out of connections. Additionally, many user
18+
// user libraries and applications also use timers. As such there may be a
19+
// significantly large amount of timeouts scheduled at any given time.
20+
// Therefore, it is very important that the timers implementation is performant
21+
// and efficient.
22+
//
23+
// Note: It is suggested you first read though the lib/internal/linkedlist.js
24+
// linked list implementation, since timers depend on it extensively. It can be
25+
// somewhat counter-intuitive at first, as it is not actually a class. Instead,
26+
// it is a set of helpers that operate on an existing object.
27+
//
28+
// In order to be as performant as possible, the architecture and data
29+
// structures are designed so that they are optimized to handle the following
30+
// use cases as efficiently as possible:
31+
32+
// - Adding a new timer. (insert)
33+
// - Removing an existing timer. (remove)
34+
// - Handling a timer timing out. (timeout)
35+
//
36+
// Whenever possible, the implementation tries to make the complexity of these
37+
// operations as close to constant-time as possible.
38+
// (So that performance is not impacted by the number of scheduled timers.)
39+
//
40+
// Object maps are kept which contain linked lists keyed by their duration in
41+
// milliseconds.
42+
// The linked lists within also have some meta-properties, one of which is a
43+
// TimerWrap C++ handle, which makes the call after the duration to process the
44+
// list it is attached to.
45+
//
46+
//
47+
// ╔════ > Object Map
48+
// ║
49+
// ╠══
50+
// ║ refedLists: { '40': { }, '320': { etc } } (keys of millisecond duration)
51+
// ╚══ ┌─────────┘
52+
// │
53+
// ╔══ │
54+
// ║ TimersList { _idleNext: { }, _idlePrev: (self), _timer: (TimerWrap) }
55+
// ║ ┌────────────────┘
56+
// ║ ╔══ │ ^
57+
// ║ ║ { _idleNext: { }, _idlePrev: { }, _onTimeout: (callback) }
58+
// ║ ║ ┌───────────┘
59+
// ║ ║ │ ^
60+
// ║ ║ { _idleNext: { etc }, _idlePrev: { }, _onTimeout: (callback) }
61+
// ╠══ ╠══
62+
// ║ ║
63+
// ║ ╚════ > Actual JavaScript timeouts
64+
// ║
65+
// ╚════ > Linked List
66+
//
67+
//
68+
// With this, virtually constant-time insertion (append), removal, and timeout
69+
// is possible in the JavaScript layer. Any one list of timers is able to be
70+
// sorted by just appending to it because all timers within share the same
71+
// duration. Therefore, any timer added later will always have been scheduled to
72+
// timeout later, thus only needing to be appended.
73+
// Removal from an object-property linked list is also virtually constant-time
74+
// as can be seen in the lib/internal/linkedlist.js implementation.
75+
// Timeouts only need to process any timers due to currently timeout, which will
76+
// always be at the beginning of the list for reasons stated above. Any timers
77+
// after the first one encountered that does not yet need to timeout will also
78+
// always be due to timeout at a later time.
79+
//
80+
// Less-than constant time operations are thus contained in two places:
81+
// TimerWrap's backing libuv timers implementation (a performant heap-based
82+
// queue), and the object map lookup of a specific list by the duration of
83+
// timers within (or creation of a new list).
84+
// However, these operations combined have shown to be trivial in comparison to
85+
// other alternative timers architectures.
86+
87+
1488
// Object maps containing linked lists of timers, keyed and sorted by their
1589
// duration in milliseconds.
1690
//
17-
// Because often many sockets will have the same idle timeout we will not
18-
// use one timeout watcher per item. It is too much overhead. Instead
19-
// we'll use a single watcher for all sockets with the same timeout value
20-
// and a linked list. This technique is described in the libev manual:
21-
// http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#Be_smart_about_timeouts
22-
91+
// The difference between these two objects is that the former contains timers
92+
// that will keep the process open if they are the only thing left, while the
93+
// latter will not.
94+
//
95+
// - key = time in milliseconds
96+
// - value = linked list
2397
const refedLists = {};
2498
const unrefedLists = {};
2599

@@ -38,6 +112,10 @@ exports._unrefActive = function(item) {
38112

39113

40114
// The underlying logic for scheduling or re-scheduling a timer.
115+
//
116+
// Appends a timer onto the end of an existing timers list, or creates a new
117+
// TimerWrap backed list if one does not already exist for the specified timeout
118+
// duration.
41119
function insert(item, unrefed) {
42120
const msecs = item._idleTimeout;
43121
if (msecs < 0 || msecs === undefined) return;
@@ -46,9 +124,12 @@ function insert(item, unrefed) {
46124

47125
const lists = unrefed === true ? unrefedLists : refedLists;
48126

127+
// Use an existing list if there is one, otherwise we need to make a new one.
49128
var list = lists[msecs];
50129
if (!list) {
51130
debug('no %d list was found in insert, creating a new one', msecs);
131+
// Make a new linked list of timers, and create a TimerWrap to schedule
132+
// processing for the list.
52133
list = new TimersList(msecs, unrefed);
53134
L.init(list);
54135
list._timer._list = list;
@@ -85,12 +166,15 @@ function listOnTimeout() {
85166
while (timer = L.peek(list)) {
86167
diff = now - timer._idleStart;
87168

169+
// Check if this loop iteration is too early for the next timer.
170+
// This happens if there are more timers scheduled for later in the list.
88171
if (diff < msecs) {
89172
this.start(msecs - diff, 0);
90173
debug('%d list wait because diff is %d', msecs, diff);
91174
return;
92175
}
93176

177+
// The actual logic for when a timeout happens.
94178

95179
L.remove(timer);
96180
assert(timer !== L.peek(list));
@@ -117,6 +201,9 @@ function listOnTimeout() {
117201
domain.exit();
118202
}
119203

204+
// If `L.peek(list)` returned nothing, the list was either empty or we have
205+
// called all of the timer timeouts.
206+
// As such, we can remove the list and clean up the TimerWrap C++ handle.
120207
debug('%d list empty', msecs);
121208
assert(L.isEmpty(list));
122209
this.close();
@@ -144,6 +231,7 @@ function tryOnTimeout(timer, list) {
144231
// when the timeout threw its exception.
145232
const domain = process.domain;
146233
process.domain = null;
234+
// If we threw, we need to process the rest of the list in nextTick.
147235
process.nextTick(listOnTimeoutNT, list);
148236
process.domain = domain;
149237
}
@@ -155,6 +243,12 @@ function listOnTimeoutNT(list) {
155243
}
156244

157245

246+
// A convenience function for re-using TimerWrap handles more easily.
247+
//
248+
// This mostly exists to fix https://github.com/nodejs/node/issues/1264.
249+
// Handles in libuv take at least one `uv_run` to be registered as unreferenced.
250+
// Re-using an existing handle allows us to skip that, so that a second `uv_run`
251+
// will return no active handles, even when running `setTimeout(fn).unref()`.
158252
function reuse(item) {
159253
L.remove(item);
160254

@@ -171,6 +265,7 @@ function reuse(item) {
171265
}
172266

173267

268+
// Remove a timer. Cancels the timeout and resets the relevant timer properties.
174269
const unenroll = exports.unenroll = function(item) {
175270
var handle = reuse(item);
176271
if (handle) {
@@ -182,7 +277,9 @@ const unenroll = exports.unenroll = function(item) {
182277
};
183278

184279

185-
// Does not start the time, just sets up the members needed.
280+
// Make a regular object able to act as a timer by setting some properties.
281+
// This function does not start the timer, see `active()`.
282+
// Using existing objects as timers slightly reduces object overhead.
186283
exports.enroll = function(item, msecs) {
187284
if (typeof msecs !== 'number') {
188285
throw new TypeError('"msecs" argument must be a number');

0 commit comments

Comments
 (0)