• 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  toggleImmediateRef
91} = internalBinding('timers');
92
93const {
94  getDefaultTriggerAsyncId,
95  newAsyncId,
96  initHooksExist,
97  destroyHooksExist,
98  // The needed emit*() functions.
99  emitInit,
100  emitBefore,
101  emitAfter,
102  emitDestroy,
103} = require('internal/async_hooks');
104
105// Symbols for storing async id state.
106const async_id_symbol = Symbol('asyncId');
107const trigger_async_id_symbol = Symbol('triggerId');
108
109const kHasPrimitive = Symbol('kHasPrimitive');
110
111const {
112  ERR_INVALID_CALLBACK,
113  ERR_OUT_OF_RANGE
114} = require('internal/errors').codes;
115const { validateNumber } = require('internal/validators');
116
117const L = require('internal/linkedlist');
118const PriorityQueue = require('internal/priority_queue');
119
120const { inspect } = require('internal/util/inspect');
121let debug = require('internal/util/debuglog').debuglog('timer', (fn) => {
122  debug = fn;
123});
124
125// *Must* match Environment::ImmediateInfo::Fields in src/env.h.
126const kCount = 0;
127const kRefCount = 1;
128const kHasOutstanding = 2;
129
130// Timeout values > TIMEOUT_MAX are set to 1.
131const TIMEOUT_MAX = 2 ** 31 - 1;
132
133let timerListId = NumberMIN_SAFE_INTEGER;
134
135const kRefed = Symbol('refed');
136
137// Create a single linked list instance only once at startup
138const immediateQueue = new ImmediateList();
139
140let nextExpiry = Infinity;
141let refCount = 0;
142
143// This is a priority queue with a custom sorting function that first compares
144// the expiry times of two lists and if they're the same then compares their
145// individual IDs to determine which list was created first.
146const timerListQueue = new PriorityQueue(compareTimersLists, setPosition);
147
148// Object map containing linked lists of timers, keyed and sorted by their
149// duration in milliseconds.
150//
151// - key = time in milliseconds
152// - value = linked list
153const timerListMap = ObjectCreate(null);
154
155function initAsyncResource(resource, type) {
156  const asyncId = resource[async_id_symbol] = newAsyncId();
157  const triggerAsyncId =
158    resource[trigger_async_id_symbol] = getDefaultTriggerAsyncId();
159  if (initHooksExist())
160    emitInit(asyncId, type, triggerAsyncId, resource);
161}
162
163// Timer constructor function.
164// The entire prototype is defined in lib/timers.js
165function Timeout(callback, after, args, isRepeat, isRefed) {
166  after *= 1; // Coalesce to number or NaN
167  if (!(after >= 1 && after <= TIMEOUT_MAX)) {
168    if (after > TIMEOUT_MAX) {
169      process.emitWarning(`${after} does not fit into` +
170                          ' a 32-bit signed integer.' +
171                          '\nTimeout duration was set to 1.',
172                          'TimeoutOverflowWarning');
173    }
174    after = 1; // Schedule on next tick, follows browser behavior
175  }
176
177  this._idleTimeout = after;
178  this._idlePrev = this;
179  this._idleNext = this;
180  this._idleStart = null;
181  // This must be set to null first to avoid function tracking
182  // on the hidden class, revisit in V8 versions after 6.2
183  this._onTimeout = null;
184  this._onTimeout = callback;
185  this._timerArgs = args;
186  this._repeat = isRepeat ? after : null;
187  this._destroyed = false;
188
189  if (isRefed)
190    incRefCount();
191  this[kRefed] = isRefed;
192  this[kHasPrimitive] = false;
193
194  initAsyncResource(this, 'Timeout');
195}
196
197// Make sure the linked list only shows the minimal necessary information.
198Timeout.prototype[inspect.custom] = function(_, options) {
199  return inspect(this, {
200    ...options,
201    // Only inspect one level.
202    depth: 0,
203    // It should not recurse.
204    customInspect: false
205  });
206};
207
208Timeout.prototype.refresh = function() {
209  if (this[kRefed])
210    active(this);
211  else
212    unrefActive(this);
213
214  return this;
215};
216
217Timeout.prototype.unref = function() {
218  if (this[kRefed]) {
219    this[kRefed] = false;
220    if (!this._destroyed)
221      decRefCount();
222  }
223  return this;
224};
225
226Timeout.prototype.ref = function() {
227  if (!this[kRefed]) {
228    this[kRefed] = true;
229    if (!this._destroyed)
230      incRefCount();
231  }
232  return this;
233};
234
235Timeout.prototype.hasRef = function() {
236  return this[kRefed];
237};
238
239function TimersList(expiry, msecs) {
240  this._idleNext = this; // Create the list with the linkedlist properties to
241  this._idlePrev = this; // Prevent any unnecessary hidden class changes.
242  this.expiry = expiry;
243  this.id = timerListId++;
244  this.msecs = msecs;
245  this.priorityQueuePosition = null;
246}
247
248// Make sure the linked list only shows the minimal necessary information.
249TimersList.prototype[inspect.custom] = function(_, options) {
250  return inspect(this, {
251    ...options,
252    // Only inspect one level.
253    depth: 0,
254    // It should not recurse.
255    customInspect: false
256  });
257};
258
259// A linked list for storing `setImmediate()` requests
260function ImmediateList() {
261  this.head = null;
262  this.tail = null;
263}
264
265// Appends an item to the end of the linked list, adjusting the current tail's
266// next pointer and the item's previous pointer where applicable
267ImmediateList.prototype.append = function(item) {
268  if (this.tail !== null) {
269    this.tail._idleNext = item;
270    item._idlePrev = this.tail;
271  } else {
272    this.head = item;
273  }
274  this.tail = item;
275};
276
277// Removes an item from the linked list, adjusting the pointers of adjacent
278// items and the linked list's head or tail pointers as necessary
279ImmediateList.prototype.remove = function(item) {
280  if (item._idleNext) {
281    item._idleNext._idlePrev = item._idlePrev;
282  }
283
284  if (item._idlePrev) {
285    item._idlePrev._idleNext = item._idleNext;
286  }
287
288  if (item === this.head)
289    this.head = item._idleNext;
290  if (item === this.tail)
291    this.tail = item._idlePrev;
292
293  item._idleNext = null;
294  item._idlePrev = null;
295};
296
297function incRefCount() {
298  if (refCount++ === 0)
299    toggleTimerRef(true);
300}
301
302function decRefCount() {
303  if (--refCount === 0)
304    toggleTimerRef(false);
305}
306
307// Schedule or re-schedule a timer.
308// The item must have been enroll()'d first.
309function active(item) {
310  insertGuarded(item, true);
311}
312
313// Internal APIs that need timeouts should use `unrefActive()` instead of
314// `active()` so that they do not unnecessarily keep the process open.
315function unrefActive(item) {
316  insertGuarded(item, false);
317}
318
319// The underlying logic for scheduling or re-scheduling a timer.
320//
321// Appends a timer onto the end of an existing timers list, or creates a new
322// list if one does not already exist for the specified timeout duration.
323function insertGuarded(item, refed, start) {
324  const msecs = item._idleTimeout;
325  if (msecs < 0 || msecs === undefined)
326    return;
327
328  insert(item, msecs, start);
329
330  const isDestroyed = item._destroyed;
331  if (isDestroyed || !item[async_id_symbol]) {
332    item._destroyed = false;
333    initAsyncResource(item, 'Timeout');
334  }
335
336  if (isDestroyed) {
337    if (refed)
338      incRefCount();
339  } else if (refed === !item[kRefed]) {
340    if (refed)
341      incRefCount();
342    else
343      decRefCount();
344  }
345  item[kRefed] = refed;
346}
347
348function insert(item, msecs, start = getLibuvNow()) {
349  // Truncate so that accuracy of sub-millisecond timers is not assumed.
350  msecs = MathTrunc(msecs);
351  item._idleStart = start;
352
353  // Use an existing list if there is one, otherwise we need to make a new one.
354  let list = timerListMap[msecs];
355  if (list === undefined) {
356    debug('no %d list was found in insert, creating a new one', msecs);
357    const expiry = start + msecs;
358    timerListMap[msecs] = list = new TimersList(expiry, msecs);
359    timerListQueue.insert(list);
360
361    if (nextExpiry > expiry) {
362      scheduleTimer(msecs);
363      nextExpiry = expiry;
364    }
365  }
366
367  L.append(list, item);
368}
369
370function setUnrefTimeout(callback, after) {
371  // Type checking identical to setTimeout()
372  if (typeof callback !== 'function') {
373    throw new ERR_INVALID_CALLBACK(callback);
374  }
375
376  const timer = new Timeout(callback, after, undefined, false, false);
377  insert(timer, timer._idleTimeout);
378
379  return timer;
380}
381
382// Type checking used by timers.enroll() and Socket#setTimeout()
383function getTimerDuration(msecs, name) {
384  validateNumber(msecs, name);
385  if (msecs < 0 || !NumberIsFinite(msecs)) {
386    throw new ERR_OUT_OF_RANGE(name, 'a non-negative finite number', msecs);
387  }
388
389  // Ensure that msecs fits into signed int32
390  if (msecs > TIMEOUT_MAX) {
391    process.emitWarning(`${msecs} does not fit into a 32-bit signed integer.` +
392                        `\nTimer duration was truncated to ${TIMEOUT_MAX}.`,
393                        'TimeoutOverflowWarning');
394    return TIMEOUT_MAX;
395  }
396
397  return msecs;
398}
399
400function compareTimersLists(a, b) {
401  const expiryDiff = a.expiry - b.expiry;
402  if (expiryDiff === 0) {
403    if (a.id < b.id)
404      return -1;
405    if (a.id > b.id)
406      return 1;
407  }
408  return expiryDiff;
409}
410
411function setPosition(node, pos) {
412  node.priorityQueuePosition = pos;
413}
414
415function getTimerCallbacks(runNextTicks) {
416  // If an uncaught exception was thrown during execution of immediateQueue,
417  // this queue will store all remaining Immediates that need to run upon
418  // resolution of all error handling (if process is still alive).
419  const outstandingQueue = new ImmediateList();
420
421  function processImmediate() {
422    const queue = outstandingQueue.head !== null ?
423      outstandingQueue : immediateQueue;
424    let immediate = queue.head;
425
426    // Clear the linked list early in case new `setImmediate()`
427    // calls occur while immediate callbacks are executed
428    if (queue !== outstandingQueue) {
429      queue.head = queue.tail = null;
430      immediateInfo[kHasOutstanding] = 1;
431    }
432
433    let prevImmediate;
434    let ranAtLeastOneImmediate = false;
435    while (immediate !== null) {
436      if (ranAtLeastOneImmediate)
437        runNextTicks();
438      else
439        ranAtLeastOneImmediate = true;
440
441      // It's possible for this current Immediate to be cleared while executing
442      // the next tick queue above, which means we need to use the previous
443      // Immediate's _idleNext which is guaranteed to not have been cleared.
444      if (immediate._destroyed) {
445        outstandingQueue.head = immediate = prevImmediate._idleNext;
446        continue;
447      }
448
449      immediate._destroyed = true;
450
451      immediateInfo[kCount]--;
452      if (immediate[kRefed])
453        immediateInfo[kRefCount]--;
454      immediate[kRefed] = null;
455
456      prevImmediate = immediate;
457
458      const asyncId = immediate[async_id_symbol];
459      emitBefore(asyncId, immediate[trigger_async_id_symbol], immediate);
460
461      try {
462        const argv = immediate._argv;
463        if (!argv)
464          immediate._onImmediate();
465        else
466          immediate._onImmediate(...argv);
467      } finally {
468        immediate._onImmediate = null;
469
470        if (destroyHooksExist())
471          emitDestroy(asyncId);
472
473        outstandingQueue.head = immediate = immediate._idleNext;
474      }
475
476      emitAfter(asyncId);
477    }
478
479    if (queue === outstandingQueue)
480      outstandingQueue.head = null;
481    immediateInfo[kHasOutstanding] = 0;
482  }
483
484
485  function processTimers(now) {
486    debug('process timer lists %d', now);
487    nextExpiry = Infinity;
488
489    let list;
490    let ranAtLeastOneList = false;
491    while (list = timerListQueue.peek()) {
492      if (list.expiry > now) {
493        nextExpiry = list.expiry;
494        return refCount > 0 ? nextExpiry : -nextExpiry;
495      }
496      if (ranAtLeastOneList)
497        runNextTicks();
498      else
499        ranAtLeastOneList = true;
500      listOnTimeout(list, now);
501    }
502    return 0;
503  }
504
505  function listOnTimeout(list, now) {
506    const msecs = list.msecs;
507
508    debug('timeout callback %d', msecs);
509
510    let ranAtLeastOneTimer = false;
511    let timer;
512    while (timer = L.peek(list)) {
513      const diff = now - timer._idleStart;
514
515      // Check if this loop iteration is too early for the next timer.
516      // This happens if there are more timers scheduled for later in the list.
517      if (diff < msecs) {
518        list.expiry = MathMax(timer._idleStart + msecs, now + 1);
519        list.id = timerListId++;
520        timerListQueue.percolateDown(1);
521        debug('%d list wait because diff is %d', msecs, diff);
522        return;
523      }
524
525      if (ranAtLeastOneTimer)
526        runNextTicks();
527      else
528        ranAtLeastOneTimer = true;
529
530      // The actual logic for when a timeout happens.
531      L.remove(timer);
532
533      const asyncId = timer[async_id_symbol];
534
535      if (!timer._onTimeout) {
536        if (!timer._destroyed) {
537          timer._destroyed = true;
538
539          if (timer[kRefed])
540            refCount--;
541
542          if (destroyHooksExist())
543            emitDestroy(asyncId);
544        }
545        continue;
546      }
547
548      emitBefore(asyncId, timer[trigger_async_id_symbol], timer);
549
550      let start;
551      if (timer._repeat)
552        start = getLibuvNow();
553
554      try {
555        const args = timer._timerArgs;
556        if (args === undefined)
557          timer._onTimeout();
558        else
559          ReflectApply(timer._onTimeout, timer, args);
560      } finally {
561        if (timer._repeat && timer._idleTimeout !== -1) {
562          timer._idleTimeout = timer._repeat;
563          insert(timer, timer._idleTimeout, start);
564        } else if (!timer._idleNext && !timer._idlePrev && !timer._destroyed) {
565          timer._destroyed = true;
566
567          if (timer[kRefed])
568            refCount--;
569
570          if (destroyHooksExist())
571            emitDestroy(asyncId);
572        }
573      }
574
575      emitAfter(asyncId);
576    }
577
578    // If `L.peek(list)` returned nothing, the list was either empty or we have
579    // called all of the timer timeouts.
580    // As such, we can remove the list from the object map and
581    // the PriorityQueue.
582    debug('%d list empty', msecs);
583
584    // The current list may have been removed and recreated since the reference
585    // to `list` was created. Make sure they're the same instance of the list
586    // before destroying.
587    if (list === timerListMap[msecs]) {
588      delete timerListMap[msecs];
589      timerListQueue.shift();
590    }
591  }
592
593  return {
594    processImmediate,
595    processTimers
596  };
597}
598
599class Immediate {
600  constructor(callback, args) {
601    this._idleNext = null;
602    this._idlePrev = null;
603    this._onImmediate = callback;
604    this._argv = args;
605    this._destroyed = false;
606    this[kRefed] = false;
607
608    initAsyncResource(this, 'Immediate');
609
610    this.ref();
611    immediateInfo[kCount]++;
612
613    immediateQueue.append(this);
614  }
615
616  ref() {
617    if (this[kRefed] === false) {
618      this[kRefed] = true;
619      if (immediateInfo[kRefCount]++ === 0)
620        toggleImmediateRef(true);
621    }
622    return this;
623  }
624
625  unref() {
626    if (this[kRefed] === true) {
627      this[kRefed] = false;
628      if (--immediateInfo[kRefCount] === 0)
629        toggleImmediateRef(false);
630    }
631    return this;
632  }
633
634  hasRef() {
635    return !!this[kRefed];
636  }
637}
638
639module.exports = {
640  TIMEOUT_MAX,
641  kTimeout: Symbol('timeout'), // For hiding Timeouts on other internals.
642  async_id_symbol,
643  trigger_async_id_symbol,
644  Timeout,
645  Immediate,
646  kRefed,
647  kHasPrimitive,
648  initAsyncResource,
649  setUnrefTimeout,
650  getTimerDuration,
651  immediateQueue,
652  getTimerCallbacks,
653  immediateInfoFields: {
654    kCount,
655    kRefCount,
656    kHasOutstanding
657  },
658  active,
659  unrefActive,
660  insert,
661  timerListMap,
662  timerListQueue,
663  decRefCount,
664  incRefCount
665};
666