1'use strict'; 2 3const { 4 ArrayFrom, 5 ArrayIsArray, 6 ArrayPrototypeFilter, 7 ArrayPrototypeIncludes, 8 ArrayPrototypePush, 9 ArrayPrototypePushApply, 10 ArrayPrototypeSlice, 11 ArrayPrototypeSort, 12 Error, 13 MathMax, 14 MathMin, 15 ObjectDefineProperties, 16 ObjectFreeze, 17 ObjectKeys, 18 SafeMap, 19 SafeSet, 20 Symbol, 21} = primordials; 22 23const { 24 constants: { 25 NODE_PERFORMANCE_ENTRY_TYPE_GC, 26 NODE_PERFORMANCE_ENTRY_TYPE_HTTP2, 27 NODE_PERFORMANCE_ENTRY_TYPE_HTTP, 28 NODE_PERFORMANCE_ENTRY_TYPE_NET, 29 NODE_PERFORMANCE_ENTRY_TYPE_DNS, 30 }, 31 installGarbageCollectionTracking, 32 observerCounts, 33 removeGarbageCollectionTracking, 34 setupObservers, 35} = internalBinding('performance'); 36 37const { 38 InternalPerformanceEntry, 39 isPerformanceEntry, 40} = require('internal/perf/performance_entry'); 41 42const { 43 codes: { 44 ERR_INVALID_ARG_VALUE, 45 ERR_INVALID_ARG_TYPE, 46 ERR_MISSING_ARGS, 47 }, 48} = require('internal/errors'); 49 50const { 51 validateFunction, 52 validateObject, 53} = require('internal/validators'); 54 55const { 56 customInspectSymbol: kInspect, 57 deprecate, 58 lazyDOMException, 59 kEmptyObject, 60} = require('internal/util'); 61 62const { 63 setImmediate, 64} = require('timers'); 65 66const { inspect } = require('util'); 67 68const { now } = require('internal/perf/utils'); 69const { convertToInt } = require('internal/webidl'); 70 71const kDispatch = Symbol('kDispatch'); 72const kMaybeBuffer = Symbol('kMaybeBuffer'); 73const kDeprecatedFields = Symbol('kDeprecatedFields'); 74 75const kDeprecationMessage = 76 'Custom PerformanceEntry accessors are deprecated. ' + 77 'Please use the detail property.'; 78 79const kTypeSingle = 0; 80const kTypeMultiple = 1; 81 82let gcTrackingInstalled = false; 83 84const kSupportedEntryTypes = ObjectFreeze([ 85 'dns', 86 'function', 87 'gc', 88 'http', 89 'http2', 90 'mark', 91 'measure', 92 'net', 93 'resource', 94]); 95 96// Performance timeline entry Buffers 97let markEntryBuffer = []; 98let measureEntryBuffer = []; 99let resourceTimingBuffer = []; 100let resourceTimingSecondaryBuffer = []; 101const kPerformanceEntryBufferWarnSize = 1e6; 102// https://www.w3.org/TR/timing-entrytypes-registry/#registry 103// Default buffer limit for resource timing entries. 104let resourceTimingBufferSizeLimit = 250; 105let dispatchBufferFull; 106let resourceTimingBufferFullPending = false; 107 108const kClearPerformanceEntryBuffers = ObjectFreeze({ 109 'mark': 'performance.clearMarks', 110 'measure': 'performance.clearMeasures', 111}); 112const kWarnedEntryTypes = new SafeMap(); 113 114const kObservers = new SafeSet(); 115const kPending = new SafeSet(); 116let isPending = false; 117 118function queuePending() { 119 if (isPending) return; 120 isPending = true; 121 setImmediate(() => { 122 isPending = false; 123 const pendings = ArrayFrom(kPending.values()); 124 kPending.clear(); 125 for (const pending of pendings) 126 pending[kDispatch](); 127 }); 128} 129 130function getObserverType(type) { 131 switch (type) { 132 case 'gc': return NODE_PERFORMANCE_ENTRY_TYPE_GC; 133 case 'http2': return NODE_PERFORMANCE_ENTRY_TYPE_HTTP2; 134 case 'http': return NODE_PERFORMANCE_ENTRY_TYPE_HTTP; 135 case 'net': return NODE_PERFORMANCE_ENTRY_TYPE_NET; 136 case 'dns': return NODE_PERFORMANCE_ENTRY_TYPE_DNS; 137 } 138} 139 140function maybeDecrementObserverCounts(entryTypes) { 141 for (const type of entryTypes) { 142 const observerType = getObserverType(type); 143 144 if (observerType !== undefined) { 145 observerCounts[observerType]--; 146 147 if (observerType === NODE_PERFORMANCE_ENTRY_TYPE_GC && 148 observerCounts[observerType] === 0) { 149 removeGarbageCollectionTracking(); 150 gcTrackingInstalled = false; 151 } 152 } 153 } 154} 155 156function maybeIncrementObserverCount(type) { 157 const observerType = getObserverType(type); 158 159 if (observerType !== undefined) { 160 observerCounts[observerType]++; 161 if (!gcTrackingInstalled && 162 observerType === NODE_PERFORMANCE_ENTRY_TYPE_GC) { 163 installGarbageCollectionTracking(); 164 gcTrackingInstalled = true; 165 } 166 } 167} 168 169class PerformanceObserverEntryList { 170 #buffer = []; 171 172 constructor(entries) { 173 this.#buffer = ArrayPrototypeSort(entries, (first, second) => { 174 return first.startTime - second.startTime; 175 }); 176 } 177 178 getEntries() { 179 return ArrayPrototypeSlice(this.#buffer); 180 } 181 182 getEntriesByType(type) { 183 type = `${type}`; 184 return ArrayPrototypeFilter( 185 this.#buffer, 186 (entry) => entry.entryType === type); 187 } 188 189 getEntriesByName(name, type) { 190 name = `${name}`; 191 if (type != null /** not nullish */) { 192 return ArrayPrototypeFilter( 193 this.#buffer, 194 (entry) => entry.name === name && entry.entryType === type); 195 } 196 return ArrayPrototypeFilter( 197 this.#buffer, 198 (entry) => entry.name === name); 199 } 200 201 [kInspect](depth, options) { 202 if (depth < 0) return this; 203 204 const opts = { 205 ...options, 206 depth: options.depth == null ? null : options.depth - 1, 207 }; 208 209 return `PerformanceObserverEntryList ${inspect(this.#buffer, opts)}`; 210 } 211} 212 213class PerformanceObserver { 214 #buffer = []; 215 #entryTypes = new SafeSet(); 216 #type; 217 #callback; 218 219 constructor(callback) { 220 validateFunction(callback, 'callback'); 221 this.#callback = callback; 222 } 223 224 observe(options = kEmptyObject) { 225 validateObject(options, 'options'); 226 const { 227 entryTypes, 228 type, 229 buffered, 230 } = { ...options }; 231 if (entryTypes === undefined && type === undefined) 232 throw new ERR_MISSING_ARGS('options.entryTypes', 'options.type'); 233 if (entryTypes != null && type != null) 234 throw new ERR_INVALID_ARG_VALUE('options.entryTypes', 235 entryTypes, 236 'options.entryTypes can not set with ' + 237 'options.type together'); 238 239 switch (this.#type) { 240 case undefined: 241 if (entryTypes !== undefined) this.#type = kTypeMultiple; 242 if (type !== undefined) this.#type = kTypeSingle; 243 break; 244 case kTypeSingle: 245 if (entryTypes !== undefined) 246 throw lazyDOMException( 247 'PerformanceObserver can not change to multiple observations', 248 'InvalidModificationError'); 249 break; 250 case kTypeMultiple: 251 if (type !== undefined) 252 throw lazyDOMException( 253 'PerformanceObserver can not change to single observation', 254 'InvalidModificationError'); 255 break; 256 } 257 258 if (this.#type === kTypeMultiple) { 259 if (!ArrayIsArray(entryTypes)) { 260 throw new ERR_INVALID_ARG_TYPE( 261 'options.entryTypes', 262 'string[]', 263 entryTypes); 264 } 265 maybeDecrementObserverCounts(this.#entryTypes); 266 this.#entryTypes.clear(); 267 for (let n = 0; n < entryTypes.length; n++) { 268 if (ArrayPrototypeIncludes(kSupportedEntryTypes, entryTypes[n])) { 269 this.#entryTypes.add(entryTypes[n]); 270 maybeIncrementObserverCount(entryTypes[n]); 271 } 272 } 273 } else { 274 if (!ArrayPrototypeIncludes(kSupportedEntryTypes, type)) 275 return; 276 this.#entryTypes.add(type); 277 maybeIncrementObserverCount(type); 278 if (buffered) { 279 const entries = filterBufferMapByNameAndType(undefined, type); 280 ArrayPrototypePushApply(this.#buffer, entries); 281 kPending.add(this); 282 if (kPending.size) 283 queuePending(); 284 } 285 } 286 287 if (this.#entryTypes.size) 288 kObservers.add(this); 289 else 290 this.disconnect(); 291 } 292 293 disconnect() { 294 maybeDecrementObserverCounts(this.#entryTypes); 295 kObservers.delete(this); 296 kPending.delete(this); 297 this.#buffer = []; 298 this.#entryTypes.clear(); 299 this.#type = undefined; 300 } 301 302 takeRecords() { 303 const list = this.#buffer; 304 this.#buffer = []; 305 return list; 306 } 307 308 static get supportedEntryTypes() { 309 return kSupportedEntryTypes; 310 } 311 312 [kMaybeBuffer](entry) { 313 if (!this.#entryTypes.has(entry.entryType)) 314 return; 315 ArrayPrototypePush(this.#buffer, entry); 316 kPending.add(this); 317 if (kPending.size) 318 queuePending(); 319 } 320 321 [kDispatch]() { 322 this.#callback(new PerformanceObserverEntryList(this.takeRecords()), 323 this); 324 } 325 326 [kInspect](depth, options) { 327 if (depth < 0) return this; 328 329 const opts = { 330 ...options, 331 depth: options.depth == null ? null : options.depth - 1, 332 }; 333 334 return `PerformanceObserver ${inspect({ 335 connected: kObservers.has(this), 336 pending: kPending.has(this), 337 entryTypes: ArrayFrom(this.#entryTypes), 338 buffer: this.#buffer, 339 }, opts)}`; 340 } 341} 342 343/** 344 * https://www.w3.org/TR/performance-timeline/#dfn-queue-a-performanceentry 345 * 346 * Add the performance entry to the interested performance observer's queue. 347 */ 348function enqueue(entry) { 349 if (!isPerformanceEntry(entry)) 350 throw new ERR_INVALID_ARG_TYPE('entry', 'PerformanceEntry', entry); 351 352 for (const obs of kObservers) { 353 obs[kMaybeBuffer](entry); 354 } 355} 356 357/** 358 * Add the user timing entry to the global buffer. 359 */ 360function bufferUserTiming(entry) { 361 const entryType = entry.entryType; 362 let buffer; 363 if (entryType === 'mark') { 364 buffer = markEntryBuffer; 365 } else if (entryType === 'measure') { 366 buffer = measureEntryBuffer; 367 } else { 368 return; 369 } 370 371 ArrayPrototypePush(buffer, entry); 372 const count = buffer.length; 373 374 if (count > kPerformanceEntryBufferWarnSize && 375 !kWarnedEntryTypes.has(entryType)) { 376 kWarnedEntryTypes.set(entryType, true); 377 // No error code for this since it is a Warning 378 // eslint-disable-next-line no-restricted-syntax 379 const w = new Error('Possible perf_hooks memory leak detected. ' + 380 `${count} ${entryType} entries added to the global ` + 381 'performance entry buffer. Use ' + 382 `${kClearPerformanceEntryBuffers[entryType]} to ` + 383 'clear the buffer.'); 384 w.name = 'MaxPerformanceEntryBufferExceededWarning'; 385 w.entryType = entryType; 386 w.count = count; 387 process.emitWarning(w); 388 } 389} 390 391/** 392 * Add the resource timing entry to the global buffer if the buffer size is not 393 * exceeding the buffer limit, or dispatch a buffer full event on the global 394 * performance object. 395 * 396 * See also https://www.w3.org/TR/resource-timing-2/#dfn-add-a-performanceresourcetiming-entry 397 */ 398function bufferResourceTiming(entry) { 399 if (resourceTimingBuffer.length < resourceTimingBufferSizeLimit && !resourceTimingBufferFullPending) { 400 ArrayPrototypePush(resourceTimingBuffer, entry); 401 return; 402 } 403 404 if (!resourceTimingBufferFullPending) { 405 resourceTimingBufferFullPending = true; 406 setImmediate(() => { 407 while (resourceTimingSecondaryBuffer.length > 0) { 408 const excessNumberBefore = resourceTimingSecondaryBuffer.length; 409 dispatchBufferFull('resourcetimingbufferfull'); 410 411 // Calculate the number of items to be pushed to the global buffer. 412 const numbersToPreserve = MathMax( 413 MathMin(resourceTimingBufferSizeLimit - resourceTimingBuffer.length, resourceTimingSecondaryBuffer.length), 414 0, 415 ); 416 const excessNumberAfter = resourceTimingSecondaryBuffer.length - numbersToPreserve; 417 for (let idx = 0; idx < numbersToPreserve; idx++) { 418 ArrayPrototypePush(resourceTimingBuffer, resourceTimingSecondaryBuffer[idx]); 419 } 420 421 if (excessNumberBefore <= excessNumberAfter) { 422 resourceTimingSecondaryBuffer = []; 423 } 424 } 425 resourceTimingBufferFullPending = false; 426 }); 427 } 428 429 ArrayPrototypePush(resourceTimingSecondaryBuffer, entry); 430} 431 432// https://w3c.github.io/resource-timing/#dom-performance-setresourcetimingbuffersize 433function setResourceTimingBufferSize(maxSize) { 434 // unsigned long 435 maxSize = convertToInt('maxSize', maxSize, 32); 436 // If the maxSize parameter is less than resource timing buffer current 437 // size, no PerformanceResourceTiming objects are to be removed from the 438 // performance entry buffer. 439 resourceTimingBufferSizeLimit = maxSize; 440} 441 442function setDispatchBufferFull(fn) { 443 dispatchBufferFull = fn; 444} 445 446function clearEntriesFromBuffer(type, name) { 447 if (type !== 'mark' && type !== 'measure' && type !== 'resource') { 448 return; 449 } 450 451 if (type === 'mark') { 452 markEntryBuffer = name === undefined ? 453 [] : ArrayPrototypeFilter(markEntryBuffer, (entry) => entry.name !== name); 454 } else if (type === 'measure') { 455 measureEntryBuffer = name === undefined ? 456 [] : ArrayPrototypeFilter(measureEntryBuffer, (entry) => entry.name !== name); 457 } else { 458 resourceTimingBuffer = name === undefined ? 459 [] : ArrayPrototypeFilter(resourceTimingBuffer, (entry) => entry.name !== name); 460 } 461} 462 463function filterBufferMapByNameAndType(name, type) { 464 let bufferList; 465 if (type === 'mark') { 466 bufferList = markEntryBuffer; 467 } else if (type === 'measure') { 468 bufferList = measureEntryBuffer; 469 } else if (type === 'resource') { 470 bufferList = resourceTimingBuffer; 471 } else if (type !== undefined) { 472 // Unrecognized type; 473 return []; 474 } else { 475 bufferList = []; 476 ArrayPrototypePushApply(bufferList, markEntryBuffer); 477 ArrayPrototypePushApply(bufferList, measureEntryBuffer); 478 ArrayPrototypePushApply(bufferList, resourceTimingBuffer); 479 } 480 if (name !== undefined) { 481 bufferList = ArrayPrototypeFilter(bufferList, (buffer) => buffer.name === name); 482 } else if (type !== undefined) { 483 bufferList = ArrayPrototypeSlice(bufferList); 484 } 485 486 return ArrayPrototypeSort(bufferList, (first, second) => { 487 return first.startTime - second.startTime; 488 }); 489} 490 491function observerCallback(name, type, startTime, duration, details) { 492 const entry = 493 new InternalPerformanceEntry( 494 name, 495 type, 496 startTime, 497 duration, 498 details); 499 500 if (details !== undefined) { 501 // GC, HTTP2, and HTTP PerformanceEntry used additional 502 // properties directly off the entry. Those have been 503 // moved into the details property. The existing accessors 504 // are still included but are deprecated. 505 entry[kDeprecatedFields] = new SafeMap(); 506 507 const detailKeys = ObjectKeys(details); 508 const props = {}; 509 for (let n = 0; n < detailKeys.length; n++) { 510 const key = detailKeys[n]; 511 entry[kDeprecatedFields].set(key, details[key]); 512 props[key] = { 513 configurable: true, 514 enumerable: true, 515 get: deprecate(() => { 516 return entry[kDeprecatedFields].get(key); 517 }, kDeprecationMessage, 'DEP0152'), 518 set: deprecate((value) => { 519 entry[kDeprecatedFields].set(key, value); 520 }, kDeprecationMessage, 'DEP0152'), 521 }; 522 } 523 ObjectDefineProperties(entry, props); 524 } 525 526 enqueue(entry); 527} 528 529setupObservers(observerCallback); 530 531function hasObserver(type) { 532 const observerType = getObserverType(type); 533 return observerCounts[observerType] > 0; 534} 535 536 537function startPerf(target, key, context = {}) { 538 target[key] = { 539 ...context, 540 startTime: now(), 541 }; 542} 543 544function stopPerf(target, key, context = {}) { 545 const ctx = target[key]; 546 if (!ctx) { 547 return; 548 } 549 const startTime = ctx.startTime; 550 const entry = new InternalPerformanceEntry( 551 ctx.name, 552 ctx.type, 553 startTime, 554 now() - startTime, 555 { ...ctx.detail, ...context.detail }, 556 ); 557 enqueue(entry); 558} 559 560module.exports = { 561 PerformanceObserver, 562 PerformanceObserverEntryList, 563 enqueue, 564 hasObserver, 565 clearEntriesFromBuffer, 566 filterBufferMapByNameAndType, 567 startPerf, 568 stopPerf, 569 570 bufferUserTiming, 571 bufferResourceTiming, 572 setResourceTimingBufferSize, 573 setDispatchBufferFull, 574}; 575