Skip to content

Commit

Permalink
perf_hooks: web performance timeline compliance
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
legendecas committed Jul 25, 2021
1 parent 5c4e673 commit 062f8e3
Show file tree
Hide file tree
Showing 91 changed files with 3,844 additions and 59 deletions.
19 changes: 11 additions & 8 deletions benchmark/perf_hooks/usertiming.js
Expand Up @@ -8,24 +8,27 @@ const {
} = require('perf_hooks');

const bench = common.createBenchmark(main, {
n: [1e5]
n: [1e5],
observe: ['all', 'measure'],
});

function test() {
performance.mark('a');
setImmediate(() => {
performance.mark('b');
performance.measure('a to b', 'a', 'b');
});
performance.mark('b');
performance.measure('a to b', 'a', 'b');
}

function main({ n }) {
function main({ n, observe }) {
const entryTypes = observe === 'all' ?
[ 'mark', 'measure' ] :
[ observe ];
const obs = new PerformanceObserver(() => {
bench.end(n);
});
obs.observe({ entryTypes: ['measure'], buffered: true });
obs.observe({ entryTypes, buffered: true });

bench.start();
for (let i = 0; i < n; i++)
performance.mark('start');
for (let i = 0; i < 1e5; i++)
test();
}
2 changes: 0 additions & 2 deletions lib/internal/errors.js
Expand Up @@ -1266,8 +1266,6 @@ E('ERR_INVALID_PACKAGE_TARGET',
pkgPath}package.json${base ? ` imported from ${base}` : ''}${relError ?
'; targets must start with "./"' : ''}`;
}, Error);
E('ERR_INVALID_PERFORMANCE_MARK',
'The "%s" performance mark has not been set', Error);
E('ERR_INVALID_PROTOCOL',
'Protocol "%s" not supported. Expected "%s"',
TypeError);
Expand Down
131 changes: 131 additions & 0 deletions lib/internal/perf/observe.js
Expand Up @@ -4,10 +4,13 @@ const {
ArrayFrom,
ArrayIsArray,
ArrayPrototypeFilter,
ArrayPrototypeFlatMap,
ArrayPrototypeIncludes,
ArrayPrototypePush,
ArrayPrototypePushApply,
ArrayPrototypeSlice,
ArrayPrototypeSort,
Error,
ObjectDefineProperties,
ObjectFreeze,
ObjectKeys,
Expand All @@ -31,6 +34,7 @@ const {
const {
InternalPerformanceEntry,
isPerformanceEntry,
kBufferNext,
} = require('internal/perf/performance_entry');

const {
Expand Down Expand Up @@ -83,6 +87,16 @@ const kSupportedEntryTypes = ObjectFreeze([
'measure',
]);

// Performance timeline entry Buffers
const markEntryBuffer = createBuffer();
const measureEntryBuffer = createBuffer();
const kMaxPerformanceEntryBuffers = 1e6;
const kClearPerformanceEntryBuffers = ObjectFreeze({
'mark': 'performance.clearMarks',
'measure': 'performance.clearMeasures',
});
const kWarnedEntryTypes = new SafeMap();

const kObservers = new SafeSet();
const kPending = new SafeSet();
let isPending = false;
Expand Down Expand Up @@ -190,6 +204,7 @@ class PerformanceObserver {
const {
entryTypes,
type,
buffered,
} = { ...options };
if (entryTypes === undefined && type === undefined)
throw new ERR_MISSING_ARGS('options.entryTypes', 'options.type');
Expand Down Expand Up @@ -229,6 +244,13 @@ class PerformanceObserver {
return;
this[kEntryTypes].add(type);
maybeIncrementObserverCount(type);
if (buffered) {
const entries = filterBufferMapByNameAndType(undefined, type);
ArrayPrototypePushApply(this[kBuffer], entries);
kPending.add(this);
if (kPending.size)
queuePending();
}
}

if (this[kEntryTypes].size)
Expand Down Expand Up @@ -291,6 +313,99 @@ function enqueue(entry) {
for (const obs of kObservers) {
obs[kMaybeBuffer](entry);
}

const entryType = entry.entryType;
let buffer;
if (entryType === 'mark') {
buffer = markEntryBuffer;
} else if (entryType === 'measure') {
buffer = measureEntryBuffer;
} else {
return;
}

const count = buffer.count + 1;
buffer.count = count;
if (count === 1) {
buffer.head = entry;
buffer.tail = entry;
return;
}
buffer.tail[kBufferNext] = entry;
buffer.tail = entry;

if (count > kMaxPerformanceEntryBuffers &&
!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);
}
}

function clearEntriesFromBuffer(type, name) {
let buffer;
if (type === 'mark') {
buffer = markEntryBuffer;
} else if (type === 'measure') {
buffer = measureEntryBuffer;
} else {
return;
}
if (name === undefined) {
resetBuffer(buffer);
return;
}

let head = null;
let tail = null;
for (let entry = buffer.head; entry !== null; entry = entry[kBufferNext]) {
if (entry.name !== name) {
head = head ?? entry;
tail = entry;
continue;
}
if (tail === null) {
continue;
}
tail[kBufferNext] = entry[kBufferNext];
}
buffer.head = head;
buffer.tail = tail;
}

function filterBufferMapByNameAndType(name, type) {
let bufferList;
if (type === 'mark') {
bufferList = [markEntryBuffer];
} else if (type === 'measure') {
bufferList = [measureEntryBuffer];
} else if (type !== undefined) {
// Unrecognized type;
return [];
} else {
bufferList = [markEntryBuffer, measureEntryBuffer];
}
return ArrayPrototypeFlatMap(bufferList,
(buffer) => filterBufferByName(buffer, name));
}

function filterBufferByName(buffer, name) {
const arr = [];
for (let entry = buffer.head; entry !== null; entry = entry[kBufferNext]) {
if (name === undefined || entry.name === name) {
ArrayPrototypePush(arr, entry);
}
}
return arr;
}

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

function createBuffer() {
return {
head: null,
tail: null,
count: 0,
};
}

function resetBuffer(buffer) {
buffer.head = null;
buffer.tail = null;
buffer.count = 0;
}

module.exports = {
PerformanceObserver,
enqueue,
hasObserver,
clearEntriesFromBuffer,
filterBufferMapByNameAndType,
};
60 changes: 58 additions & 2 deletions lib/internal/perf/performance.js
Expand Up @@ -16,8 +16,12 @@ const { now } = require('internal/perf/utils');
const {
mark,
measure,
clearMarks,
clearMarkTimings,
} = require('internal/perf/usertiming');
const {
clearEntriesFromBuffer,
filterBufferMapByNameAndType,
} = require('internal/perf/observe');

const eventLoopUtilization = require('internal/perf/event_loop_utilization');
const nodeTiming = require('internal/perf/nodetiming');
Expand Down Expand Up @@ -48,7 +52,6 @@ class Performance extends EventTarget {
timeOrigin: this.timeOrigin,
}, opts)}`;
}

}

function toJSON() {
Expand All @@ -59,6 +62,39 @@ function toJSON() {
};
}

function clearMarks(name) {
if (name !== undefined) {
name = `${name}`;
}
clearMarkTimings(name);
clearEntriesFromBuffer('mark', name);
}

function clearMeasures(name) {
if (name !== undefined) {
name = `${name}`;
}
clearEntriesFromBuffer('measure', name);
}

function getEntries() {
return filterBufferMapByNameAndType();
}

function getEntriesByName(name) {
if (name !== undefined) {
name = `${name}`;
}
return filterBufferMapByNameAndType(name, undefined);
}

function getEntriesByType(type) {
if (type !== undefined) {
type = `${type}`;
}
return filterBufferMapByNameAndType(undefined, type);
}

class InternalPerformance extends EventTarget {}
InternalPerformance.prototype.constructor = Performance.prototype.constructor;
ObjectSetPrototypeOf(InternalPerformance.prototype, Performance.prototype);
Expand All @@ -69,11 +105,31 @@ ObjectDefineProperties(Performance.prototype, {
enumerable: false,
value: clearMarks,
},
clearMeasures: {
configurable: true,
enumerable: false,
value: clearMeasures,
},
eventLoopUtilization: {
configurable: true,
enumerable: false,
value: eventLoopUtilization,
},
getEntries: {
configurable: true,
enumerable: false,
value: getEntries,
},
getEntriesByName: {
configurable: true,
enumerable: false,
value: getEntriesByName,
},
getEntriesByType: {
configurable: true,
enumerable: false,
value: getEntriesByType,
},
mark: {
configurable: true,
enumerable: false,
Expand Down
3 changes: 3 additions & 0 deletions lib/internal/perf/performance_entry.js
Expand Up @@ -17,6 +17,7 @@ const kType = Symbol('kType');
const kStart = Symbol('kStart');
const kDuration = Symbol('kDuration');
const kDetail = Symbol('kDetail');
const kBufferNext = Symbol('kBufferNext');

function isPerformanceEntry(obj) {
return obj?.[kName] !== undefined;
Expand Down Expand Up @@ -67,6 +68,7 @@ class InternalPerformanceEntry {
this[kStart] = start;
this[kDuration] = duration;
this[kDetail] = detail;
this[kBufferNext] = null;
}
}

Expand All @@ -79,4 +81,5 @@ module.exports = {
InternalPerformanceEntry,
PerformanceEntry,
isPerformanceEntry,
kBufferNext,
};

0 comments on commit 062f8e3

Please sign in to comment.