node/lib/internal/perf/observe.js
Vinicius Lourenço 328bdac7f0
perf_hooks: reducing overhead of performance observer entry list
PR-URL: https://github.com/nodejs/node/pull/50008
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
2023-10-04 00:38:32 +00:00

614 lines
16 KiB
JavaScript

'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,
};