node/lib/internal/perf/observe.js
legendecas a27d245698
perf_hooks: fix performance timeline wpt failures
PR-URL: https://github.com/nodejs/node/pull/39532
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Khaidi Chu <i@2333.moe>
2021-07-30 07:29:44 -07:00

500 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();
}
}
}
}
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,
};