Skip to content

Commit 601bfad

Browse files
committed
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.
1 parent ec44403 commit 601bfad

9 files changed

+163
-11
lines changed

doc/api/perf_hooks.md

+18
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,15 @@ added: v8.5.0
308308
Returns the current high resolution millisecond timestamp, where 0 represents
309309
the start of the current `node` process.
310310

311+
### `performance.setResourceTimingBufferSize(maxSize)`
312+
313+
<!-- YAML
314+
added: REPLACEME
315+
-->
316+
317+
Sets the global performance resource timing buffer size to the specified number
318+
of "resource" type performance entry objects.
319+
311320
### `performance.timeOrigin`
312321

313322
<!-- YAML
@@ -383,6 +392,15 @@ added: v16.1.0
383392
An object which is JSON representation of the `performance` object. It
384393
is similar to [`window.performance.toJSON`][] in browsers.
385394

395+
#### Event: `'resourcetimingbufferfull'`
396+
397+
<!-- YAML
398+
added: REPLACEME
399+
-->
400+
401+
The `'resourcetimingbufferfull'` event is fired when the global performance
402+
resource timing buffer is full.
403+
386404
## Class: `PerformanceEntry`
387405

388406
<!-- YAML

lib/internal/bootstrap/browser.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,11 @@ defineOperation(globalThis, 'btoa', buffer.btoa);
7373
exposeInterface(globalThis, 'Blob', buffer.Blob);
7474

7575
// https://www.w3.org/TR/hr-time-2/#the-performance-attribute
76+
const perf_hooks = require('perf_hooks');
77+
defineReplacableAttribute(globalThis, 'Performance',
78+
perf_hooks.Performance);
7679
defineReplacableAttribute(globalThis, 'performance',
77-
require('perf_hooks').performance);
80+
perf_hooks.performance);
7881

7982
function createGlobalConsole() {
8083
const consoleFromNode =

lib/internal/perf/observe.js

+48-5
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,15 @@ const kSupportedEntryTypes = ObjectFreeze([
9595
let markEntryBuffer = [];
9696
let measureEntryBuffer = [];
9797
let resourceTimingBuffer = [];
98-
const kMaxPerformanceEntryBuffers = 1e6;
98+
const kPerformanceEntryBufferWarnSize = 1e6;
99+
// https://www.w3.org/TR/timing-entrytypes-registry/#registry
100+
// Default buffer limit for resource timing entries.
101+
let resourceTimingBufferSizeLimit = 250;
102+
let dispatchBufferFull;
103+
99104
const kClearPerformanceEntryBuffers = ObjectFreeze({
100105
'mark': 'performance.clearMarks',
101106
'measure': 'performance.clearMeasures',
102-
'resource': 'performance.clearResourceTimings',
103107
});
104108
const kWarnedEntryTypes = new SafeMap();
105109

@@ -332,30 +336,38 @@ class PerformanceObserver {
332336
}
333337
}
334338

339+
/**
340+
* https://www.w3.org/TR/performance-timeline/#dfn-queue-a-performanceentry
341+
*
342+
* Add the performance entry to the interested performance observer's queue.
343+
*/
335344
function enqueue(entry) {
336345
if (!isPerformanceEntry(entry))
337346
throw new ERR_INVALID_ARG_TYPE('entry', 'PerformanceEntry', entry);
338347

339348
for (const obs of kObservers) {
340349
obs[kMaybeBuffer](entry);
341350
}
351+
}
342352

353+
/**
354+
* Add the user timing entry to the global buffer.
355+
*/
356+
function bufferUserTiming(entry) {
343357
const entryType = entry.entryType;
344358
let buffer;
345359
if (entryType === 'mark') {
346360
buffer = markEntryBuffer;
347361
} else if (entryType === 'measure') {
348362
buffer = measureEntryBuffer;
349-
} else if (entryType === 'resource') {
350-
buffer = resourceTimingBuffer;
351363
} else {
352364
return;
353365
}
354366

355367
ArrayPrototypePush(buffer, entry);
356368
const count = buffer.length;
357369

358-
if (count > kMaxPerformanceEntryBuffers &&
370+
if (count > kPerformanceEntryBufferWarnSize &&
359371
!kWarnedEntryTypes.has(entryType)) {
360372
kWarnedEntryTypes.set(entryType, true);
361373
// No error code for this since it is a Warning
@@ -372,6 +384,32 @@ function enqueue(entry) {
372384
}
373385
}
374386

387+
/**
388+
* Add the resource timing entry to the global buffer if the buffer size is not
389+
* exceeding the buffer limit, or dispatch a buffer full event on the global
390+
* performance object.
391+
*/
392+
function bufferResourceTiming(entry) {
393+
if (resourceTimingBuffer.length >= resourceTimingBufferSizeLimit) {
394+
dispatchBufferFull('resourcetimingbufferfull');
395+
return;
396+
}
397+
398+
ArrayPrototypePush(resourceTimingBuffer, entry);
399+
}
400+
401+
// https://w3c.github.io/resource-timing/#dom-performance-setresourcetimingbuffersize
402+
function setResourceTimingBufferSize(maxSize) {
403+
// If the maxSize parameter is less than resource timing buffer current
404+
// size, no PerformanceResourceTiming objects are to be removed from the
405+
// performance entry buffer.
406+
resourceTimingBufferSizeLimit = maxSize;
407+
}
408+
409+
function setDispatchBufferFull(fn) {
410+
dispatchBufferFull = fn;
411+
}
412+
375413
function clearEntriesFromBuffer(type, name) {
376414
if (type !== 'mark' && type !== 'measure' && type !== 'resource') {
377415
return;
@@ -492,4 +530,9 @@ module.exports = {
492530
filterBufferMapByNameAndType,
493531
startPerf,
494532
stopPerf,
533+
534+
bufferUserTiming,
535+
bufferResourceTiming,
536+
setResourceTimingBufferSize,
537+
setDispatchBufferFull,
495538
};

lib/internal/perf/performance.js

+22-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const {
1515

1616
const {
1717
EventTarget,
18+
Event,
19+
kTrustEvent,
1820
} = require('internal/event_target');
1921

2022
const { now } = require('internal/perf/utils');
@@ -29,6 +31,8 @@ const {
2931
const {
3032
clearEntriesFromBuffer,
3133
filterBufferMapByNameAndType,
34+
setResourceTimingBufferSize,
35+
setDispatchBufferFull,
3236
} = require('internal/perf/observe');
3337

3438
const { eventLoopUtilization } = require('internal/perf/event_loop_utilization');
@@ -190,6 +194,12 @@ ObjectDefineProperties(Performance.prototype, {
190194
enumerable: false,
191195
value: now,
192196
},
197+
setResourceTimingBufferSize: {
198+
__proto__: null,
199+
configurable: true,
200+
enumerable: false,
201+
value: setResourceTimingBufferSize
202+
},
193203
timerify: {
194204
__proto__: null,
195205
configurable: true,
@@ -223,7 +233,18 @@ function refreshTimeOrigin() {
223233
});
224234
}
225235

236+
const performance = new InternalPerformance();
237+
238+
function dispatchBufferFull(type) {
239+
const event = new Event(type, {
240+
[kTrustEvent]: true
241+
});
242+
performance.dispatchEvent(event);
243+
}
244+
setDispatchBufferFull(dispatchBufferFull);
245+
226246
module.exports = {
227-
InternalPerformance,
247+
Performance,
248+
performance,
228249
refreshTimeOrigin
229250
};

lib/internal/perf/resource_timing.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
const { InternalPerformanceEntry } = require('internal/perf/performance_entry');
55
const { SymbolToStringTag } = primordials;
66
const assert = require('internal/assert');
7-
const { enqueue } = require('internal/perf/observe');
7+
const { enqueue, bufferResourceTiming } = require('internal/perf/observe');
88
const { Symbol, ObjectSetPrototypeOf } = primordials;
99

1010
const kCacheMode = Symbol('kCacheMode');
@@ -174,6 +174,7 @@ function markResourceTiming(
174174

175175
ObjectSetPrototypeOf(resource, PerformanceResourceTiming.prototype);
176176
enqueue(resource);
177+
bufferResourceTiming(resource);
177178
return resource;
178179
}
179180

lib/internal/perf/usertiming.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const {
99

1010
const { InternalPerformanceEntry } = require('internal/perf/performance_entry');
1111
const { now } = require('internal/perf/utils');
12-
const { enqueue } = require('internal/perf/observe');
12+
const { enqueue, bufferUserTiming } = require('internal/perf/observe');
1313
const nodeTiming = require('internal/perf/nodetiming');
1414

1515
const {
@@ -97,6 +97,7 @@ class PerformanceMeasure extends InternalPerformanceEntry {
9797
function mark(name, options = kEmptyObject) {
9898
const mark = new PerformanceMark(name, options);
9999
enqueue(mark);
100+
bufferUserTiming(mark);
100101
return mark;
101102
}
102103

@@ -161,6 +162,7 @@ function measure(name, startOrMeasureOptions, endMark) {
161162
detail = detail != null ? structuredClone(detail) : null;
162163
const measure = new PerformanceMeasure(name, start, duration, detail);
163164
enqueue(measure);
165+
bufferUserTiming(measure);
164166
return measure;
165167
}
166168

lib/perf_hooks.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ const {
1818
PerformanceMark,
1919
PerformanceMeasure,
2020
} = require('internal/perf/usertiming');
21-
const { InternalPerformance } = require('internal/perf/performance');
21+
const {
22+
Performance,
23+
performance,
24+
} = require('internal/perf/performance');
2225

2326
const {
2427
createHistogram
@@ -27,6 +30,7 @@ const {
2730
const monitorEventLoopDelay = require('internal/perf/event_loop_delay');
2831

2932
module.exports = {
33+
Performance,
3034
PerformanceEntry,
3135
PerformanceMark,
3236
PerformanceMeasure,
@@ -35,7 +39,7 @@ module.exports = {
3539
PerformanceResourceTiming,
3640
monitorEventLoopDelay,
3741
createHistogram,
38-
performance: new InternalPerformance(),
42+
performance,
3943
};
4044

4145
ObjectDefineProperty(module.exports, 'constants', {

test/common/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,9 @@ if (global.gc) {
289289
knownGlobals.push(global.gc);
290290
}
291291

292+
if (global.Performance) {
293+
knownGlobals.push(global.Performance);
294+
}
292295
if (global.performance) {
293296
knownGlobals.push(global.performance);
294297
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
6+
const { performance } = require('perf_hooks');
7+
8+
const timingInfo = {
9+
startTime: 0,
10+
endTime: 0,
11+
finalServiceWorkerStartTime: 0,
12+
redirectStartTime: 0,
13+
redirectEndTime: 0,
14+
postRedirectStartTime: 0,
15+
finalConnectionTimingInfo: {
16+
domainLookupStartTime: 0,
17+
domainLookupEndTime: 0,
18+
connectionStartTime: 0,
19+
connectionEndTime: 0,
20+
secureConnectionStartTime: 0,
21+
ALPNNegotiatedProtocol: 0,
22+
},
23+
finalNetworkRequestStartTime: 0,
24+
finalNetworkResponseStartTime: 0,
25+
encodedBodySize: 0,
26+
decodedBodySize: 0,
27+
};
28+
const requestedUrl = 'https://nodejs.org';
29+
const initiatorType = '';
30+
const cacheMode = '';
31+
32+
performance.addEventListener('resourcetimingbufferfull', common.mustCall((event) => {
33+
assert.strictEqual(event.type, 'resourcetimingbufferfull');
34+
}, 2));
35+
36+
performance.setResourceTimingBufferSize(1);
37+
performance.markResourceTiming(timingInfo, requestedUrl, initiatorType, globalThis, cacheMode);
38+
// Trigger a resourcetimingbufferfull event.
39+
performance.markResourceTiming(timingInfo, requestedUrl, initiatorType, globalThis, cacheMode);
40+
assert.strictEqual(performance.getEntriesByType('resource').length, 1);
41+
42+
// Apply a new buffer size limit
43+
performance.setResourceTimingBufferSize(0);
44+
// Buffer is not cleared on `performance.setResourceTimingBufferSize`.
45+
assert.strictEqual(performance.getEntriesByType('resource').length, 1);
46+
47+
performance.clearResourceTimings();
48+
assert.strictEqual(performance.getEntriesByType('resource').length, 0);
49+
// Trigger a resourcetimingbufferfull event.
50+
performance.markResourceTiming(timingInfo, requestedUrl, initiatorType, globalThis, cacheMode);
51+
// New entry is not added to the global buffer.
52+
assert.strictEqual(performance.getEntriesByType('resource').length, 0);
53+
54+
// Apply a new buffer size limit
55+
performance.setResourceTimingBufferSize(1);
56+
performance.markResourceTiming(timingInfo, requestedUrl, initiatorType, globalThis, cacheMode);
57+
assert.strictEqual(performance.getEntriesByType('resource').length, 1);

0 commit comments

Comments
 (0)