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

perf_hooks: create clearResourceTimings perf_hooks: add resourcetiming test parallel perf_hooks: add markResourceTiming perf_hooks: fix observable when using resource perf_hooks: fix observable when using resource perf_hooks: add class comments perf_hooks: add PerformanceResourceTiming perf_hooks: create clearResourceTimings perf_hooks: add resourcetiming test parallel perf_hooks: add markResourceTiming perf_hooks: fix observable when using resource perf_hooks: fix observable when using resource perf_hooks: add class comments perf_hooks: add Resource Timing documentation benchmark: measure resource timing module perf_hooks: add check avoiding new PerformanceResourceTiming perf_hooks: adjust doc PR-URL: https://github.com/nodejs/node/pull/42725 Fixes: https://github.com/nodejs/undici/issues/952 Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
495 lines
12 KiB
JavaScript
495 lines
12 KiB
JavaScript
'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',
|
|
'resource',
|
|
]);
|
|
|
|
// Performance timeline entry Buffers
|
|
let markEntryBuffer = [];
|
|
let measureEntryBuffer = [];
|
|
let resourceTimingBuffer = [];
|
|
const kMaxPerformanceEntryBuffers = 1e6;
|
|
const kClearPerformanceEntryBuffers = ObjectFreeze({
|
|
'mark': 'performance.clearMarks',
|
|
'measure': 'performance.clearMeasures',
|
|
'resource': 'performance.clearResourceTimings',
|
|
});
|
|
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 if (entryType === 'resource') {
|
|
buffer = resourceTimingBuffer;
|
|
} 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' && 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 = ArrayPrototypeConcat(markEntryBuffer, measureEntryBuffer, resourceTimingBuffer);
|
|
}
|
|
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,
|
|
};
|