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

* Update the user timing implementation to conform to User Timing Level 3. * Reimplement user timing and timerify with pure JavaScript implementations * Simplify the C++ implementation for gc and http2 perf * Runtime deprecate additional perf entry properties in favor of the standard detail argument * Disable the `buffered` option on PerformanceObserver, all entries are queued and dispatched on setImmediate. Only entries with active observers are buffered. * This does remove the user timing and timerify trace events. Because the trace_events are still considered experimental, those can be removed without a deprecation cycle. They are removed to improve performance and reduce complexity. Old: `perf_hooks/usertiming.js n=100000: 92,378.01249733355` New: perf_hooks/usertiming.js n=100000: 270,393.5280638482` PR-URL: https://github.com/nodejs/node/pull/37136 Refs: https://github.com/nodejs/diagnostics/issues/464 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Michaël Zasso <targos@protonmail.com>
353 lines
8.3 KiB
JavaScript
353 lines
8.3 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
ArrayFrom,
|
|
ArrayIsArray,
|
|
ArrayPrototypeFilter,
|
|
ArrayPrototypeIncludes,
|
|
ArrayPrototypePush,
|
|
ArrayPrototypeSlice,
|
|
ArrayPrototypeSort,
|
|
ObjectDefineProperties,
|
|
ObjectFreeze,
|
|
ObjectKeys,
|
|
SafeMap,
|
|
SafeSet,
|
|
Symbol,
|
|
} = primordials;
|
|
|
|
const {
|
|
constants: {
|
|
NODE_PERFORMANCE_ENTRY_TYPE_GC,
|
|
NODE_PERFORMANCE_ENTRY_TYPE_FUNCTION,
|
|
NODE_PERFORMANCE_ENTRY_TYPE_HTTP2,
|
|
NODE_PERFORMANCE_ENTRY_TYPE_HTTP,
|
|
},
|
|
installGarbageCollectionTracking,
|
|
observerCounts,
|
|
removeGarbageCollectionTracking,
|
|
setupObservers,
|
|
} = internalBinding('performance');
|
|
|
|
const {
|
|
InternalPerformanceEntry,
|
|
isPerformanceEntry,
|
|
} = require('internal/perf/perf');
|
|
|
|
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,
|
|
} = 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;
|
|
|
|
const kSupportedEntryTypes = ObjectFreeze([
|
|
'function',
|
|
'gc',
|
|
'http',
|
|
'http2',
|
|
'mark',
|
|
'measure',
|
|
]);
|
|
|
|
const kObservers = new SafeSet();
|
|
const kPending = new SafeSet();
|
|
let isPending = false;
|
|
|
|
function queuePending() {
|
|
if (isPending) return;
|
|
isPending = true;
|
|
setImmediate(() => {
|
|
isPending = false;
|
|
for (const pending of kPending)
|
|
pending[kDispatch]();
|
|
kPending.clear();
|
|
});
|
|
}
|
|
|
|
function maybeDecrementObserverCounts(entryTypes) {
|
|
for (const type of entryTypes) {
|
|
let observerType;
|
|
switch (type) {
|
|
case 'gc':
|
|
observerType = NODE_PERFORMANCE_ENTRY_TYPE_GC;
|
|
break;
|
|
case 'function':
|
|
observerType = NODE_PERFORMANCE_ENTRY_TYPE_FUNCTION;
|
|
break;
|
|
case 'http2':
|
|
observerType = NODE_PERFORMANCE_ENTRY_TYPE_HTTP2;
|
|
break;
|
|
case 'http':
|
|
observerType = NODE_PERFORMANCE_ENTRY_TYPE_HTTP;
|
|
break;
|
|
}
|
|
if (observerType !== undefined) {
|
|
observerCounts[observerType]--;
|
|
|
|
if (observerType === NODE_PERFORMANCE_ENTRY_TYPE_GC &&
|
|
observerCounts[observerType] === 0) {
|
|
removeGarbageCollectionTracking();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function maybeIncrementObserverCount(type) {
|
|
let observerType;
|
|
switch (type) {
|
|
case 'gc':
|
|
observerType = NODE_PERFORMANCE_ENTRY_TYPE_GC;
|
|
break;
|
|
case 'function':
|
|
observerType = NODE_PERFORMANCE_ENTRY_TYPE_FUNCTION;
|
|
break;
|
|
case 'http2':
|
|
observerType = NODE_PERFORMANCE_ENTRY_TYPE_HTTP2;
|
|
break;
|
|
case 'http':
|
|
observerType = NODE_PERFORMANCE_ENTRY_TYPE_HTTP;
|
|
break;
|
|
}
|
|
if (observerType !== undefined) {
|
|
observerCounts[observerType]++;
|
|
|
|
if (observerType === NODE_PERFORMANCE_ENTRY_TYPE_GC)
|
|
installGarbageCollectionTracking();
|
|
}
|
|
}
|
|
|
|
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) {
|
|
name = `${name}`;
|
|
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 {
|
|
[kBuffer] = [];
|
|
[kEntryTypes] = new SafeSet();
|
|
[kType] = undefined;
|
|
|
|
constructor(callback) {
|
|
validateCallback(callback);
|
|
this[kCallback] = callback;
|
|
}
|
|
|
|
observe(options = {}) {
|
|
validateObject(options, 'options');
|
|
const {
|
|
entryTypes,
|
|
type,
|
|
} = { ...options };
|
|
if (entryTypes === undefined && type === undefined)
|
|
throw new ERR_MISSING_ARGS('options.entryTypes', 'options.type');
|
|
|
|
switch (this[kType]) {
|
|
case undefined:
|
|
if (entryTypes !== undefined) this[kType] = kTypeMultiple;
|
|
if (type !== undefined) this[kType] = kTypeSingle;
|
|
break;
|
|
case kTypeSingle:
|
|
if (entryTypes !== undefined)
|
|
throw new ERR_INVALID_ARG_VALUE('options.entryTypes', entryTypes);
|
|
break;
|
|
case kTypeMultiple:
|
|
if (type !== undefined)
|
|
throw new ERR_INVALID_ARG_VALUE('options.type', type);
|
|
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 (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 new PerformanceObserverEntryList(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](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);
|
|
}
|
|
}
|
|
|
|
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, 'DEP0XXX'),
|
|
set: deprecate((value) => {
|
|
entry[kDeprecatedFields].set(key, value);
|
|
}, kDeprecationMessage, 'DEP0XXX'),
|
|
};
|
|
}
|
|
ObjectDefineProperties(entry, props);
|
|
}
|
|
|
|
enqueue(entry);
|
|
}
|
|
|
|
setupObservers(observerCallback);
|
|
|
|
module.exports = {
|
|
PerformanceObserver,
|
|
enqueue,
|
|
};
|