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