nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
638 lines (570 loc) • 17.1 kB
JavaScript
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/perf/observe.js
import {
constants as __constants__,
installGarbageCollectionTracking,
observerCounts,
removeGarbageCollectionTracking,
setupObservers,
} from "nstdlib/stub/binding/performance";
import {
isPerformanceEntry,
createPerformanceNodeEntry,
} from "nstdlib/lib/internal/perf/performance_entry";
import { codes as __codes__ } from "nstdlib/lib/internal/errors";
import {
validateFunction,
validateObject,
validateInternalField,
} from "nstdlib/lib/internal/validators";
import {
customInspectSymbol as kInspect,
deprecate,
lazyDOMException,
kEmptyObject,
kEnumerableProperty,
} from "nstdlib/lib/internal/util";
import { setImmediate } from "nstdlib/lib/timers";
import { inspect } from "nstdlib/lib/util";
import { now } from "nstdlib/lib/internal/perf/utils";
const {
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,
} = __constants__;
const {
ERR_ILLEGAL_CONSTRUCTOR,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_MISSING_ARGS,
} = __codes__;
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 = Object.freeze([
"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 = Object.freeze({
mark: "performance.clearMarks",
measure: "performance.clearMeasures",
});
const kWarnedEntryTypes = new Map();
const kObservers = new Set();
const kPending = new Set();
let isPending = false;
function queuePending() {
if (isPending) return;
isPending = true;
setImmediate(() => {
isPending = false;
const pendings = Array.from(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] = Array.prototype.sort.call(
entries,
performanceObserverSorter,
);
}
getEntries() {
validateInternalField(this, kBuffer, "PerformanceObserverEntryList");
return Array.prototype.slice.call(this[kBuffer]);
}
getEntriesByType(type) {
validateInternalField(this, kBuffer, "PerformanceObserverEntryList");
if (arguments.length === 0) {
throw new ERR_MISSING_ARGS("type");
}
type = `${type}`;
return Array.prototype.filter.call(
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 Array.prototype.filter.call(
this[kBuffer],
(entry) => entry.name === name && entry.entryType === type,
);
}
return Array.prototype.filter.call(
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)}`;
}
}
Object.defineProperties(PerformanceObserverEntryList.prototype, {
getEntries: kEnumerableProperty,
getEntriesByType: kEnumerableProperty,
getEntriesByName: kEnumerableProperty,
[Symbol.toStringTag]: {
__proto__: null,
writable: false,
enumerable: false,
configurable: true,
value: "PerformanceObserverEntryList",
},
});
class PerformanceObserver {
#buffer = [];
#entryTypes = new Set();
#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 (!Array.isArray(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 (
Array.prototype.includes.call(kSupportedEntryTypes, entryTypes[n])
) {
this.#entryTypes.add(entryTypes[n]);
maybeIncrementObserverCount(entryTypes[n]);
}
}
} else {
if (!Array.prototype.includes.call(kSupportedEntryTypes, type)) return;
this.#entryTypes.add(type);
maybeIncrementObserverCount(type);
if (buffered) {
const entries = filterBufferMapByNameAndType(undefined, type);
Array.prototype.push.apply(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;
Array.prototype.push.call(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: Array.from(this.#entryTypes),
buffer: this.#buffer,
},
opts,
)}`;
}
}
Object.defineProperties(PerformanceObserver.prototype, {
observe: kEnumerableProperty,
disconnect: kEnumerableProperty,
takeRecords: kEnumerableProperty,
[Symbol.toStringTag]: {
__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;
}
Array.prototype.push.call(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
) {
Array.prototype.push.call(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 = Math.max(
Math.min(
resourceTimingBufferSizeLimit - resourceTimingBuffer.length,
resourceTimingSecondaryBuffer.length,
),
0,
);
const excessNumberAfter =
resourceTimingSecondaryBuffer.length - numbersToPreserve;
for (let idx = 0; idx < numbersToPreserve; idx++) {
Array.prototype.push.call(
resourceTimingBuffer,
resourceTimingSecondaryBuffer[idx],
);
}
if (excessNumberBefore <= excessNumberAfter) {
resourceTimingSecondaryBuffer = [];
}
}
resourceTimingBufferFullPending = false;
});
}
Array.prototype.push.call(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
? []
: Array.prototype.filter.call(
markEntryBuffer,
(entry) => entry.name !== name,
);
} else if (type === "measure") {
measureEntryBuffer =
name === undefined
? []
: Array.prototype.filter.call(
measureEntryBuffer,
(entry) => entry.name !== name,
);
} else {
resourceTimingBuffer =
name === undefined
? []
: Array.prototype.filter.call(
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 = [];
Array.prototype.push.apply(bufferList, markEntryBuffer);
Array.prototype.push.apply(bufferList, measureEntryBuffer);
Array.prototype.push.apply(bufferList, resourceTimingBuffer);
}
if (name !== undefined) {
bufferList = Array.prototype.filter.call(
bufferList,
(buffer) => buffer.name === name,
);
} else if (type !== undefined) {
bufferList = Array.prototype.slice.call(bufferList);
}
return Array.prototype.sort.call(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 Map();
const detailKeys = Object.keys(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",
),
};
}
Object.defineProperties(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);
}
export { PerformanceObserver };
export { PerformanceObserverEntryList };
export { enqueue };
export { hasObserver };
export { clearEntriesFromBuffer };
export { filterBufferMapByNameAndType };
export { startPerf };
export { stopPerf };
export { bufferUserTiming };
export { bufferResourceTiming };
export { setResourceTimingBufferSize };
export { setDispatchBufferFull };