1'use strict'; 2 3const { 4 ArrayIsArray, 5 ArrayPrototypeFilter, 6 ArrayPrototypeForEach, 7 ArrayPrototypeIncludes, 8 ArrayPrototypeMap, 9 ArrayPrototypePush, 10 ArrayPrototypeSplice, 11 ArrayPrototypeUnshift, 12 Boolean, 13 NumberIsSafeInteger, 14 ObjectDefineProperties, 15 ObjectDefineProperty, 16 ObjectKeys, 17 SafeSet, 18 Symbol, 19 TypeError, 20} = primordials; 21 22const { 23 ELDHistogram: _ELDHistogram, 24 PerformanceEntry, 25 mark: _mark, 26 clearMark: _clearMark, 27 measure: _measure, 28 milestones, 29 observerCounts, 30 setupObservers, 31 timeOrigin, 32 timeOriginTimestamp, 33 timerify, 34 constants, 35 installGarbageCollectionTracking, 36 removeGarbageCollectionTracking, 37 loopIdleTime, 38} = internalBinding('performance'); 39 40const { 41 NODE_PERFORMANCE_ENTRY_TYPE_NODE, 42 NODE_PERFORMANCE_ENTRY_TYPE_MARK, 43 NODE_PERFORMANCE_ENTRY_TYPE_MEASURE, 44 NODE_PERFORMANCE_ENTRY_TYPE_GC, 45 NODE_PERFORMANCE_ENTRY_TYPE_FUNCTION, 46 NODE_PERFORMANCE_ENTRY_TYPE_HTTP2, 47 NODE_PERFORMANCE_ENTRY_TYPE_HTTP, 48 49 NODE_PERFORMANCE_MILESTONE_NODE_START, 50 NODE_PERFORMANCE_MILESTONE_V8_START, 51 NODE_PERFORMANCE_MILESTONE_LOOP_START, 52 NODE_PERFORMANCE_MILESTONE_LOOP_EXIT, 53 NODE_PERFORMANCE_MILESTONE_BOOTSTRAP_COMPLETE, 54 NODE_PERFORMANCE_MILESTONE_ENVIRONMENT 55} = constants; 56 57const { AsyncResource } = require('async_hooks'); 58const L = require('internal/linkedlist'); 59const kInspect = require('internal/util').customInspectSymbol; 60 61const { 62 ERR_INVALID_CALLBACK, 63 ERR_INVALID_ARG_TYPE, 64 ERR_INVALID_OPT_VALUE, 65 ERR_VALID_PERFORMANCE_ENTRY_TYPE, 66 ERR_INVALID_PERFORMANCE_MARK 67} = require('internal/errors').codes; 68 69const { 70 Histogram, 71 createHistogram, 72 kHandle, 73} = require('internal/histogram'); 74 75const { setImmediate } = require('timers'); 76const kCallback = Symbol('callback'); 77const kTypes = Symbol('types'); 78const kEntries = Symbol('entries'); 79const kBuffer = Symbol('buffer'); 80const kBuffering = Symbol('buffering'); 81const kQueued = Symbol('queued'); 82const kTimerified = Symbol('timerified'); 83const kInsertEntry = Symbol('insert-entry'); 84const kGetEntries = Symbol('get-entries'); 85const kIndex = Symbol('index'); 86const kMarks = Symbol('marks'); 87const kEnabled = Symbol('kEnabled'); 88 89const observers = {}; 90const observerableTypes = [ 91 'node', 92 'mark', 93 'measure', 94 'gc', 95 'function', 96 'http2', 97 'http', 98]; 99 100const IDX_STREAM_STATS_ID = 0; 101const IDX_STREAM_STATS_TIMETOFIRSTBYTE = 1; 102const IDX_STREAM_STATS_TIMETOFIRSTHEADER = 2; 103const IDX_STREAM_STATS_TIMETOFIRSTBYTESENT = 3; 104const IDX_STREAM_STATS_SENTBYTES = 4; 105const IDX_STREAM_STATS_RECEIVEDBYTES = 5; 106 107const IDX_SESSION_STATS_TYPE = 0; 108const IDX_SESSION_STATS_PINGRTT = 1; 109const IDX_SESSION_STATS_FRAMESRECEIVED = 2; 110const IDX_SESSION_STATS_FRAMESSENT = 3; 111const IDX_SESSION_STATS_STREAMCOUNT = 4; 112const IDX_SESSION_STATS_STREAMAVERAGEDURATION = 5; 113const IDX_SESSION_STATS_DATA_SENT = 6; 114const IDX_SESSION_STATS_DATA_RECEIVED = 7; 115const IDX_SESSION_STATS_MAX_CONCURRENT_STREAMS = 8; 116 117let http2; 118let sessionStats; 119let streamStats; 120 121function collectHttp2Stats(entry) { 122 if (http2 === undefined) http2 = internalBinding('http2'); 123 switch (entry.name) { 124 case 'Http2Stream': 125 if (streamStats === undefined) 126 streamStats = http2.streamStats; 127 entry.id = 128 streamStats[IDX_STREAM_STATS_ID] >>> 0; 129 entry.timeToFirstByte = 130 streamStats[IDX_STREAM_STATS_TIMETOFIRSTBYTE]; 131 entry.timeToFirstHeader = 132 streamStats[IDX_STREAM_STATS_TIMETOFIRSTHEADER]; 133 entry.timeToFirstByteSent = 134 streamStats[IDX_STREAM_STATS_TIMETOFIRSTBYTESENT]; 135 entry.bytesWritten = 136 streamStats[IDX_STREAM_STATS_SENTBYTES]; 137 entry.bytesRead = 138 streamStats[IDX_STREAM_STATS_RECEIVEDBYTES]; 139 break; 140 case 'Http2Session': 141 if (sessionStats === undefined) 142 sessionStats = http2.sessionStats; 143 entry.type = 144 sessionStats[IDX_SESSION_STATS_TYPE] >>> 0 === 0 ? 'server' : 'client'; 145 entry.pingRTT = 146 sessionStats[IDX_SESSION_STATS_PINGRTT]; 147 entry.framesReceived = 148 sessionStats[IDX_SESSION_STATS_FRAMESRECEIVED]; 149 entry.framesSent = 150 sessionStats[IDX_SESSION_STATS_FRAMESSENT]; 151 entry.streamCount = 152 sessionStats[IDX_SESSION_STATS_STREAMCOUNT]; 153 entry.streamAverageDuration = 154 sessionStats[IDX_SESSION_STATS_STREAMAVERAGEDURATION]; 155 entry.bytesWritten = 156 sessionStats[IDX_SESSION_STATS_DATA_SENT]; 157 entry.bytesRead = 158 sessionStats[IDX_SESSION_STATS_DATA_RECEIVED]; 159 entry.maxConcurrentStreams = 160 sessionStats[IDX_SESSION_STATS_MAX_CONCURRENT_STREAMS]; 161 break; 162 } 163} 164 165function now() { 166 const hr = process.hrtime(); 167 return hr[0] * 1000 + hr[1] / 1e6; 168} 169 170function getMilestoneTimestamp(milestoneIdx) { 171 const ns = milestones[milestoneIdx]; 172 if (ns === -1) 173 return ns; 174 return ns / 1e6 - timeOrigin; 175} 176 177class PerformanceNodeTiming extends PerformanceEntry { 178 constructor() { 179 super(); 180 181 ObjectDefineProperties(this, { 182 name: { 183 enumerable: true, 184 configurable: true, 185 value: 'node' 186 }, 187 188 entryType: { 189 enumerable: true, 190 configurable: true, 191 value: 'node' 192 }, 193 194 startTime: { 195 enumerable: true, 196 configurable: true, 197 value: 0 198 }, 199 200 duration: { 201 enumerable: true, 202 configurable: true, 203 get() { 204 return now() - timeOrigin; 205 } 206 }, 207 208 nodeStart: { 209 enumerable: true, 210 configurable: true, 211 get() { 212 return getMilestoneTimestamp(NODE_PERFORMANCE_MILESTONE_NODE_START); 213 } 214 }, 215 216 v8Start: { 217 enumerable: true, 218 configurable: true, 219 get() { 220 return getMilestoneTimestamp(NODE_PERFORMANCE_MILESTONE_V8_START); 221 } 222 }, 223 224 environment: { 225 enumerable: true, 226 configurable: true, 227 get() { 228 return getMilestoneTimestamp(NODE_PERFORMANCE_MILESTONE_ENVIRONMENT); 229 } 230 }, 231 232 loopStart: { 233 enumerable: true, 234 configurable: true, 235 get() { 236 return getMilestoneTimestamp(NODE_PERFORMANCE_MILESTONE_LOOP_START); 237 } 238 }, 239 240 loopExit: { 241 enumerable: true, 242 configurable: true, 243 get() { 244 return getMilestoneTimestamp(NODE_PERFORMANCE_MILESTONE_LOOP_EXIT); 245 } 246 }, 247 248 bootstrapComplete: { 249 enumerable: true, 250 configurable: true, 251 get() { 252 return getMilestoneTimestamp( 253 NODE_PERFORMANCE_MILESTONE_BOOTSTRAP_COMPLETE); 254 } 255 }, 256 257 idleTime: { 258 enumerable: true, 259 configurable: true, 260 get() { 261 return loopIdleTime(); 262 } 263 } 264 }); 265 } 266 [kInspect]() { 267 return { 268 name: 'node', 269 entryType: 'node', 270 startTime: this.startTime, 271 duration: this.duration, 272 nodeStart: this.nodeStart, 273 v8Start: this.v8Start, 274 bootstrapComplete: this.bootstrapComplete, 275 environment: this.environment, 276 loopStart: this.loopStart, 277 loopExit: this.loopExit 278 }; 279 } 280} 281 282const nodeTiming = new PerformanceNodeTiming(); 283 284// Maintains a list of entries as a linked list stored in insertion order. 285class PerformanceObserverEntryList { 286 constructor() { 287 ObjectDefineProperties(this, { 288 [kEntries]: { 289 writable: true, 290 enumerable: false, 291 value: {} 292 } 293 }); 294 L.init(this[kEntries]); 295 } 296 297 [kInsertEntry](entry) { 298 const item = { entry }; 299 L.append(this[kEntries], item); 300 } 301 302 [kGetEntries](name, type) { 303 const ret = []; 304 const list = this[kEntries]; 305 if (!L.isEmpty(list)) { 306 let item = L.peek(list); 307 while (item && item !== list) { 308 const entry = item.entry; 309 if ((name && entry.name !== name) || 310 (type && entry.entryType !== type)) { 311 item = item._idlePrev; 312 continue; 313 } 314 sortedInsert(ret, entry); 315 item = item._idlePrev; 316 } 317 } 318 return ret; 319 } 320 321 // While the items are stored in insertion order, getEntries() is 322 // required to return items sorted by startTime. 323 getEntries() { 324 return this[kGetEntries](); 325 } 326 327 getEntriesByType(type) { 328 return this[kGetEntries](undefined, `${type}`); 329 } 330 331 getEntriesByName(name, type) { 332 return this[kGetEntries](`${name}`, type !== undefined ? `${type}` : type); 333 } 334} 335 336class PerformanceObserver extends AsyncResource { 337 constructor(callback) { 338 if (typeof callback !== 'function') { 339 throw new ERR_INVALID_CALLBACK(callback); 340 } 341 super('PerformanceObserver'); 342 ObjectDefineProperties(this, { 343 [kTypes]: { 344 enumerable: false, 345 writable: true, 346 value: {} 347 }, 348 [kCallback]: { 349 enumerable: false, 350 writable: true, 351 value: callback 352 }, 353 [kBuffer]: { 354 enumerable: false, 355 writable: true, 356 value: new PerformanceObserverEntryList() 357 }, 358 [kBuffering]: { 359 enumerable: false, 360 writable: true, 361 value: false 362 }, 363 [kQueued]: { 364 enumerable: false, 365 writable: true, 366 value: false 367 } 368 }); 369 } 370 371 disconnect() { 372 const observerCountsGC = observerCounts[NODE_PERFORMANCE_ENTRY_TYPE_GC]; 373 const types = this[kTypes]; 374 ArrayPrototypeForEach(ObjectKeys(types), (key) => { 375 const item = types[key]; 376 if (item) { 377 L.remove(item); 378 observerCounts[key]--; 379 } 380 }); 381 this[kTypes] = {}; 382 if (observerCountsGC === 1 && 383 observerCounts[NODE_PERFORMANCE_ENTRY_TYPE_GC] === 0) { 384 removeGarbageCollectionTracking(); 385 } 386 } 387 388 observe(options) { 389 if (typeof options !== 'object' || options === null) { 390 throw new ERR_INVALID_ARG_TYPE('options', 'Object', options); 391 } 392 const { entryTypes } = options; 393 if (!ArrayIsArray(entryTypes)) { 394 throw new ERR_INVALID_OPT_VALUE('entryTypes', entryTypes); 395 } 396 const filteredEntryTypes = 397 ArrayPrototypeMap(ArrayPrototypeFilter(entryTypes, filterTypes), 398 mapTypes); 399 if (filteredEntryTypes.length === 0) { 400 throw new ERR_VALID_PERFORMANCE_ENTRY_TYPE(); 401 } 402 this.disconnect(); 403 const observerCountsGC = observerCounts[NODE_PERFORMANCE_ENTRY_TYPE_GC]; 404 this[kBuffer][kEntries] = []; 405 L.init(this[kBuffer][kEntries]); 406 this[kBuffering] = Boolean(options.buffered); 407 ArrayPrototypeForEach(filteredEntryTypes, (entryType) => { 408 const list = getObserversList(entryType); 409 if (this[kTypes][entryType]) return; 410 const item = { obs: this }; 411 this[kTypes][entryType] = item; 412 L.append(list, item); 413 observerCounts[entryType]++; 414 }); 415 if (observerCountsGC === 0 && 416 observerCounts[NODE_PERFORMANCE_ENTRY_TYPE_GC] === 1) { 417 installGarbageCollectionTracking(); 418 } 419 } 420} 421 422class Performance { 423 constructor() { 424 this[kIndex] = { 425 [kMarks]: new SafeSet() 426 }; 427 } 428 429 get nodeTiming() { 430 return nodeTiming; 431 } 432 433 get timeOrigin() { 434 return timeOriginTimestamp; 435 } 436 437 now() { 438 return now() - timeOrigin; 439 } 440 441 mark(name) { 442 name = `${name}`; 443 _mark(name); 444 this[kIndex][kMarks].add(name); 445 } 446 447 measure(name, startMark, endMark) { 448 name = `${name}`; 449 const marks = this[kIndex][kMarks]; 450 if (arguments.length >= 3) { 451 if (!marks.has(endMark) && !(endMark in nodeTiming)) 452 throw new ERR_INVALID_PERFORMANCE_MARK(endMark); 453 else 454 endMark = `${endMark}`; 455 } 456 startMark = startMark !== undefined ? `${startMark}` : ''; 457 _measure(name, startMark, endMark); 458 } 459 460 clearMarks(name) { 461 if (name !== undefined) { 462 name = `${name}`; 463 this[kIndex][kMarks].delete(name); 464 _clearMark(name); 465 } else { 466 this[kIndex][kMarks].clear(); 467 _clearMark(); 468 } 469 } 470 471 timerify(fn) { 472 if (typeof fn !== 'function') { 473 throw new ERR_INVALID_ARG_TYPE('fn', 'Function', fn); 474 } 475 if (fn[kTimerified]) 476 return fn[kTimerified]; 477 const ret = timerify(fn, fn.length); 478 ObjectDefineProperty(fn, kTimerified, { 479 enumerable: false, 480 configurable: true, 481 writable: false, 482 value: ret 483 }); 484 ObjectDefineProperties(ret, { 485 [kTimerified]: { 486 enumerable: false, 487 configurable: true, 488 writable: false, 489 value: ret 490 }, 491 name: { 492 enumerable: false, 493 configurable: true, 494 writable: false, 495 value: `timerified ${fn.name}` 496 } 497 }); 498 return ret; 499 } 500 501 eventLoopUtilization(util1, util2) { 502 const ls = nodeTiming.loopStart; 503 504 if (ls <= 0) { 505 return { idle: 0, active: 0, utilization: 0 }; 506 } 507 508 if (util2) { 509 const idle = util1.idle - util2.idle; 510 const active = util1.active - util2.active; 511 return { idle, active, utilization: active / (idle + active) }; 512 } 513 514 const idle = nodeTiming.idleTime; 515 const active = performance.now() - ls - idle; 516 517 if (!util1) { 518 return { idle, active, utilization: active / (idle + active) }; 519 } 520 521 const idle_delta = idle - util1.idle; 522 const active_delta = active - util1.active; 523 const utilization = active_delta / (idle_delta + active_delta); 524 return { idle: idle_delta, active: active_delta, utilization }; 525 } 526 527 [kInspect]() { 528 return { 529 nodeTiming: this.nodeTiming, 530 timeOrigin: this.timeOrigin, 531 idleTime: this.idleTime, 532 }; 533 } 534} 535 536const performance = new Performance(); 537 538function getObserversList(type) { 539 let list = observers[type]; 540 if (list === undefined) { 541 list = observers[type] = {}; 542 L.init(list); 543 } 544 return list; 545} 546 547function doNotify(observer) { 548 observer[kQueued] = false; 549 observer.runInAsyncScope(observer[kCallback], 550 observer, 551 observer[kBuffer], 552 observer); 553 observer[kBuffer][kEntries] = []; 554 L.init(observer[kBuffer][kEntries]); 555} 556 557// Set up the callback used to receive PerformanceObserver notifications 558function observersCallback(entry) { 559 const type = mapTypes(entry.entryType); 560 561 if (type === NODE_PERFORMANCE_ENTRY_TYPE_HTTP2) 562 collectHttp2Stats(entry); 563 564 const list = getObserversList(type); 565 566 let current = L.peek(list); 567 568 while (current && current.obs) { 569 const observer = current.obs; 570 // First, add the item to the observers buffer 571 const buffer = observer[kBuffer]; 572 buffer[kInsertEntry](entry); 573 // Second, check to see if we're buffering 574 if (observer[kBuffering]) { 575 // If we are, schedule a setImmediate call if one hasn't already 576 if (!observer[kQueued]) { 577 observer[kQueued] = true; 578 // Use setImmediate instead of nextTick to give more time 579 // for multiple entries to collect. 580 setImmediate(doNotify, observer); 581 } 582 } else { 583 // If not buffering, notify immediately 584 doNotify(observer); 585 } 586 current = current._idlePrev; 587 } 588} 589setupObservers(observersCallback); 590 591function filterTypes(i) { 592 return ArrayPrototypeIncludes(observerableTypes, `${i}`); 593} 594 595function mapTypes(i) { 596 switch (i) { 597 case 'node': return NODE_PERFORMANCE_ENTRY_TYPE_NODE; 598 case 'mark': return NODE_PERFORMANCE_ENTRY_TYPE_MARK; 599 case 'measure': return NODE_PERFORMANCE_ENTRY_TYPE_MEASURE; 600 case 'gc': return NODE_PERFORMANCE_ENTRY_TYPE_GC; 601 case 'function': return NODE_PERFORMANCE_ENTRY_TYPE_FUNCTION; 602 case 'http2': return NODE_PERFORMANCE_ENTRY_TYPE_HTTP2; 603 case 'http': return NODE_PERFORMANCE_ENTRY_TYPE_HTTP; 604 } 605} 606 607// The specification requires that PerformanceEntry instances are sorted 608// according to startTime. Unfortunately, they are not necessarily created 609// in that same order, and can be reported to the JS layer in any order, 610// which means we need to keep the list sorted as we insert. 611function getInsertLocation(list, entryStartTime) { 612 let start = 0; 613 let end = list.length; 614 while (start < end) { 615 const pivot = (end + start) >>> 1; 616 if (list[pivot].startTime === entryStartTime) 617 return pivot; 618 if (list[pivot].startTime < entryStartTime) 619 start = pivot + 1; 620 else 621 end = pivot; 622 } 623 return start; 624} 625 626function sortedInsert(list, entry) { 627 const entryStartTime = entry.startTime; 628 if (list.length === 0 || 629 (list[list.length - 1].startTime < entryStartTime)) { 630 ArrayPrototypePush(list, entry); 631 return; 632 } 633 if (list[0] && (list[0].startTime > entryStartTime)) { 634 ArrayPrototypeUnshift(list, entry); 635 return; 636 } 637 const location = getInsertLocation(list, entryStartTime); 638 ArrayPrototypeSplice(list, location, 0, entry); 639} 640 641class ELDHistogram extends Histogram { 642 constructor(i) { 643 if (!(i instanceof _ELDHistogram)) { 644 // eslint-disable-next-line no-restricted-syntax 645 throw new TypeError('illegal constructor'); 646 } 647 super(i); 648 this[kEnabled] = false; 649 } 650 enable() { 651 if (this[kEnabled]) return false; 652 this[kEnabled] = true; 653 this[kHandle].start(); 654 return true; 655 } 656 disable() { 657 if (!this[kEnabled]) return false; 658 this[kEnabled] = false; 659 this[kHandle].stop(); 660 return true; 661 } 662} 663 664function monitorEventLoopDelay(options = {}) { 665 if (typeof options !== 'object' || options === null) { 666 throw new ERR_INVALID_ARG_TYPE('options', 'Object', options); 667 } 668 const { resolution = 10 } = options; 669 if (typeof resolution !== 'number') { 670 throw new ERR_INVALID_ARG_TYPE('options.resolution', 671 'number', resolution); 672 } 673 if (resolution <= 0 || !NumberIsSafeInteger(resolution)) { 674 throw new ERR_INVALID_OPT_VALUE.RangeError('resolution', resolution); 675 } 676 return new ELDHistogram(new _ELDHistogram(resolution)); 677} 678 679module.exports = { 680 performance, 681 PerformanceObserver, 682 monitorEventLoopDelay, 683 createHistogram, 684}; 685 686ObjectDefineProperty(module.exports, 'constants', { 687 configurable: false, 688 enumerable: true, 689 value: constants 690}); 691