mirror of
https://github.com/nodejs/node.git
synced 2025-05-03 02:06:12 +00:00

PR-URL: https://github.com/nodejs/node/pull/50008 Reviewed-By: Stephen Belanger <admin@stephenbelanger.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
614 lines
16 KiB
JavaScript
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,
|
|
};
|