• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3// HOW and WHY the timers implementation works the way it does.
4//
5// Timers are crucial to Node.js. Internally, any TCP I/O connection creates a
6// timer so that we can time out of connections. Additionally, many user
7// libraries and applications also use timers. As such there may be a
8// significantly large amount of timeouts scheduled at any given time.
9// Therefore, it is very important that the timers implementation is performant
10// and efficient.
11//
12// Note: It is suggested you first read through the lib/internal/linkedlist.js
13// linked list implementation, since timers depend on it extensively. It can be
14// somewhat counter-intuitive at first, as it is not actually a class. Instead,
15// it is a set of helpers that operate on an existing object.
16//
17// In order to be as performant as possible, the architecture and data
18// structures are designed so that they are optimized to handle the following
19// use cases as efficiently as possible:
20
21// - Adding a new timer. (insert)
22// - Removing an existing timer. (remove)
23// - Handling a timer timing out. (timeout)
24//
25// Whenever possible, the implementation tries to make the complexity of these
26// operations as close to constant-time as possible.
27// (So that performance is not impacted by the number of scheduled timers.)
28//
29// Object maps are kept which contain linked lists keyed by their duration in
30// milliseconds.
31//
32/* eslint-disable node-core/non-ascii-character */
33//
34// ╔════ > Object Map
35// ║
36// ╠══
37// ║ lists: { '40': { }, '320': { etc } } (keys of millisecond duration)
38// ╚══          ┌────┘
39//              │
40// ╔══          │
41// ║ TimersList { _idleNext: { }, _idlePrev: (self) }
42// ║         ┌────────────────┘
43// ║    ╔══  │                              ^
44// ║    ║    { _idleNext: { },  _idlePrev: { }, _onTimeout: (callback) }
45// ║    ║      ┌───────────┘
46// ║    ║      │                                  ^
47// ║    ║      { _idleNext: { etc },  _idlePrev: { }, _onTimeout: (callback) }
48// ╠══  ╠══
49// ║    ║
50// ║    ╚════ >  Actual JavaScript timeouts
51// ║
52// ╚════ > Linked List
53//
54/* eslint-enable node-core/non-ascii-character */
55//
56// With this, virtually constant-time insertion (append), removal, and timeout
57// is possible in the JavaScript layer. Any one list of timers is able to be
58// sorted by just appending to it because all timers within share the same
59// duration. Therefore, any timer added later will always have been scheduled to
60// timeout later, thus only needing to be appended.
61// Removal from an object-property linked list is also virtually constant-time
62// as can be seen in the lib/internal/linkedlist.js implementation.
63// Timeouts only need to process any timers currently due to expire, which will
64// always be at the beginning of the list for reasons stated above. Any timers
65// after the first one encountered that does not yet need to timeout will also
66// always be due to timeout at a later time.
67//
68// Less-than constant time operations are thus contained in two places:
69// The PriorityQueue — an efficient binary heap implementation that does all
70// operations in worst-case O(log n) time — which manages the order of expiring
71// Timeout lists and the object map lookup of a specific list by the duration of
72// timers within (or creation of a new list). However, these operations combined
73// have shown to be trivial in comparison to other timers architectures.
74
75const {
76  MathMax,
77  MathTrunc,
78  NumberIsFinite,
79  NumberMIN_SAFE_INTEGER,
80  ObjectCreate,
81  ReflectApply,
82  Symbol,
83} = primordials;
84
85const {
86  scheduleTimer,
87  toggleTimerRef,
88  getLibuvNow,
89  immediateInfo,
90  timeoutInfo,
91  toggleImmediateRef,
92} = internalBinding('timers');
93
94const {
95  getDefaultTriggerAsyncId,
96  newAsyncId,
97  initHooksExist,
98  destroyHooksExist,
99  // The needed emit*() functions.
100  emitInit,
101  emitBefore,
102  emitAfter,
103  emitDestroy,
104} = require('internal/async_hooks');
105
106// Symbols for storing async id state.
107const async_id_symbol = Symbol('asyncId');
108const trigger_async_id_symbol = Symbol('triggerId');
109
110const kHasPrimitive = Symbol('kHasPrimitive');
111
112const {
113  ERR_OUT_OF_RANGE,
114} = require('internal/errors').codes;
115const {
116  validateFunction,
117  validateNumber,
118} = require('internal/validators');
119
120const L = require('internal/linkedlist');
121const PriorityQueue = require('internal/priority_queue');
122
123const { inspect } = require('internal/util/inspect');
124let debug = require('internal/util/debuglog').debuglog('timer', (fn) => {
125  debug = fn;
126});
127
128// *Must* match Environment::ImmediateInfo::Fields in src/env.h.
129const kCount = 0;
130const kRefCount = 1;
131const kHasOutstanding = 2;
132
133// Timeout values > TIMEOUT_MAX are set to 1.
134const TIMEOUT_MAX = 2 ** 31 - 1;
135
136let timerListId = NumberMIN_SAFE_INTEGER;
137
138const kRefed = Symbol('refed');
139
140let nextExpiry = Infinity;
141// timeoutInfo is an Int32Array that contains the reference count of Timeout
142// objects at index 0. This is a TypedArray so that GetActiveResourcesInfo() in
143// `src/node_process_methods.cc` is able to access this value without crossing
144// the JS-C++ boundary, which is slow at the time of writing.
145timeoutInfo[0] = 0;
146
147// This is a priority queue with a custom sorting function that first compares
148// the expiry times of two lists and if they're the same then compares their
149// individual IDs to determine which list was created first.
150const timerListQueue = new PriorityQueue(compareTimersLists, setPosition);
151
152// Object map containing linked lists of timers, keyed and sorted by their
153// duration in milliseconds.
154//
155// - key = time in milliseconds
156// - value = linked list
157const timerListMap = ObjectCreate(null);
158
159function initAsyncResource(resource, type) {
160  const asyncId = resource[async_id_symbol] = newAsyncId();
161  const triggerAsyncId =
162    resource[trigger_async_id_symbol] = getDefaultTriggerAsyncId();
163  if (initHooksExist())
164    emitInit(asyncId, type, triggerAsyncId, resource);
165}
166class Timeout {
167  // Timer constructor function.
168  // The entire prototype is defined in lib/timers.js
169  constructor(callback, after, args, isRepeat, isRefed) {
170    after *= 1; // Coalesce to number or NaN
171    if (!(after >= 1 && after <= TIMEOUT_MAX)) {
172      if (after > TIMEOUT_MAX) {
173        process.emitWarning(`${after} does not fit into` +
174                            ' a 32-bit signed integer.' +
175                            '\nTimeout duration was set to 1.',
176                            'TimeoutOverflowWarning');
177      }
178      after = 1; // Schedule on next tick, follows browser behavior
179    }
180
181    this._idleTimeout = after;
182    this._idlePrev = this;
183    this._idleNext = this;
184    this._idleStart = null;
185    // This must be set to null first to avoid function tracking
186    // on the hidden class, revisit in V8 versions after 6.2
187    this._onTimeout = null;
188    this._onTimeout = callback;
189    this._timerArgs = args;
190    this._repeat = isRepeat ? after : null;
191    this._destroyed = false;
192
193    if (isRefed)
194      incRefCount();
195    this[kRefed] = isRefed;
196    this[kHasPrimitive] = false;
197
198    initAsyncResource(this, 'Timeout');
199  }
200
201  // Make sure the linked list only shows the minimal necessary information.
202  [inspect.custom](_, options) {
203    return inspect(this, {
204      ...options,
205      // Only inspect one level.
206      depth: 0,
207      // It should not recurse.
208      customInspect: false,
209    });
210  }
211
212  refresh() {
213    if (this[kRefed])
214      active(this);
215    else
216      unrefActive(this);
217
218    return this;
219  }
220
221  unref() {
222    if (this[kRefed]) {
223      this[kRefed] = false;
224      if (!this._destroyed)
225        decRefCount();
226    }
227    return this;
228  }
229
230  ref() {
231    if (!this[kRefed]) {
232      this[kRefed] = true;
233      if (!this._destroyed)
234        incRefCount();
235    }
236    return this;
237  }
238
239  hasRef() {
240    return this[kRefed];
241  }
242}
243
244class TimersList {
245  constructor(expiry, msecs) {
246    this._idleNext = this; // Create the list with the linkedlist properties to
247    this._idlePrev = this; // Prevent any unnecessary hidden class changes.
248    this.expiry = expiry;
249    this.id = timerListId++;
250    this.msecs = msecs;
251    this.priorityQueuePosition = null;
252  }
253
254  // Make sure the linked list only shows the minimal necessary information.
255  [inspect.custom](_, options) {
256    return inspect(this, {
257      ...options,
258      // Only inspect one level.
259      depth: 0,
260      // It should not recurse.
261      customInspect: false,
262    });
263  }
264}
265
266// A linked list for storing `setImmediate()` requests
267class ImmediateList {
268  constructor() {
269    this.head = null;
270    this.tail = null;
271  }
272
273  // Appends an item to the end of the linked list, adjusting the current tail's
274  // next pointer and the item's previous pointer where applicable
275  append(item) {
276    if (this.tail !== null) {
277      this.tail._idleNext = item;
278      item._idlePrev = this.tail;
279    } else {
280      this.head = item;
281    }
282    this.tail = item;
283  }
284
285  // Removes an item from the linked list, adjusting the pointers of adjacent
286  // items and the linked list's head or tail pointers as necessary
287  remove(item) {
288    if (item._idleNext) {
289      item._idleNext._idlePrev = item._idlePrev;
290    }
291
292    if (item._idlePrev) {
293      item._idlePrev._idleNext = item._idleNext;
294    }
295
296    if (item === this.head)
297      this.head = item._idleNext;
298    if (item === this.tail)
299      this.tail = item._idlePrev;
300
301    item._idleNext = null;
302    item._idlePrev = null;
303  }
304}
305
306// Create a single linked list instance only once at startup
307const immediateQueue = new ImmediateList();
308
309function incRefCount() {
310  if (timeoutInfo[0]++ === 0)
311    toggleTimerRef(true);
312}
313
314function decRefCount() {
315  if (--timeoutInfo[0] === 0)
316    toggleTimerRef(false);
317}
318
319// Schedule or re-schedule a timer.
320// The item must have been enroll()'d first.
321function active(item) {
322  insertGuarded(item, true);
323}
324
325// Internal APIs that need timeouts should use `unrefActive()` instead of
326// `active()` so that they do not unnecessarily keep the process open.
327function unrefActive(item) {
328  insertGuarded(item, false);
329}
330
331// The underlying logic for scheduling or re-scheduling a timer.
332//
333// Appends a timer onto the end of an existing timers list, or creates a new
334// list if one does not already exist for the specified timeout duration.
335function insertGuarded(item, refed, start) {
336  const msecs = item._idleTimeout;
337  if (msecs < 0 || msecs === undefined)
338    return;
339
340  insert(item, msecs, start);
341
342  const isDestroyed = item._destroyed;
343  if (isDestroyed || !item[async_id_symbol]) {
344    item._destroyed = false;
345    initAsyncResource(item, 'Timeout');
346  }
347
348  if (isDestroyed) {
349    if (refed)
350      incRefCount();
351  } else if (refed === !item[kRefed]) {
352    if (refed)
353      incRefCount();
354    else
355      decRefCount();
356  }
357  item[kRefed] = refed;
358}
359
360function insert(item, msecs, start = getLibuvNow()) {
361  // Truncate so that accuracy of sub-millisecond timers is not assumed.
362  msecs = MathTrunc(msecs);
363  item._idleStart = start;
364
365  // Use an existing list if there is one, otherwise we need to make a new one.
366  let list = timerListMap[msecs];
367  if (list === undefined) {
368    debug('no %d list was found in insert, creating a new one', msecs);
369    const expiry = start + msecs;
370    timerListMap[msecs] = list = new TimersList(expiry, msecs);
371    timerListQueue.insert(list);
372
373    if (nextExpiry > expiry) {
374      scheduleTimer(msecs);
375      nextExpiry = expiry;
376    }
377  }
378
379  L.append(list, item);
380}
381
382function setUnrefTimeout(callback, after) {
383  // Type checking identical to setTimeout()
384  validateFunction(callback, 'callback');
385
386  const timer = new Timeout(callback, after, undefined, false, false);
387  insert(timer, timer._idleTimeout);
388
389  return timer;
390}
391
392// Type checking used by timers.enroll() and Socket#setTimeout()
393function getTimerDuration(msecs, name) {
394  validateNumber(msecs, name);
395  if (msecs < 0 || !NumberIsFinite(msecs)) {
396    throw new ERR_OUT_OF_RANGE(name, 'a non-negative finite number', msecs);
397  }
398
399  // Ensure that msecs fits into signed int32
400  if (msecs > TIMEOUT_MAX) {
401    process.emitWarning(`${msecs} does not fit into a 32-bit signed integer.` +
402                        `\nTimer duration was truncated to ${TIMEOUT_MAX}.`,
403                        'TimeoutOverflowWarning');
404    return TIMEOUT_MAX;
405  }
406
407  return msecs;
408}
409
410function compareTimersLists(a, b) {
411  const expiryDiff = a.expiry - b.expiry;
412  if (expiryDiff === 0) {
413    if (a.id < b.id)
414      return -1;
415    if (a.id > b.id)
416      return 1;
417  }
418  return expiryDiff;
419}
420
421function setPosition(node, pos) {
422  node.priorityQueuePosition = pos;
423}
424
425function getTimerCallbacks(runNextTicks) {
426  // If an uncaught exception was thrown during execution of immediateQueue,
427  // this queue will store all remaining Immediates that need to run upon
428  // resolution of all error handling (if process is still alive).
429  const outstandingQueue = new ImmediateList();
430
431  function processImmediate() {
432    const queue = outstandingQueue.head !== null ?
433      outstandingQueue : immediateQueue;
434    let immediate = queue.head;
435
436    // Clear the linked list early in case new `setImmediate()`
437    // calls occur while immediate callbacks are executed
438    if (queue !== outstandingQueue) {
439      queue.head = queue.tail = null;
440      immediateInfo[kHasOutstanding] = 1;
441    }
442
443    let prevImmediate;
444    let ranAtLeastOneImmediate = false;
445    while (immediate !== null) {
446      if (ranAtLeastOneImmediate)
447        runNextTicks();
448      else
449        ranAtLeastOneImmediate = true;
450
451      // It's possible for this current Immediate to be cleared while executing
452      // the next tick queue above, which means we need to use the previous
453      // Immediate's _idleNext which is guaranteed to not have been cleared.
454      if (immediate._destroyed) {
455        outstandingQueue.head = immediate = prevImmediate._idleNext;
456        continue;
457      }
458
459      // TODO(RaisinTen): Destroy and unref the Immediate after _onImmediate()
460      // gets executed, just like how Timeouts work.
461      immediate._destroyed = true;
462
463      immediateInfo[kCount]--;
464      if (immediate[kRefed])
465        immediateInfo[kRefCount]--;
466      immediate[kRefed] = null;
467
468      prevImmediate = immediate;
469
470      const asyncId = immediate[async_id_symbol];
471      emitBefore(asyncId, immediate[trigger_async_id_symbol], immediate);
472
473      try {
474        const argv = immediate._argv;
475        if (!argv)
476          immediate._onImmediate();
477        else
478          immediate._onImmediate(...argv);
479      } finally {
480        immediate._onImmediate = null;
481
482        if (destroyHooksExist())
483          emitDestroy(asyncId);
484
485        outstandingQueue.head = immediate = immediate._idleNext;
486      }
487
488      emitAfter(asyncId);
489    }
490
491    if (queue === outstandingQueue)
492      outstandingQueue.head = null;
493    immediateInfo[kHasOutstanding] = 0;
494  }
495
496
497  function processTimers(now) {
498    debug('process timer lists %d', now);
499    nextExpiry = Infinity;
500
501    let list;
502    let ranAtLeastOneList = false;
503    while ((list = timerListQueue.peek()) != null) {
504      if (list.expiry > now) {
505        nextExpiry = list.expiry;
506        return timeoutInfo[0] > 0 ? nextExpiry : -nextExpiry;
507      }
508      if (ranAtLeastOneList)
509        runNextTicks();
510      else
511        ranAtLeastOneList = true;
512      listOnTimeout(list, now);
513    }
514    return 0;
515  }
516
517  function listOnTimeout(list, now) {
518    const msecs = list.msecs;
519
520    debug('timeout callback %d', msecs);
521
522    let ranAtLeastOneTimer = false;
523    let timer;
524    while ((timer = L.peek(list)) != null) {
525      const diff = now - timer._idleStart;
526
527      // Check if this loop iteration is too early for the next timer.
528      // This happens if there are more timers scheduled for later in the list.
529      if (diff < msecs) {
530        list.expiry = MathMax(timer._idleStart + msecs, now + 1);
531        list.id = timerListId++;
532        timerListQueue.percolateDown(1);
533        debug('%d list wait because diff is %d', msecs, diff);
534        return;
535      }
536
537      if (ranAtLeastOneTimer)
538        runNextTicks();
539      else
540        ranAtLeastOneTimer = true;
541
542      // The actual logic for when a timeout happens.
543      L.remove(timer);
544
545      const asyncId = timer[async_id_symbol];
546
547      if (!timer._onTimeout) {
548        if (!timer._destroyed) {
549          timer._destroyed = true;
550
551          if (timer[kRefed])
552            timeoutInfo[0]--;
553
554          if (destroyHooksExist())
555            emitDestroy(asyncId);
556        }
557        continue;
558      }
559
560      emitBefore(asyncId, timer[trigger_async_id_symbol], timer);
561
562      let start;
563      if (timer._repeat)
564        start = getLibuvNow();
565
566      try {
567        const args = timer._timerArgs;
568        if (args === undefined)
569          timer._onTimeout();
570        else
571          ReflectApply(timer._onTimeout, timer, args);
572      } finally {
573        if (timer._repeat && timer._idleTimeout !== -1) {
574          timer._idleTimeout = timer._repeat;
575          insert(timer, timer._idleTimeout, start);
576        } else if (!timer._idleNext && !timer._idlePrev && !timer._destroyed) {
577          timer._destroyed = true;
578
579          if (timer[kRefed])
580            timeoutInfo[0]--;
581
582          if (destroyHooksExist())
583            emitDestroy(asyncId);
584        }
585      }
586
587      emitAfter(asyncId);
588    }
589
590    // If `L.peek(list)` returned nothing, the list was either empty or we have
591    // called all of the timer timeouts.
592    // As such, we can remove the list from the object map and
593    // the PriorityQueue.
594    debug('%d list empty', msecs);
595
596    // The current list may have been removed and recreated since the reference
597    // to `list` was created. Make sure they're the same instance of the list
598    // before destroying.
599    if (list === timerListMap[msecs]) {
600      delete timerListMap[msecs];
601      timerListQueue.shift();
602    }
603  }
604
605  return {
606    processImmediate,
607    processTimers,
608  };
609}
610
611class Immediate {
612  constructor(callback, args) {
613    this._idleNext = null;
614    this._idlePrev = null;
615    this._onImmediate = callback;
616    this._argv = args;
617    this._destroyed = false;
618    this[kRefed] = false;
619
620    initAsyncResource(this, 'Immediate');
621
622    this.ref();
623    immediateInfo[kCount]++;
624
625    immediateQueue.append(this);
626  }
627
628  ref() {
629    if (this[kRefed] === false) {
630      this[kRefed] = true;
631      if (immediateInfo[kRefCount]++ === 0)
632        toggleImmediateRef(true);
633    }
634    return this;
635  }
636
637  unref() {
638    if (this[kRefed] === true) {
639      this[kRefed] = false;
640      if (--immediateInfo[kRefCount] === 0)
641        toggleImmediateRef(false);
642    }
643    return this;
644  }
645
646  hasRef() {
647    return !!this[kRefed];
648  }
649}
650
651module.exports = {
652  TIMEOUT_MAX,
653  kTimeout: Symbol('timeout'), // For hiding Timeouts on other internals.
654  async_id_symbol,
655  trigger_async_id_symbol,
656  Timeout,
657  Immediate,
658  kRefed,
659  kHasPrimitive,
660  initAsyncResource,
661  setUnrefTimeout,
662  getTimerDuration,
663  immediateQueue,
664  getTimerCallbacks,
665  immediateInfoFields: {
666    kCount,
667    kRefCount,
668    kHasOutstanding,
669  },
670  active,
671  unrefActive,
672  insert,
673  timerListMap,
674  timerListQueue,
675  decRefCount,
676  incRefCount,
677};
678