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

Signed-off-by: James M Snell <jasnell@gmail.com> Fixes: https://github.com/nodejs/node/issues/39548 PR-URL: https://github.com/nodejs/node/pull/39550 Reviewed-By: Bryan English <bryan@bryanenglish.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
501 lines
12 KiB
JavaScript
501 lines
12 KiB
JavaScript
'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();
|
|
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 {
|
|
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,
|
|
};
|