'use strict'; const { ArrayFrom, ArrayIsArray, ArrayPrototypeFilter, ArrayPrototypeFlatMap, ArrayPrototypeIncludes, ArrayPrototypePush, ArrayPrototypePushApply, ArrayPrototypeSlice, ArrayPrototypeSort, 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, }, installGarbageCollectionTracking, observerCounts, removeGarbageCollectionTracking, setupObservers, } = internalBinding('performance'); const { InternalPerformanceEntry, isPerformanceEntry, kBufferNext, } = require('internal/perf/performance_entry'); const { codes: { ERR_INVALID_ARG_VALUE, ERR_INVALID_ARG_TYPE, ERR_MISSING_ARGS, }, } = require('internal/errors'); const { validateCallback, validateObject, } = require('internal/validators'); const { customInspectSymbol: kInspect, deprecate, lazyDOMException, } = require('internal/util'); const { setImmediate, } = require('timers'); const { inspect } = require('util'); const kBuffer = Symbol('kBuffer'); const kCallback = Symbol('kCallback'); const kDispatch = Symbol('kDispatch'); const kEntryTypes = Symbol('kEntryTypes'); const kMaybeBuffer = Symbol('kMaybeBuffer'); const kDeprecatedFields = Symbol('kDeprecatedFields'); const kType = Symbol('kType'); const kDeprecationMessage = 'Custom PerformanceEntry accessors are deprecated. ' + 'Please use the detail property.'; const kTypeSingle = 0; const kTypeMultiple = 1; let gcTrackingInstalled = false; const kSupportedEntryTypes = ObjectFreeze([ 'function', 'gc', 'http', 'http2', 'mark', 'measure', ]); // Performance timeline entry Buffers const markEntryBuffer = createBuffer(); const measureEntryBuffer = createBuffer(); 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; } } 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(); } } } } 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 { constructor(entries) { this[kBuffer] = ArrayPrototypeSort(entries, (first, second) => { if (first.startTime < second.startTime) return -1; if (first.startTime > second.startTime) return 1; return 0; }); } getEntries() { return ArrayPrototypeSlice(this[kBuffer]); } getEntriesByType(type) { type = `${type}`; return ArrayPrototypeFilter( this[kBuffer], (entry) => entry.entryType === type); } getEntriesByName(name, type) { 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)}`; } } class PerformanceObserver { constructor(callback) { // TODO(joyeecheung): V8 snapshot does not support instance member // initializers for now: // https://bugs.chromium.org/p/v8/issues/detail?id=10704 this[kBuffer] = []; this[kEntryTypes] = new SafeSet(); this[kType] = undefined; validateCallback(callback); this[kCallback] = 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[kType]) { case undefined: if (entryTypes !== undefined) this[kType] = kTypeMultiple; if (type !== undefined) this[kType] = 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[kType] === kTypeMultiple) { if (!ArrayIsArray(entryTypes)) { throw new ERR_INVALID_ARG_TYPE( 'options.entryTypes', 'string[]', entryTypes); } maybeDecrementObserverCounts(this[kEntryTypes]); this[kEntryTypes].clear(); for (let n = 0; n < entryTypes.length; n++) { if (ArrayPrototypeIncludes(kSupportedEntryTypes, entryTypes[n])) { this[kEntryTypes].add(entryTypes[n]); maybeIncrementObserverCount(entryTypes[n]); } } } else { if (!ArrayPrototypeIncludes(kSupportedEntryTypes, type)) return; this[kEntryTypes].add(type); maybeIncrementObserverCount(type); if (buffered) { const entries = filterBufferMapByNameAndType(undefined, type); ArrayPrototypePushApply(this[kBuffer], entries); kPending.add(this); if (kPending.size) queuePending(); } } if (this[kEntryTypes].size) kObservers.add(this); else this.disconnect(); } disconnect() { maybeDecrementObserverCounts(this[kEntryTypes]); kObservers.delete(this); kPending.delete(this); this[kBuffer] = []; this[kEntryTypes].clear(); this[kType] = undefined; } takeRecords() { const list = this[kBuffer]; this[kBuffer] = []; return list; } static get supportedEntryTypes() { return kSupportedEntryTypes; } [kMaybeBuffer](entry) { if (!this[kEntryTypes].has(entry.entryType)) return; ArrayPrototypePush(this[kBuffer], entry); kPending.add(this); if (kPending.size) queuePending(); } [kDispatch]() { this[kCallback](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[kEntryTypes]), buffer: this[kBuffer], }, 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; } const count = buffer.count + 1; buffer.count = count; if (count === 1) { buffer.head = entry; buffer.tail = entry; return; } buffer.tail[kBufferNext] = entry; buffer.tail = entry; 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) { let buffer; if (type === 'mark') { buffer = markEntryBuffer; } else if (type === 'measure') { buffer = measureEntryBuffer; } else { return; } if (name === undefined) { resetBuffer(buffer); return; } let head = null; let tail = null; let count = 0; for (let entry = buffer.head; entry !== null; entry = entry[kBufferNext]) { if (entry.name !== name) { head = head ?? entry; tail = entry; continue; } if (tail === null) { continue; } tail[kBufferNext] = entry[kBufferNext]; count++; } buffer.head = head; buffer.tail = tail; buffer.count = count; } 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 = [markEntryBuffer, measureEntryBuffer]; } return ArrayPrototypeFlatMap(bufferList, (buffer) => filterBufferByName(buffer, name)); } function filterBufferByName(buffer, name) { const arr = []; for (let entry = buffer.head; entry !== null; entry = entry[kBufferNext]) { if (name === undefined || entry.name === name) { ArrayPrototypePush(arr, entry); } } return arr; } 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 createBuffer() { return { head: null, tail: null, count: 0, }; } function resetBuffer(buffer) { buffer.head = null; buffer.tail = null; buffer.count = 0; } module.exports = { PerformanceObserver, PerformanceObserverEntryList, enqueue, hasObserver, clearEntriesFromBuffer, filterBufferMapByNameAndType, };