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