'use strict'; const { ArrayFrom, ArrayIsArray, ArrayPrototypeFilter, ArrayPrototypeIncludes, ArrayPrototypePush, ArrayPrototypePushApply, ArrayPrototypeSlice, ArrayPrototypeSort, Error, MathMax, MathMin, ObjectDefineProperties, ObjectFreeze, ObjectKeys, SafeMap, SafeSet, Symbol, SymbolToStringTag, } = primordials; const { constants: { NODE_PERFORMANCE_ENTRY_TYPE_GC, NODE_PERFORMANCE_ENTRY_TYPE_HTTP2, NODE_PERFORMANCE_ENTRY_TYPE_HTTP, NODE_PERFORMANCE_ENTRY_TYPE_NET, NODE_PERFORMANCE_ENTRY_TYPE_DNS, }, installGarbageCollectionTracking, observerCounts, removeGarbageCollectionTracking, setupObservers, } = internalBinding('performance'); const { isPerformanceEntry, createPerformanceNodeEntry, } = require('internal/perf/performance_entry'); const { codes: { ERR_ILLEGAL_CONSTRUCTOR, ERR_INVALID_ARG_VALUE, ERR_INVALID_ARG_TYPE, ERR_MISSING_ARGS, }, } = require('internal/errors'); const { validateFunction, validateObject, validateInternalField, } = require('internal/validators'); const { customInspectSymbol: kInspect, deprecate, lazyDOMException, kEmptyObject, kEnumerableProperty, } = require('internal/util'); const { setImmediate, } = require('timers'); const { inspect } = require('util'); const { now } = require('internal/perf/utils'); const kBuffer = Symbol('kBuffer'); const kDispatch = Symbol('kDispatch'); const kMaybeBuffer = Symbol('kMaybeBuffer'); const kDeprecatedFields = Symbol('kDeprecatedFields'); const kDeprecationMessage = 'Custom PerformanceEntry accessors are deprecated. ' + 'Please use the detail property.'; const kTypeSingle = 0; const kTypeMultiple = 1; let gcTrackingInstalled = false; const kSupportedEntryTypes = ObjectFreeze([ 'dns', 'function', 'gc', 'http', 'http2', 'mark', 'measure', 'net', 'resource', ]); // Performance timeline entry Buffers let markEntryBuffer = []; let measureEntryBuffer = []; let resourceTimingBuffer = []; let resourceTimingSecondaryBuffer = []; const kPerformanceEntryBufferWarnSize = 1e6; // https://www.w3.org/TR/timing-entrytypes-registry/#registry // Default buffer limit for resource timing entries. let resourceTimingBufferSizeLimit = 250; let dispatchBufferFull; let resourceTimingBufferFullPending = false; const kClearPerformanceEntryBuffers = ObjectFreeze({ 'mark': 'performance.clearMarks', 'measure': 'performance.clearMeasures', }); const kWarnedEntryTypes = new SafeMap(); const kObservers = new SafeSet(); const kPending = new SafeSet(); let isPending = false; function queuePending() { if (isPending) return; isPending = true; setImmediate(() => { isPending = false; const pendings = ArrayFrom(kPending.values()); kPending.clear(); for (const pending of pendings) pending[kDispatch](); }); } function getObserverType(type) { switch (type) { case 'gc': return NODE_PERFORMANCE_ENTRY_TYPE_GC; case 'http2': return NODE_PERFORMANCE_ENTRY_TYPE_HTTP2; case 'http': return NODE_PERFORMANCE_ENTRY_TYPE_HTTP; case 'net': return NODE_PERFORMANCE_ENTRY_TYPE_NET; case 'dns': return NODE_PERFORMANCE_ENTRY_TYPE_DNS; } } function maybeDecrementObserverCounts(entryTypes) { for (const type of entryTypes) { const observerType = getObserverType(type); if (observerType !== undefined) { observerCounts[observerType]--; if (observerType === NODE_PERFORMANCE_ENTRY_TYPE_GC && observerCounts[observerType] === 0) { removeGarbageCollectionTracking(); gcTrackingInstalled = false; } } } } function maybeIncrementObserverCount(type) { const observerType = getObserverType(type); if (observerType !== undefined) { observerCounts[observerType]++; if (!gcTrackingInstalled && observerType === NODE_PERFORMANCE_ENTRY_TYPE_GC) { installGarbageCollectionTracking(); gcTrackingInstalled = true; } } } const kSkipThrow = Symbol('kSkipThrow'); const performanceObserverSorter = (first, second) => { return first.startTime - second.startTime; }; class PerformanceObserverEntryList { constructor(skipThrowSymbol = undefined, entries = []) { if (skipThrowSymbol !== kSkipThrow) { throw new ERR_ILLEGAL_CONSTRUCTOR(); } this[kBuffer] = ArrayPrototypeSort(entries, performanceObserverSorter); } getEntries() { validateInternalField(this, kBuffer, 'PerformanceObserverEntryList'); return ArrayPrototypeSlice(this[kBuffer]); } getEntriesByType(type) { validateInternalField(this, kBuffer, 'PerformanceObserverEntryList'); if (arguments.length === 0) { throw new ERR_MISSING_ARGS('type'); } type = `${type}`; return ArrayPrototypeFilter( this[kBuffer], (entry) => entry.entryType === type); } getEntriesByName(name, type = undefined) { validateInternalField(this, kBuffer, 'PerformanceObserverEntryList'); if (arguments.length === 0) { throw new ERR_MISSING_ARGS('name'); } name = `${name}`; if (type != null /** not nullish */) { return ArrayPrototypeFilter( this[kBuffer], (entry) => entry.name === name && entry.entryType === type); } return ArrayPrototypeFilter( this[kBuffer], (entry) => entry.name === name); } [kInspect](depth, options) { if (depth < 0) return this; const opts = { ...options, depth: options.depth == null ? null : options.depth - 1, }; return `PerformanceObserverEntryList ${inspect(this[kBuffer], opts)}`; } } ObjectDefineProperties(PerformanceObserverEntryList.prototype, { getEntries: kEnumerableProperty, getEntriesByType: kEnumerableProperty, getEntriesByName: kEnumerableProperty, [SymbolToStringTag]: { __proto__: null, writable: false, enumerable: false, configurable: true, value: 'PerformanceObserverEntryList', }, }); class PerformanceObserver { #buffer = []; #entryTypes = new SafeSet(); #type; #callback; constructor(callback) { validateFunction(callback, 'callback'); this.#callback = callback; } observe(options = kEmptyObject) { validateObject(options, 'options'); const { entryTypes, type, buffered, } = { ...options }; if (entryTypes === undefined && type === undefined) throw new ERR_MISSING_ARGS('options.entryTypes', 'options.type'); if (entryTypes != null && type != null) throw new ERR_INVALID_ARG_VALUE('options.entryTypes', entryTypes, 'options.entryTypes can not set with ' + 'options.type together'); switch (this.#type) { case undefined: if (entryTypes !== undefined) this.#type = kTypeMultiple; if (type !== undefined) this.#type = kTypeSingle; break; case kTypeSingle: if (entryTypes !== undefined) throw lazyDOMException( 'PerformanceObserver can not change to multiple observations', 'InvalidModificationError'); break; case kTypeMultiple: if (type !== undefined) throw lazyDOMException( 'PerformanceObserver can not change to single observation', 'InvalidModificationError'); break; } if (this.#type === kTypeMultiple) { if (!ArrayIsArray(entryTypes)) { throw new ERR_INVALID_ARG_TYPE( 'options.entryTypes', 'string[]', entryTypes); } maybeDecrementObserverCounts(this.#entryTypes); this.#entryTypes.clear(); for (let n = 0; n < entryTypes.length; n++) { if (ArrayPrototypeIncludes(kSupportedEntryTypes, entryTypes[n])) { this.#entryTypes.add(entryTypes[n]); maybeIncrementObserverCount(entryTypes[n]); } } } else { if (!ArrayPrototypeIncludes(kSupportedEntryTypes, type)) return; this.#entryTypes.add(type); maybeIncrementObserverCount(type); if (buffered) { const entries = filterBufferMapByNameAndType(undefined, type); ArrayPrototypePushApply(this.#buffer, entries); kPending.add(this); if (kPending.size) queuePending(); } } if (this.#entryTypes.size) kObservers.add(this); else this.disconnect(); } disconnect() { maybeDecrementObserverCounts(this.#entryTypes); kObservers.delete(this); kPending.delete(this); this.#buffer = []; this.#entryTypes.clear(); this.#type = undefined; } takeRecords() { const list = this.#buffer; this.#buffer = []; return list; } static get supportedEntryTypes() { return kSupportedEntryTypes; } [kMaybeBuffer](entry) { if (!this.#entryTypes.has(entry.entryType)) return; ArrayPrototypePush(this.#buffer, entry); kPending.add(this); if (kPending.size) queuePending(); } [kDispatch]() { const entryList = new PerformanceObserverEntryList(kSkipThrow, this.takeRecords()); this.#callback(entryList, this); } [kInspect](depth, options) { if (depth < 0) return this; const opts = { ...options, depth: options.depth == null ? null : options.depth - 1, }; return `PerformanceObserver ${inspect({ connected: kObservers.has(this), pending: kPending.has(this), entryTypes: ArrayFrom(this.#entryTypes), buffer: this.#buffer, }, opts)}`; } } ObjectDefineProperties(PerformanceObserver.prototype, { observe: kEnumerableProperty, disconnect: kEnumerableProperty, takeRecords: kEnumerableProperty, [SymbolToStringTag]: { __proto__: null, writable: false, enumerable: false, configurable: true, value: 'PerformanceObserver', }, }); /** * https://www.w3.org/TR/performance-timeline/#dfn-queue-a-performanceentry * * Add the performance entry to the interested performance observer's queue. */ function enqueue(entry) { if (!isPerformanceEntry(entry)) throw new ERR_INVALID_ARG_TYPE('entry', 'PerformanceEntry', entry); for (const obs of kObservers) { obs[kMaybeBuffer](entry); } } /** * Add the user timing entry to the global buffer. */ function bufferUserTiming(entry) { const entryType = entry.entryType; let buffer; if (entryType === 'mark') { buffer = markEntryBuffer; } else if (entryType === 'measure') { buffer = measureEntryBuffer; } else { return; } ArrayPrototypePush(buffer, entry); const count = buffer.length; if (count > kPerformanceEntryBufferWarnSize && !kWarnedEntryTypes.has(entryType)) { kWarnedEntryTypes.set(entryType, true); // No error code for this since it is a Warning // eslint-disable-next-line no-restricted-syntax const w = new Error('Possible perf_hooks memory leak detected. ' + `${count} ${entryType} entries added to the global ` + 'performance entry buffer. Use ' + `${kClearPerformanceEntryBuffers[entryType]} to ` + 'clear the buffer.'); w.name = 'MaxPerformanceEntryBufferExceededWarning'; w.entryType = entryType; w.count = count; process.emitWarning(w); } } /** * Add the resource timing entry to the global buffer if the buffer size is not * exceeding the buffer limit, or dispatch a buffer full event on the global * performance object. * * See also https://www.w3.org/TR/resource-timing-2/#dfn-add-a-performanceresourcetiming-entry */ function bufferResourceTiming(entry) { if (resourceTimingBuffer.length < resourceTimingBufferSizeLimit && !resourceTimingBufferFullPending) { ArrayPrototypePush(resourceTimingBuffer, entry); return; } if (!resourceTimingBufferFullPending) { resourceTimingBufferFullPending = true; setImmediate(() => { while (resourceTimingSecondaryBuffer.length > 0) { const excessNumberBefore = resourceTimingSecondaryBuffer.length; dispatchBufferFull('resourcetimingbufferfull'); // Calculate the number of items to be pushed to the global buffer. const numbersToPreserve = MathMax( MathMin(resourceTimingBufferSizeLimit - resourceTimingBuffer.length, resourceTimingSecondaryBuffer.length), 0, ); const excessNumberAfter = resourceTimingSecondaryBuffer.length - numbersToPreserve; for (let idx = 0; idx < numbersToPreserve; idx++) { ArrayPrototypePush(resourceTimingBuffer, resourceTimingSecondaryBuffer[idx]); } if (excessNumberBefore <= excessNumberAfter) { resourceTimingSecondaryBuffer = []; } } resourceTimingBufferFullPending = false; }); } ArrayPrototypePush(resourceTimingSecondaryBuffer, entry); } // https://w3c.github.io/resource-timing/#dom-performance-setresourcetimingbuffersize function setResourceTimingBufferSize(maxSize) { // If the maxSize parameter is less than resource timing buffer current // size, no PerformanceResourceTiming objects are to be removed from the // performance entry buffer. resourceTimingBufferSizeLimit = maxSize; } function setDispatchBufferFull(fn) { dispatchBufferFull = fn; } function clearEntriesFromBuffer(type, name) { if (type !== 'mark' && type !== 'measure' && type !== 'resource') { return; } if (type === 'mark') { markEntryBuffer = name === undefined ? [] : ArrayPrototypeFilter(markEntryBuffer, (entry) => entry.name !== name); } else if (type === 'measure') { measureEntryBuffer = name === undefined ? [] : ArrayPrototypeFilter(measureEntryBuffer, (entry) => entry.name !== name); } else { resourceTimingBuffer = name === undefined ? [] : ArrayPrototypeFilter(resourceTimingBuffer, (entry) => entry.name !== name); } } function filterBufferMapByNameAndType(name, type) { let bufferList; if (type === 'mark') { bufferList = markEntryBuffer; } else if (type === 'measure') { bufferList = measureEntryBuffer; } else if (type === 'resource') { bufferList = resourceTimingBuffer; } else if (type !== undefined) { // Unrecognized type; return []; } else { bufferList = []; ArrayPrototypePushApply(bufferList, markEntryBuffer); ArrayPrototypePushApply(bufferList, measureEntryBuffer); ArrayPrototypePushApply(bufferList, resourceTimingBuffer); } if (name !== undefined) { bufferList = ArrayPrototypeFilter(bufferList, (buffer) => buffer.name === name); } else if (type !== undefined) { bufferList = ArrayPrototypeSlice(bufferList); } return ArrayPrototypeSort(bufferList, performanceObserverSorter); } function observerCallback(name, type, startTime, duration, details) { const entry = createPerformanceNodeEntry( name, type, startTime, duration, details); if (details !== undefined) { // GC, HTTP2, and HTTP PerformanceEntry used additional // properties directly off the entry. Those have been // moved into the details property. The existing accessors // are still included but are deprecated. entry[kDeprecatedFields] = new SafeMap(); const detailKeys = ObjectKeys(details); const props = {}; for (let n = 0; n < detailKeys.length; n++) { const key = detailKeys[n]; entry[kDeprecatedFields].set(key, details[key]); props[key] = { configurable: true, enumerable: true, get: deprecate(() => { return entry[kDeprecatedFields].get(key); }, kDeprecationMessage, 'DEP0152'), set: deprecate((value) => { entry[kDeprecatedFields].set(key, value); }, kDeprecationMessage, 'DEP0152'), }; } ObjectDefineProperties(entry, props); } enqueue(entry); } setupObservers(observerCallback); function hasObserver(type) { const observerType = getObserverType(type); return observerCounts[observerType] > 0; } function startPerf(target, key, context = {}) { target[key] = { ...context, startTime: now(), }; } function stopPerf(target, key, context = {}) { const ctx = target[key]; if (!ctx) { return; } const startTime = ctx.startTime; const entry = createPerformanceNodeEntry( ctx.name, ctx.type, startTime, now() - startTime, { ...ctx.detail, ...context.detail }, ); enqueue(entry); } module.exports = { PerformanceObserver, PerformanceObserverEntryList, enqueue, hasObserver, clearEntriesFromBuffer, filterBufferMapByNameAndType, startPerf, stopPerf, bufferUserTiming, bufferResourceTiming, setResourceTimingBufferSize, setDispatchBufferFull, };