'use strict'; const { ArrayFrom, ArrayIsArray, ArrayPrototypeFilter, ArrayPrototypeIncludes, ArrayPrototypePush, ArrayPrototypePushApply, ArrayPrototypeSlice, ArrayPrototypeSort, ArrayPrototypeConcat, Error, ObjectDefineProperties, ObjectFreeze, ObjectKeys, SafeMap, SafeSet, Symbol, } = 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 { InternalPerformanceEntry, isPerformanceEntry, } = require('internal/perf/performance_entry'); const { codes: { ERR_INVALID_ARG_VALUE, ERR_INVALID_ARG_TYPE, ERR_MISSING_ARGS, }, } = require('internal/errors'); const { validateFunction, validateObject, } = require('internal/validators'); const { customInspectSymbol: kInspect, deprecate, lazyDOMException, } = require('internal/util'); const { setImmediate, } = require('timers'); const { inspect } = require('util'); 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', ]); // Performance timeline entry Buffers let markEntryBuffer = []; let measureEntryBuffer = []; const kMaxPerformanceEntryBuffers = 1e6; 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; } } } class PerformanceObserverEntryList { #buffer = []; constructor(entries) { this.#buffer = ArrayPrototypeSort(entries, (first, second) => { return first.startTime - second.startTime; }); } getEntries() { return ArrayPrototypeSlice(this.#buffer); } getEntriesByType(type) { type = `${type}`; return ArrayPrototypeFilter( this.#buffer, (entry) => entry.entryType === type); } getEntriesByName(name, type) { name = `${name}`; if (type != null /** not nullish */) { return ArrayPrototypeFilter( this.#buffer, (entry) => entry.name === name && entry.entryType === type); } return ArrayPrototypeFilter( this.#buffer, (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.#buffer, opts)}`; } } class PerformanceObserver { #buffer = []; #entryTypes = new SafeSet(); #type; #callback; constructor(callback) { validateFunction(callback, 'callback'); this.#callback = callback; } observe(options = {}) { 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]() { this.#callback(new PerformanceObserverEntryList(this.takeRecords()), 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)}`; } } function enqueue(entry) { if (!isPerformanceEntry(entry)) throw new ERR_INVALID_ARG_TYPE('entry', 'PerformanceEntry', entry); for (const obs of kObservers) { obs[kMaybeBuffer](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 > kMaxPerformanceEntryBuffers && !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); } } function clearEntriesFromBuffer(type, name) { if (type !== 'mark' && type !== 'measure') { return; } if (type === 'mark') { markEntryBuffer = name === undefined ? [] : ArrayPrototypeFilter(markEntryBuffer, (entry) => entry.name !== name); } else { measureEntryBuffer = name === undefined ? [] : ArrayPrototypeFilter(measureEntryBuffer, (entry) => entry.name !== name); } } function filterBufferMapByNameAndType(name, type) { let bufferList; if (type === 'mark') { bufferList = markEntryBuffer; } else if (type === 'measure') { bufferList = measureEntryBuffer; } else if (type !== undefined) { // Unrecognized type; return []; } else { bufferList = ArrayPrototypeConcat(markEntryBuffer, measureEntryBuffer); } if (name !== undefined) { bufferList = ArrayPrototypeFilter(bufferList, (buffer) => buffer.name === name); } else if (type !== undefined) { bufferList = ArrayPrototypeSlice(bufferList); } return ArrayPrototypeSort(bufferList, (first, second) => { return first.startTime - second.startTime; }); } function observerCallback(name, type, startTime, duration, details) { const entry = new InternalPerformanceEntry( 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 = {}) { if (hasObserver(context.type)) { target[key] = { ...context, startTime: process.hrtime(), }; } } function stopPerf(target, key, context = {}) { const ctx = target[key]; if (ctx && hasObserver(ctx.type)) { const startTime = ctx.startTime; const diff = process.hrtime(startTime); const entry = new InternalPerformanceEntry( ctx.name, ctx.type, startTime[0] * 1000 + startTime[1] / 1e6, diff[0] * 1000 + diff[1] / 1e6, { ...ctx.detail, ...context.detail }, ); enqueue(entry); } } module.exports = { PerformanceObserver, PerformanceObserverEntryList, enqueue, hasObserver, clearEntriesFromBuffer, filterBufferMapByNameAndType, startPerf, stopPerf, };