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