Skip to content

Commit

Permalink
perf_hooks: add resourcetiming buffer limit
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
legendecas committed Aug 12, 2022
1 parent ec44403 commit 2579888
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 11 deletions.
18 changes: 18 additions & 0 deletions doc/api/perf_hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,15 @@ 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.

### `performance.timeOrigin`

<!-- YAML
Expand Down Expand Up @@ -383,6 +392,15 @@ 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.

## Class: `PerformanceEntry`

<!-- YAML
Expand Down
4 changes: 3 additions & 1 deletion lib/internal/bootstrap/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
53 changes: 48 additions & 5 deletions lib/internal/perf/observe.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,15 @@ const kSupportedEntryTypes = ObjectFreeze([
let markEntryBuffer = [];
let measureEntryBuffer = [];
let resourceTimingBuffer = [];
const kMaxPerformanceEntryBuffers = 1e6;
const kPerformanceEntryBufferWarnSize = 1e6;
// https://www.w3.org/TR/timing-entrytypes-registry/#registry
// Default buffer limit for resource timing entries.
let resourceTimingBufferSizeLimit = 250;
let dispatchBufferFull;

const kClearPerformanceEntryBuffers = ObjectFreeze({
'mark': 'performance.clearMarks',
'measure': 'performance.clearMeasures',
'resource': 'performance.clearResourceTimings',
});
const kWarnedEntryTypes = new SafeMap();

Expand Down Expand Up @@ -332,30 +336,38 @@ 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);

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;
}

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
Expand All @@ -372,6 +384,32 @@ 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.
*/
function bufferResourceTiming(entry) {
if (resourceTimingBuffer.length >= resourceTimingBufferSizeLimit) {
dispatchBufferFull('resourcetimingbufferfull');
return;
}

ArrayPrototypePush(resourceTimingBuffer, 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;
Expand Down Expand Up @@ -492,4 +530,9 @@ module.exports = {
filterBufferMapByNameAndType,
startPerf,
stopPerf,

bufferUserTiming,
bufferResourceTiming,
setResourceTimingBufferSize,
setDispatchBufferFull,
};
23 changes: 22 additions & 1 deletion lib/internal/perf/performance.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const {

const {
EventTarget,
Event,
kTrustEvent,
} = require('internal/event_target');

const { now } = require('internal/perf/utils');
Expand All @@ -29,6 +31,8 @@ const {
const {
clearEntriesFromBuffer,
filterBufferMapByNameAndType,
setResourceTimingBufferSize,
setDispatchBufferFull,
} = require('internal/perf/observe');

const { eventLoopUtilization } = require('internal/perf/event_loop_utilization');
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
};
3 changes: 2 additions & 1 deletion lib/internal/perf/resource_timing.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -174,6 +174,7 @@ function markResourceTiming(

ObjectSetPrototypeOf(resource, PerformanceResourceTiming.prototype);
enqueue(resource);
bufferResourceTiming(resource);
return resource;
}

Expand Down
4 changes: 3 additions & 1 deletion lib/internal/perf/usertiming.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand Down
8 changes: 6 additions & 2 deletions lib/perf_hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,6 +30,7 @@ const {
const monitorEventLoopDelay = require('internal/perf/event_loop_delay');

module.exports = {
Performance,
PerformanceEntry,
PerformanceMark,
PerformanceMeasure,
Expand All @@ -35,7 +39,7 @@ module.exports = {
PerformanceResourceTiming,
monitorEventLoopDelay,
createHistogram,
performance: new InternalPerformance(),
performance,
};

ObjectDefineProperty(module.exports, 'constants', {
Expand Down
3 changes: 3 additions & 0 deletions test/common/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
57 changes: 57 additions & 0 deletions test/parallel/test-performance-resourcetimingbuffersize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'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 = '';

performance.addEventListener('resourcetimingbufferfull', common.mustCall((event) => {
assert.strictEqual(event.type, 'resourcetimingbufferfull');
}, 2));

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);

// 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);

// Apply a new buffer size limit
performance.setResourceTimingBufferSize(1);
performance.markResourceTiming(timingInfo, requestedUrl, initiatorType, globalThis, cacheMode);
assert.strictEqual(performance.getEntriesByType('resource').length, 1);

0 comments on commit 2579888

Please sign in to comment.