Skip to content

Commit 062f8e3

Browse files
committed
perf_hooks: web performance timeline compliance
All API introduced in this PR are compliant with web [performance-timeline](https://w3c.github.io/performance-timeline) spec. "performance-timeline" is listed as supported web spec in the doc https://nodejs.org/docs/latest/api/perf_hooks.html#perf_hooks_performance_measurement_apis. Changes summary: 1. Add new supported wpt test subsets: user-timing and performance-timeline. 2. Add support for `Performance.getEntries`, `Performance.getEntriesByName` and `Performance.getEntriesByType` to synchronously fetch buffered performance entries. This means the user should invoke `Performance.clearMarks` and `Performance.clearMeasures` to clear buffered entries to prevent from those entries been kept alive forever. 3. Add support (again after #37136) for `buffered` flags for `PerformanceObserver`. 3. Fixes `PerformanceMark` and `PerformanceMeasure` wpt compliance issues. 4. Only user-created performance entries will be buffered globally. This behavior should be compliant with https://w3c.github.io/timing-entrytypes-registry/#registry. With the new ability to fetch user-created performance entries synchronously, the issues raised in nodejs/diagnostics#464 (comment) could also be fixed. PR-URL: #39297 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 5c4e673 commit 062f8e3

File tree

91 files changed

+3844
-59
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+3844
-59
lines changed

benchmark/perf_hooks/usertiming.js

+11-8
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,27 @@ const {
88
} = require('perf_hooks');
99

1010
const bench = common.createBenchmark(main, {
11-
n: [1e5]
11+
n: [1e5],
12+
observe: ['all', 'measure'],
1213
});
1314

1415
function test() {
1516
performance.mark('a');
16-
setImmediate(() => {
17-
performance.mark('b');
18-
performance.measure('a to b', 'a', 'b');
19-
});
17+
performance.mark('b');
18+
performance.measure('a to b', 'a', 'b');
2019
}
2120

22-
function main({ n }) {
21+
function main({ n, observe }) {
22+
const entryTypes = observe === 'all' ?
23+
[ 'mark', 'measure' ] :
24+
[ observe ];
2325
const obs = new PerformanceObserver(() => {
2426
bench.end(n);
2527
});
26-
obs.observe({ entryTypes: ['measure'], buffered: true });
28+
obs.observe({ entryTypes, buffered: true });
2729

2830
bench.start();
29-
for (let i = 0; i < n; i++)
31+
performance.mark('start');
32+
for (let i = 0; i < 1e5; i++)
3033
test();
3134
}

lib/internal/errors.js

-2
Original file line numberDiff line numberDiff line change
@@ -1266,8 +1266,6 @@ E('ERR_INVALID_PACKAGE_TARGET',
12661266
pkgPath}package.json${base ? ` imported from ${base}` : ''}${relError ?
12671267
'; targets must start with "./"' : ''}`;
12681268
}, Error);
1269-
E('ERR_INVALID_PERFORMANCE_MARK',
1270-
'The "%s" performance mark has not been set', Error);
12711269
E('ERR_INVALID_PROTOCOL',
12721270
'Protocol "%s" not supported. Expected "%s"',
12731271
TypeError);

lib/internal/perf/observe.js

+131
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ const {
44
ArrayFrom,
55
ArrayIsArray,
66
ArrayPrototypeFilter,
7+
ArrayPrototypeFlatMap,
78
ArrayPrototypeIncludes,
89
ArrayPrototypePush,
10+
ArrayPrototypePushApply,
911
ArrayPrototypeSlice,
1012
ArrayPrototypeSort,
13+
Error,
1114
ObjectDefineProperties,
1215
ObjectFreeze,
1316
ObjectKeys,
@@ -31,6 +34,7 @@ const {
3134
const {
3235
InternalPerformanceEntry,
3336
isPerformanceEntry,
37+
kBufferNext,
3438
} = require('internal/perf/performance_entry');
3539

3640
const {
@@ -83,6 +87,16 @@ const kSupportedEntryTypes = ObjectFreeze([
8387
'measure',
8488
]);
8589

90+
// Performance timeline entry Buffers
91+
const markEntryBuffer = createBuffer();
92+
const measureEntryBuffer = createBuffer();
93+
const kMaxPerformanceEntryBuffers = 1e6;
94+
const kClearPerformanceEntryBuffers = ObjectFreeze({
95+
'mark': 'performance.clearMarks',
96+
'measure': 'performance.clearMeasures',
97+
});
98+
const kWarnedEntryTypes = new SafeMap();
99+
86100
const kObservers = new SafeSet();
87101
const kPending = new SafeSet();
88102
let isPending = false;
@@ -190,6 +204,7 @@ class PerformanceObserver {
190204
const {
191205
entryTypes,
192206
type,
207+
buffered,
193208
} = { ...options };
194209
if (entryTypes === undefined && type === undefined)
195210
throw new ERR_MISSING_ARGS('options.entryTypes', 'options.type');
@@ -229,6 +244,13 @@ class PerformanceObserver {
229244
return;
230245
this[kEntryTypes].add(type);
231246
maybeIncrementObserverCount(type);
247+
if (buffered) {
248+
const entries = filterBufferMapByNameAndType(undefined, type);
249+
ArrayPrototypePushApply(this[kBuffer], entries);
250+
kPending.add(this);
251+
if (kPending.size)
252+
queuePending();
253+
}
232254
}
233255

234256
if (this[kEntryTypes].size)
@@ -291,6 +313,99 @@ function enqueue(entry) {
291313
for (const obs of kObservers) {
292314
obs[kMaybeBuffer](entry);
293315
}
316+
317+
const entryType = entry.entryType;
318+
let buffer;
319+
if (entryType === 'mark') {
320+
buffer = markEntryBuffer;
321+
} else if (entryType === 'measure') {
322+
buffer = measureEntryBuffer;
323+
} else {
324+
return;
325+
}
326+
327+
const count = buffer.count + 1;
328+
buffer.count = count;
329+
if (count === 1) {
330+
buffer.head = entry;
331+
buffer.tail = entry;
332+
return;
333+
}
334+
buffer.tail[kBufferNext] = entry;
335+
buffer.tail = entry;
336+
337+
if (count > kMaxPerformanceEntryBuffers &&
338+
!kWarnedEntryTypes.has(entryType)) {
339+
kWarnedEntryTypes.set(entryType, true);
340+
// No error code for this since it is a Warning
341+
// eslint-disable-next-line no-restricted-syntax
342+
const w = new Error('Possible perf_hooks memory leak detected. ' +
343+
`${count} ${entryType} entries added to the global ` +
344+
'performance entry buffer. Use ' +
345+
`${kClearPerformanceEntryBuffers[entryType]} to ` +
346+
'clear the buffer.');
347+
w.name = 'MaxPerformanceEntryBufferExceededWarning';
348+
w.entryType = entryType;
349+
w.count = count;
350+
process.emitWarning(w);
351+
}
352+
}
353+
354+
function clearEntriesFromBuffer(type, name) {
355+
let buffer;
356+
if (type === 'mark') {
357+
buffer = markEntryBuffer;
358+
} else if (type === 'measure') {
359+
buffer = measureEntryBuffer;
360+
} else {
361+
return;
362+
}
363+
if (name === undefined) {
364+
resetBuffer(buffer);
365+
return;
366+
}
367+
368+
let head = null;
369+
let tail = null;
370+
for (let entry = buffer.head; entry !== null; entry = entry[kBufferNext]) {
371+
if (entry.name !== name) {
372+
head = head ?? entry;
373+
tail = entry;
374+
continue;
375+
}
376+
if (tail === null) {
377+
continue;
378+
}
379+
tail[kBufferNext] = entry[kBufferNext];
380+
}
381+
buffer.head = head;
382+
buffer.tail = tail;
383+
}
384+
385+
function filterBufferMapByNameAndType(name, type) {
386+
let bufferList;
387+
if (type === 'mark') {
388+
bufferList = [markEntryBuffer];
389+
} else if (type === 'measure') {
390+
bufferList = [measureEntryBuffer];
391+
} else if (type !== undefined) {
392+
// Unrecognized type;
393+
return [];
394+
} else {
395+
bufferList = [markEntryBuffer, measureEntryBuffer];
396+
}
397+
return ArrayPrototypeFlatMap(bufferList,
398+
(buffer) => filterBufferByName(buffer, name));
399+
}
400+
401+
function filterBufferByName(buffer, name) {
402+
const arr = [];
403+
for (let entry = buffer.head; entry !== null; entry = entry[kBufferNext]) {
404+
if (name === undefined || entry.name === name) {
405+
ArrayPrototypePush(arr, entry);
406+
}
407+
}
408+
return arr;
294409
}
295410

296411
function observerCallback(name, type, startTime, duration, details) {
@@ -338,8 +453,24 @@ function hasObserver(type) {
338453
return observerCounts[observerType] > 0;
339454
}
340455

456+
function createBuffer() {
457+
return {
458+
head: null,
459+
tail: null,
460+
count: 0,
461+
};
462+
}
463+
464+
function resetBuffer(buffer) {
465+
buffer.head = null;
466+
buffer.tail = null;
467+
buffer.count = 0;
468+
}
469+
341470
module.exports = {
342471
PerformanceObserver,
343472
enqueue,
344473
hasObserver,
474+
clearEntriesFromBuffer,
475+
filterBufferMapByNameAndType,
345476
};

lib/internal/perf/performance.js

+58-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ const { now } = require('internal/perf/utils');
1616
const {
1717
mark,
1818
measure,
19-
clearMarks,
19+
clearMarkTimings,
2020
} = require('internal/perf/usertiming');
21+
const {
22+
clearEntriesFromBuffer,
23+
filterBufferMapByNameAndType,
24+
} = require('internal/perf/observe');
2125

2226
const eventLoopUtilization = require('internal/perf/event_loop_utilization');
2327
const nodeTiming = require('internal/perf/nodetiming');
@@ -48,7 +52,6 @@ class Performance extends EventTarget {
4852
timeOrigin: this.timeOrigin,
4953
}, opts)}`;
5054
}
51-
5255
}
5356

5457
function toJSON() {
@@ -59,6 +62,39 @@ function toJSON() {
5962
};
6063
}
6164

65+
function clearMarks(name) {
66+
if (name !== undefined) {
67+
name = `${name}`;
68+
}
69+
clearMarkTimings(name);
70+
clearEntriesFromBuffer('mark', name);
71+
}
72+
73+
function clearMeasures(name) {
74+
if (name !== undefined) {
75+
name = `${name}`;
76+
}
77+
clearEntriesFromBuffer('measure', name);
78+
}
79+
80+
function getEntries() {
81+
return filterBufferMapByNameAndType();
82+
}
83+
84+
function getEntriesByName(name) {
85+
if (name !== undefined) {
86+
name = `${name}`;
87+
}
88+
return filterBufferMapByNameAndType(name, undefined);
89+
}
90+
91+
function getEntriesByType(type) {
92+
if (type !== undefined) {
93+
type = `${type}`;
94+
}
95+
return filterBufferMapByNameAndType(undefined, type);
96+
}
97+
6298
class InternalPerformance extends EventTarget {}
6399
InternalPerformance.prototype.constructor = Performance.prototype.constructor;
64100
ObjectSetPrototypeOf(InternalPerformance.prototype, Performance.prototype);
@@ -69,11 +105,31 @@ ObjectDefineProperties(Performance.prototype, {
69105
enumerable: false,
70106
value: clearMarks,
71107
},
108+
clearMeasures: {
109+
configurable: true,
110+
enumerable: false,
111+
value: clearMeasures,
112+
},
72113
eventLoopUtilization: {
73114
configurable: true,
74115
enumerable: false,
75116
value: eventLoopUtilization,
76117
},
118+
getEntries: {
119+
configurable: true,
120+
enumerable: false,
121+
value: getEntries,
122+
},
123+
getEntriesByName: {
124+
configurable: true,
125+
enumerable: false,
126+
value: getEntriesByName,
127+
},
128+
getEntriesByType: {
129+
configurable: true,
130+
enumerable: false,
131+
value: getEntriesByType,
132+
},
77133
mark: {
78134
configurable: true,
79135
enumerable: false,

lib/internal/perf/performance_entry.js

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const kType = Symbol('kType');
1717
const kStart = Symbol('kStart');
1818
const kDuration = Symbol('kDuration');
1919
const kDetail = Symbol('kDetail');
20+
const kBufferNext = Symbol('kBufferNext');
2021

2122
function isPerformanceEntry(obj) {
2223
return obj?.[kName] !== undefined;
@@ -67,6 +68,7 @@ class InternalPerformanceEntry {
6768
this[kStart] = start;
6869
this[kDuration] = duration;
6970
this[kDetail] = detail;
71+
this[kBufferNext] = null;
7072
}
7173
}
7274

@@ -79,4 +81,5 @@ module.exports = {
7981
InternalPerformanceEntry,
8082
PerformanceEntry,
8183
isPerformanceEntry,
84+
kBufferNext,
8285
};

0 commit comments

Comments
 (0)