UNPKG

nstdlib-nightly

Version:

Node.js standard library converted to runtime-agnostic ES modules.

638 lines (570 loc) 17.1 kB
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/perf/observe.js import { constants as __constants__, installGarbageCollectionTracking, observerCounts, removeGarbageCollectionTracking, setupObservers, } from "nstdlib/stub/binding/performance"; import { isPerformanceEntry, createPerformanceNodeEntry, } from "nstdlib/lib/internal/perf/performance_entry"; import { codes as __codes__ } from "nstdlib/lib/internal/errors"; import { validateFunction, validateObject, validateInternalField, } from "nstdlib/lib/internal/validators"; import { customInspectSymbol as kInspect, deprecate, lazyDOMException, kEmptyObject, kEnumerableProperty, } from "nstdlib/lib/internal/util"; import { setImmediate } from "nstdlib/lib/timers"; import { inspect } from "nstdlib/lib/util"; import { now } from "nstdlib/lib/internal/perf/utils"; const { 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, } = __constants__; const { ERR_ILLEGAL_CONSTRUCTOR, ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_MISSING_ARGS, } = __codes__; 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 = Object.freeze([ "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 = Object.freeze({ mark: "performance.clearMarks", measure: "performance.clearMeasures", }); const kWarnedEntryTypes = new Map(); const kObservers = new Set(); const kPending = new Set(); let isPending = false; function queuePending() { if (isPending) return; isPending = true; setImmediate(() => { isPending = false; const pendings = Array.from(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] = Array.prototype.sort.call( entries, performanceObserverSorter, ); } getEntries() { validateInternalField(this, kBuffer, "PerformanceObserverEntryList"); return Array.prototype.slice.call(this[kBuffer]); } getEntriesByType(type) { validateInternalField(this, kBuffer, "PerformanceObserverEntryList"); if (arguments.length === 0) { throw new ERR_MISSING_ARGS("type"); } type = `${type}`; return Array.prototype.filter.call( 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 Array.prototype.filter.call( this[kBuffer], (entry) => entry.name === name && entry.entryType === type, ); } return Array.prototype.filter.call( 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)}`; } } Object.defineProperties(PerformanceObserverEntryList.prototype, { getEntries: kEnumerableProperty, getEntriesByType: kEnumerableProperty, getEntriesByName: kEnumerableProperty, [Symbol.toStringTag]: { __proto__: null, writable: false, enumerable: false, configurable: true, value: "PerformanceObserverEntryList", }, }); class PerformanceObserver { #buffer = []; #entryTypes = new Set(); #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 (!Array.isArray(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 ( Array.prototype.includes.call(kSupportedEntryTypes, entryTypes[n]) ) { this.#entryTypes.add(entryTypes[n]); maybeIncrementObserverCount(entryTypes[n]); } } } else { if (!Array.prototype.includes.call(kSupportedEntryTypes, type)) return; this.#entryTypes.add(type); maybeIncrementObserverCount(type); if (buffered) { const entries = filterBufferMapByNameAndType(undefined, type); Array.prototype.push.apply(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; Array.prototype.push.call(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: Array.from(this.#entryTypes), buffer: this.#buffer, }, opts, )}`; } } Object.defineProperties(PerformanceObserver.prototype, { observe: kEnumerableProperty, disconnect: kEnumerableProperty, takeRecords: kEnumerableProperty, [Symbol.toStringTag]: { __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; } Array.prototype.push.call(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 ) { Array.prototype.push.call(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 = Math.max( Math.min( resourceTimingBufferSizeLimit - resourceTimingBuffer.length, resourceTimingSecondaryBuffer.length, ), 0, ); const excessNumberAfter = resourceTimingSecondaryBuffer.length - numbersToPreserve; for (let idx = 0; idx < numbersToPreserve; idx++) { Array.prototype.push.call( resourceTimingBuffer, resourceTimingSecondaryBuffer[idx], ); } if (excessNumberBefore <= excessNumberAfter) { resourceTimingSecondaryBuffer = []; } } resourceTimingBufferFullPending = false; }); } Array.prototype.push.call(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 ? [] : Array.prototype.filter.call( markEntryBuffer, (entry) => entry.name !== name, ); } else if (type === "measure") { measureEntryBuffer = name === undefined ? [] : Array.prototype.filter.call( measureEntryBuffer, (entry) => entry.name !== name, ); } else { resourceTimingBuffer = name === undefined ? [] : Array.prototype.filter.call( 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 = []; Array.prototype.push.apply(bufferList, markEntryBuffer); Array.prototype.push.apply(bufferList, measureEntryBuffer); Array.prototype.push.apply(bufferList, resourceTimingBuffer); } if (name !== undefined) { bufferList = Array.prototype.filter.call( bufferList, (buffer) => buffer.name === name, ); } else if (type !== undefined) { bufferList = Array.prototype.slice.call(bufferList); } return Array.prototype.sort.call(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 Map(); const detailKeys = Object.keys(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", ), }; } Object.defineProperties(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); } export { PerformanceObserver }; export { PerformanceObserverEntryList }; export { enqueue }; export { hasObserver }; export { clearEntriesFromBuffer }; export { filterBufferMapByNameAndType }; export { startPerf }; export { stopPerf }; export { bufferUserTiming }; export { bufferResourceTiming }; export { setResourceTimingBufferSize }; export { setDispatchBufferFull };