mirror of
https://github.com/zebrajr/node.git
synced 2026-01-15 12:15:26 +00:00
perf_hooks: add resourcetiming buffer limit
Add WebPerf API `performance.setResourceTimingBufferSize` and event `'resourcetimingbufferfull'` support. The resource timing entries are added to the global performance timeline buffer automatically when using fetch. If users are not proactively cleaning these events, it can grow without limit. Apply the https://www.w3.org/TR/timing-entrytypes-registry/ default resource timing buffer max size so that the buffer can be limited to not grow indefinitely. PR-URL: https://github.com/nodejs/node/pull/44220 Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
This commit is contained in:
committed by
legendecas
parent
0d46cf6af8
commit
798a6edddf
@@ -312,6 +312,17 @@ added: v8.5.0
|
||||
Returns the current high resolution millisecond timestamp, where 0 represents
|
||||
the start of the current `node` process.
|
||||
|
||||
### `performance.setResourceTimingBufferSize(maxSize)`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
Sets the global performance resource timing buffer size to the specified number
|
||||
of "resource" type performance entry objects.
|
||||
|
||||
By default the max buffer size is set to 250.
|
||||
|
||||
### `performance.timeOrigin`
|
||||
|
||||
<!-- YAML
|
||||
@@ -387,6 +398,18 @@ added: v16.1.0
|
||||
An object which is JSON representation of the `performance` object. It
|
||||
is similar to [`window.performance.toJSON`][] in browsers.
|
||||
|
||||
#### Event: `'resourcetimingbufferfull'`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
The `'resourcetimingbufferfull'` event is fired when the global performance
|
||||
resource timing buffer is full. Adjust resource timing buffer size with
|
||||
`performance.setResourceTimingBufferSize()` or clear the buffer with
|
||||
`performance.clearResourceTimings()` in the event listener to allow
|
||||
more entries to be added to the performance timeline buffer.
|
||||
|
||||
## Class: `PerformanceEntry`
|
||||
|
||||
<!-- YAML
|
||||
|
||||
@@ -73,8 +73,10 @@ defineOperation(globalThis, 'btoa', buffer.btoa);
|
||||
exposeInterface(globalThis, 'Blob', buffer.Blob);
|
||||
|
||||
// https://www.w3.org/TR/hr-time-2/#the-performance-attribute
|
||||
const perf_hooks = require('perf_hooks');
|
||||
exposeInterface(globalThis, 'Performance', perf_hooks.Performance);
|
||||
defineReplacableAttribute(globalThis, 'performance',
|
||||
require('perf_hooks').performance);
|
||||
perf_hooks.performance);
|
||||
|
||||
function createGlobalConsole() {
|
||||
const consoleFromNode =
|
||||
|
||||
@@ -11,6 +11,8 @@ const {
|
||||
ArrayPrototypeSort,
|
||||
ArrayPrototypeConcat,
|
||||
Error,
|
||||
MathMax,
|
||||
MathMin,
|
||||
ObjectDefineProperties,
|
||||
ObjectFreeze,
|
||||
ObjectKeys,
|
||||
@@ -95,11 +97,17 @@ const kSupportedEntryTypes = ObjectFreeze([
|
||||
let markEntryBuffer = [];
|
||||
let measureEntryBuffer = [];
|
||||
let resourceTimingBuffer = [];
|
||||
const kMaxPerformanceEntryBuffers = 1e6;
|
||||
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 = ObjectFreeze({
|
||||
'mark': 'performance.clearMarks',
|
||||
'measure': 'performance.clearMeasures',
|
||||
'resource': 'performance.clearResourceTimings',
|
||||
});
|
||||
const kWarnedEntryTypes = new SafeMap();
|
||||
|
||||
@@ -332,6 +340,11 @@ class 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);
|
||||
@@ -339,15 +352,18 @@ function enqueue(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 if (entryType === 'resource') {
|
||||
buffer = resourceTimingBuffer;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -355,7 +371,7 @@ function enqueue(entry) {
|
||||
ArrayPrototypePush(buffer, entry);
|
||||
const count = buffer.length;
|
||||
|
||||
if (count > kMaxPerformanceEntryBuffers &&
|
||||
if (count > kPerformanceEntryBufferWarnSize &&
|
||||
!kWarnedEntryTypes.has(entryType)) {
|
||||
kWarnedEntryTypes.set(entryType, true);
|
||||
// No error code for this since it is a Warning
|
||||
@@ -372,6 +388,59 @@ function enqueue(entry) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
ArrayPrototypePush(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 = MathMax(
|
||||
MathMin(resourceTimingBufferSizeLimit - resourceTimingBuffer.length, resourceTimingSecondaryBuffer.length),
|
||||
0
|
||||
);
|
||||
const excessNumberAfter = resourceTimingSecondaryBuffer.length - numbersToPreserve;
|
||||
for (let idx = 0; idx < numbersToPreserve; idx++) {
|
||||
ArrayPrototypePush(resourceTimingBuffer, resourceTimingSecondaryBuffer[idx]);
|
||||
}
|
||||
|
||||
if (excessNumberBefore <= excessNumberAfter) {
|
||||
resourceTimingSecondaryBuffer = [];
|
||||
}
|
||||
}
|
||||
resourceTimingBufferFullPending = false;
|
||||
});
|
||||
}
|
||||
|
||||
ArrayPrototypePush(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;
|
||||
@@ -492,4 +561,9 @@ module.exports = {
|
||||
filterBufferMapByNameAndType,
|
||||
startPerf,
|
||||
stopPerf,
|
||||
|
||||
bufferUserTiming,
|
||||
bufferResourceTiming,
|
||||
setResourceTimingBufferSize,
|
||||
setDispatchBufferFull,
|
||||
};
|
||||
|
||||
@@ -15,6 +15,8 @@ const {
|
||||
|
||||
const {
|
||||
EventTarget,
|
||||
Event,
|
||||
kTrustEvent,
|
||||
} = require('internal/event_target');
|
||||
|
||||
const { now } = require('internal/perf/utils');
|
||||
@@ -29,6 +31,8 @@ const {
|
||||
const {
|
||||
clearEntriesFromBuffer,
|
||||
filterBufferMapByNameAndType,
|
||||
setResourceTimingBufferSize,
|
||||
setDispatchBufferFull,
|
||||
} = require('internal/perf/observe');
|
||||
|
||||
const { eventLoopUtilization } = require('internal/perf/event_loop_utilization');
|
||||
@@ -190,6 +194,12 @@ ObjectDefineProperties(Performance.prototype, {
|
||||
enumerable: false,
|
||||
value: now,
|
||||
},
|
||||
setResourceTimingBufferSize: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
value: setResourceTimingBufferSize
|
||||
},
|
||||
timerify: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
@@ -223,7 +233,18 @@ function refreshTimeOrigin() {
|
||||
});
|
||||
}
|
||||
|
||||
const performance = new InternalPerformance();
|
||||
|
||||
function dispatchBufferFull(type) {
|
||||
const event = new Event(type, {
|
||||
[kTrustEvent]: true
|
||||
});
|
||||
performance.dispatchEvent(event);
|
||||
}
|
||||
setDispatchBufferFull(dispatchBufferFull);
|
||||
|
||||
module.exports = {
|
||||
InternalPerformance,
|
||||
Performance,
|
||||
performance,
|
||||
refreshTimeOrigin
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
const { InternalPerformanceEntry } = require('internal/perf/performance_entry');
|
||||
const { SymbolToStringTag } = primordials;
|
||||
const assert = require('internal/assert');
|
||||
const { enqueue } = require('internal/perf/observe');
|
||||
const { enqueue, bufferResourceTiming } = require('internal/perf/observe');
|
||||
const { Symbol, ObjectSetPrototypeOf } = primordials;
|
||||
|
||||
const kCacheMode = Symbol('kCacheMode');
|
||||
@@ -174,6 +174,7 @@ function markResourceTiming(
|
||||
|
||||
ObjectSetPrototypeOf(resource, PerformanceResourceTiming.prototype);
|
||||
enqueue(resource);
|
||||
bufferResourceTiming(resource);
|
||||
return resource;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const {
|
||||
|
||||
const { InternalPerformanceEntry } = require('internal/perf/performance_entry');
|
||||
const { now } = require('internal/perf/utils');
|
||||
const { enqueue } = require('internal/perf/observe');
|
||||
const { enqueue, bufferUserTiming } = require('internal/perf/observe');
|
||||
const nodeTiming = require('internal/perf/nodetiming');
|
||||
|
||||
const {
|
||||
@@ -97,6 +97,7 @@ class PerformanceMeasure extends InternalPerformanceEntry {
|
||||
function mark(name, options = kEmptyObject) {
|
||||
const mark = new PerformanceMark(name, options);
|
||||
enqueue(mark);
|
||||
bufferUserTiming(mark);
|
||||
return mark;
|
||||
}
|
||||
|
||||
@@ -161,6 +162,7 @@ function measure(name, startOrMeasureOptions, endMark) {
|
||||
detail = detail != null ? structuredClone(detail) : null;
|
||||
const measure = new PerformanceMeasure(name, start, duration, detail);
|
||||
enqueue(measure);
|
||||
bufferUserTiming(measure);
|
||||
return measure;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,10 @@ const {
|
||||
PerformanceMark,
|
||||
PerformanceMeasure,
|
||||
} = require('internal/perf/usertiming');
|
||||
const { InternalPerformance } = require('internal/perf/performance');
|
||||
const {
|
||||
Performance,
|
||||
performance,
|
||||
} = require('internal/perf/performance');
|
||||
|
||||
const {
|
||||
createHistogram
|
||||
@@ -27,6 +30,7 @@ const {
|
||||
const monitorEventLoopDelay = require('internal/perf/event_loop_delay');
|
||||
|
||||
module.exports = {
|
||||
Performance,
|
||||
PerformanceEntry,
|
||||
PerformanceMark,
|
||||
PerformanceMeasure,
|
||||
@@ -35,7 +39,7 @@ module.exports = {
|
||||
PerformanceResourceTiming,
|
||||
monitorEventLoopDelay,
|
||||
createHistogram,
|
||||
performance: new InternalPerformance(),
|
||||
performance,
|
||||
};
|
||||
|
||||
ObjectDefineProperty(module.exports, 'constants', {
|
||||
|
||||
@@ -289,6 +289,9 @@ if (global.gc) {
|
||||
knownGlobals.push(global.gc);
|
||||
}
|
||||
|
||||
if (global.Performance) {
|
||||
knownGlobals.push(global.Performance);
|
||||
}
|
||||
if (global.performance) {
|
||||
knownGlobals.push(global.performance);
|
||||
}
|
||||
|
||||
128
test/parallel/test-performance-resourcetimingbufferfull.js
Normal file
128
test/parallel/test-performance-resourcetimingbufferfull.js
Normal file
@@ -0,0 +1,128 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
|
||||
const { performance } = require('perf_hooks');
|
||||
|
||||
function createTimingInfo(startTime) {
|
||||
const timingInfo = {
|
||||
startTime: startTime,
|
||||
endTime: startTime,
|
||||
finalServiceWorkerStartTime: 0,
|
||||
redirectStartTime: 0,
|
||||
redirectEndTime: 0,
|
||||
postRedirectStartTime: 0,
|
||||
finalConnectionTimingInfo: {
|
||||
domainLookupStartTime: 0,
|
||||
domainLookupEndTime: 0,
|
||||
connectionStartTime: 0,
|
||||
connectionEndTime: 0,
|
||||
secureConnectionStartTime: 0,
|
||||
ALPNNegotiatedProtocol: 0,
|
||||
},
|
||||
finalNetworkRequestStartTime: 0,
|
||||
finalNetworkResponseStartTime: 0,
|
||||
encodedBodySize: 0,
|
||||
decodedBodySize: 0,
|
||||
};
|
||||
return timingInfo;
|
||||
}
|
||||
const requestedUrl = 'https://nodejs.org';
|
||||
const initiatorType = '';
|
||||
const cacheMode = '';
|
||||
|
||||
async function main() {
|
||||
performance.setResourceTimingBufferSize(1);
|
||||
performance.markResourceTiming(createTimingInfo(1), requestedUrl, initiatorType, globalThis, cacheMode);
|
||||
// Trigger a resourcetimingbufferfull event.
|
||||
performance.markResourceTiming(createTimingInfo(2), requestedUrl, initiatorType, globalThis, cacheMode);
|
||||
performance.markResourceTiming(createTimingInfo(3), requestedUrl, initiatorType, globalThis, cacheMode);
|
||||
assert.strictEqual(performance.getEntriesByType('resource').length, 1);
|
||||
|
||||
// Clear resource timings on resourcetimingbufferfull event.
|
||||
await new Promise((resolve) => {
|
||||
const listener = common.mustCall((event) => {
|
||||
assert.strictEqual(event.type, 'resourcetimingbufferfull');
|
||||
performance.removeEventListener('resourcetimingbufferfull', listener);
|
||||
|
||||
performance.clearResourceTimings();
|
||||
assert.strictEqual(performance.getEntriesByType('resource').length, 0);
|
||||
|
||||
resolve();
|
||||
});
|
||||
performance.addEventListener('resourcetimingbufferfull', listener);
|
||||
});
|
||||
|
||||
// Secondary buffer has been added to the global buffer.
|
||||
{
|
||||
const entries = performance.getEntriesByType('resource');
|
||||
assert.strictEqual(entries.length, 1);
|
||||
assert.strictEqual(entries[0].startTime, 2);
|
||||
// The last item is discarded.
|
||||
}
|
||||
|
||||
|
||||
performance.clearResourceTimings();
|
||||
performance.setResourceTimingBufferSize(1);
|
||||
performance.markResourceTiming(createTimingInfo(4), requestedUrl, initiatorType, globalThis, cacheMode);
|
||||
// Trigger a resourcetimingbufferfull event.
|
||||
performance.markResourceTiming(createTimingInfo(5), requestedUrl, initiatorType, globalThis, cacheMode);
|
||||
performance.markResourceTiming(createTimingInfo(6), requestedUrl, initiatorType, globalThis, cacheMode);
|
||||
|
||||
// Increase the buffer size on resourcetimingbufferfull event.
|
||||
await new Promise((resolve) => {
|
||||
const listener = common.mustCall((event) => {
|
||||
assert.strictEqual(event.type, 'resourcetimingbufferfull');
|
||||
performance.removeEventListener('resourcetimingbufferfull', listener);
|
||||
|
||||
performance.setResourceTimingBufferSize(2);
|
||||
assert.strictEqual(performance.getEntriesByType('resource').length, 1);
|
||||
|
||||
resolve();
|
||||
});
|
||||
performance.addEventListener('resourcetimingbufferfull', listener);
|
||||
});
|
||||
|
||||
// Secondary buffer has been added to the global buffer.
|
||||
{
|
||||
const entries = performance.getEntriesByType('resource');
|
||||
assert.strictEqual(entries.length, 2);
|
||||
assert.strictEqual(entries[0].startTime, 4);
|
||||
assert.strictEqual(entries[1].startTime, 5);
|
||||
// The last item is discarded.
|
||||
}
|
||||
|
||||
|
||||
performance.clearResourceTimings();
|
||||
performance.setResourceTimingBufferSize(2);
|
||||
performance.markResourceTiming(createTimingInfo(7), requestedUrl, initiatorType, globalThis, cacheMode);
|
||||
performance.markResourceTiming(createTimingInfo(8), requestedUrl, initiatorType, globalThis, cacheMode);
|
||||
// Trigger a resourcetimingbufferfull event.
|
||||
performance.markResourceTiming(createTimingInfo(9), requestedUrl, initiatorType, globalThis, cacheMode);
|
||||
|
||||
// Decrease the buffer size on resourcetimingbufferfull event.
|
||||
await new Promise((resolve) => {
|
||||
const listener = common.mustCall((event) => {
|
||||
assert.strictEqual(event.type, 'resourcetimingbufferfull');
|
||||
performance.removeEventListener('resourcetimingbufferfull', listener);
|
||||
|
||||
performance.setResourceTimingBufferSize(1);
|
||||
assert.strictEqual(performance.getEntriesByType('resource').length, 2);
|
||||
|
||||
resolve();
|
||||
});
|
||||
performance.addEventListener('resourcetimingbufferfull', listener);
|
||||
});
|
||||
|
||||
// Secondary buffer has been added to the global buffer.
|
||||
{
|
||||
const entries = performance.getEntriesByType('resource');
|
||||
assert.strictEqual(entries.length, 2);
|
||||
assert.strictEqual(entries[0].startTime, 7);
|
||||
assert.strictEqual(entries[1].startTime, 8);
|
||||
// The last item is discarded.
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
70
test/parallel/test-performance-resourcetimingbuffersize.js
Normal file
70
test/parallel/test-performance-resourcetimingbuffersize.js
Normal file
@@ -0,0 +1,70 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
|
||||
const { performance } = require('perf_hooks');
|
||||
|
||||
const timingInfo = {
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
finalServiceWorkerStartTime: 0,
|
||||
redirectStartTime: 0,
|
||||
redirectEndTime: 0,
|
||||
postRedirectStartTime: 0,
|
||||
finalConnectionTimingInfo: {
|
||||
domainLookupStartTime: 0,
|
||||
domainLookupEndTime: 0,
|
||||
connectionStartTime: 0,
|
||||
connectionEndTime: 0,
|
||||
secureConnectionStartTime: 0,
|
||||
ALPNNegotiatedProtocol: 0,
|
||||
},
|
||||
finalNetworkRequestStartTime: 0,
|
||||
finalNetworkResponseStartTime: 0,
|
||||
encodedBodySize: 0,
|
||||
decodedBodySize: 0,
|
||||
};
|
||||
const requestedUrl = 'https://nodejs.org';
|
||||
const initiatorType = '';
|
||||
const cacheMode = '';
|
||||
|
||||
async function main() {
|
||||
performance.setResourceTimingBufferSize(1);
|
||||
performance.markResourceTiming(timingInfo, requestedUrl, initiatorType, globalThis, cacheMode);
|
||||
// Trigger a resourcetimingbufferfull event.
|
||||
performance.markResourceTiming(timingInfo, requestedUrl, initiatorType, globalThis, cacheMode);
|
||||
assert.strictEqual(performance.getEntriesByType('resource').length, 1);
|
||||
await waitBufferFullEvent();
|
||||
|
||||
// Apply a new buffer size limit
|
||||
performance.setResourceTimingBufferSize(0);
|
||||
// Buffer is not cleared on `performance.setResourceTimingBufferSize`.
|
||||
assert.strictEqual(performance.getEntriesByType('resource').length, 1);
|
||||
|
||||
performance.clearResourceTimings();
|
||||
assert.strictEqual(performance.getEntriesByType('resource').length, 0);
|
||||
// Trigger a resourcetimingbufferfull event.
|
||||
performance.markResourceTiming(timingInfo, requestedUrl, initiatorType, globalThis, cacheMode);
|
||||
// New entry is not added to the global buffer.
|
||||
assert.strictEqual(performance.getEntriesByType('resource').length, 0);
|
||||
await waitBufferFullEvent();
|
||||
|
||||
// Apply a new buffer size limit
|
||||
performance.setResourceTimingBufferSize(1);
|
||||
performance.markResourceTiming(timingInfo, requestedUrl, initiatorType, globalThis, cacheMode);
|
||||
assert.strictEqual(performance.getEntriesByType('resource').length, 1);
|
||||
}
|
||||
|
||||
function waitBufferFullEvent() {
|
||||
return new Promise((resolve) => {
|
||||
const listener = common.mustCall((event) => {
|
||||
assert.strictEqual(event.type, 'resourcetimingbufferfull');
|
||||
performance.removeEventListener('resourcetimingbufferfull', listener);
|
||||
resolve();
|
||||
});
|
||||
performance.addEventListener('resourcetimingbufferfull', listener);
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user