diff --git a/benchmark/perf_hooks/usertiming.js b/benchmark/perf_hooks/usertiming.js index ae797351ad78cc..24a53a116785df 100644 --- a/benchmark/perf_hooks/usertiming.js +++ b/benchmark/perf_hooks/usertiming.js @@ -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(); } diff --git a/lib/internal/perf/observe.js b/lib/internal/perf/observe.js index c226a7106f0e2a..02d121c9eadffe 100644 --- a/lib/internal/perf/observe.js +++ b/lib/internal/perf/observe.js @@ -9,6 +9,7 @@ const { ArrayPrototypePush, ArrayPrototypeSlice, ArrayPrototypeSort, + Error, ObjectDefineProperties, ObjectFreeze, ObjectKeys, @@ -32,6 +33,7 @@ const { const { InternalPerformanceEntry, isPerformanceEntry, + kBufferNext, } = require('internal/perf/performance_entry'); const { @@ -83,12 +85,17 @@ const kSupportedEntryTypes = ObjectFreeze([ 'mark', 'measure', ]); -const kTimelineEntryTypes = ObjectFreeze([ - 'mark', - 'measure', -]); -const kPerformanceEntryBuffer = new SafeMap(); +// 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; @@ -238,7 +245,7 @@ class PerformanceObserver { maybeIncrementObserverCount(type); if (buffered) { const entries = filterBufferMapByNameAndType(undefined, type); - this[kBuffer].push(...entries); + ArrayPrototypePush(this[kBuffer], ...entries); kPending.add(this); if (kPending.size) queuePending(); @@ -307,50 +314,97 @@ function enqueue(entry) { } const entryType = entry.entryType; - if (!kTimelineEntryTypes.includes(entryType)) { + let buffer; + if (entryType === 'mark') { + buffer = markEntryBuffer; + } else if (entryType === 'measure') { + buffer = measureEntryBuffer; + } else { return; } - const buffer = getEntryBuffer(entryType); - buffer.push(entry); + + 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) { - kPerformanceEntryBuffer.delete(type); + resetBuffer(buffer); return; } - let buffer = getEntryBuffer(type); - buffer = ArrayPrototypeFilter( - buffer, - (entry) => entry.name !== name); - kPerformanceEntryBuffer.set(type, buffer); -} -function getEntryBuffer(type) { - let buffer = kPerformanceEntryBuffer.get(type); - if (buffer === undefined) { - buffer = []; - kPerformanceEntryBuffer.set(type, buffer); + 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]; } - return buffer; + buffer.head = head; + buffer.tail = tail; } function filterBufferMapByNameAndType(name, type) { let bufferList; - if (type !== undefined) { - bufferList = kPerformanceEntryBuffer.get(type) ?? []; + if (type === 'mark') { + bufferList = [markEntryBuffer]; + } else if (type === 'measure') { + bufferList = [measureEntryBuffer]; + } else if (type !== undefined) { + // Unrecognized type; + return []; } else { - bufferList = ArrayFrom(kPerformanceEntryBuffer.values()); + bufferList = [markEntryBuffer, measureEntryBuffer]; } return ArrayPrototypeFlatMap(bufferList, (buffer) => filterBufferByName(buffer, name)); } function filterBufferByName(buffer, name) { - if (name === undefined) { - return buffer; + const arr = []; + for (let entry = buffer.head; entry !== null; entry = entry[kBufferNext]) { + if (name === undefined || entry.name === name) { + ArrayPrototypePush(arr, entry); + } } - return ArrayPrototypeFilter(buffer, (it) => it.name === name); + return arr; } function observerCallback(name, type, startTime, duration, details) { @@ -398,6 +452,20 @@ 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, diff --git a/lib/internal/perf/performance_entry.js b/lib/internal/perf/performance_entry.js index f9f1c9e8966e2d..8fcb0ca3fcdc0c 100644 --- a/lib/internal/perf/performance_entry.js +++ b/lib/internal/perf/performance_entry.js @@ -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; @@ -67,6 +68,7 @@ class InternalPerformanceEntry { this[kStart] = start; this[kDuration] = duration; this[kDetail] = detail; + this[kBufferNext] = null; } } @@ -79,4 +81,5 @@ module.exports = { InternalPerformanceEntry, PerformanceEntry, isPerformanceEntry, + kBufferNext, }; diff --git a/lib/internal/perf/usertiming.js b/lib/internal/perf/usertiming.js index fa4b4e33559db5..b399e1b6867656 100644 --- a/lib/internal/perf/usertiming.js +++ b/lib/internal/perf/usertiming.js @@ -1,12 +1,10 @@ 'use strict'; const { - ObjectSetPrototypeOf, SafeMap, SafeSet, SafeArrayIterator, SymbolToStringTag, - TypeError, } = primordials; const { InternalPerformanceEntry } = require('internal/perf/performance_entry'); @@ -57,15 +55,6 @@ function getMark(name) { return ts; } -// A valid PerformanceMeasureOptions should contains one of the properties. -function isValidStartOrMeasureOptions(options) { - return typeof options === 'object' && - (options.start !== undefined || - options.end !== undefined || - options.duration !== undefined || - options.detail !== undefined); -} - class PerformanceMark extends InternalPerformanceEntry { constructor(name, options) { name = `${name}`; @@ -79,9 +68,10 @@ class PerformanceMark extends InternalPerformanceEntry { throw new ERR_PERFORMANCE_INVALID_TIMESTAMP(startTime); markTimings.set(name, startTime); - const detail = options.detail != null ? - structuredClone(options.detail) : - null; + let detail = options.detail; + detail = detail != null ? + structuredClone(detail) : + detail; super(name, 'mark', startTime, 0, detail); } @@ -91,9 +81,8 @@ class PerformanceMark extends InternalPerformanceEntry { } class PerformanceMeasure extends InternalPerformanceEntry { - constructor() { - // eslint-disable-next-line no-restricted-syntax - throw new TypeError('Illegal constructor'); + constructor(name, start, duration, detail) { + super(name, 'measure', start, duration, detail); } get [SymbolToStringTag]() { @@ -101,17 +90,6 @@ class PerformanceMeasure extends InternalPerformanceEntry { } } -class InternalPerformanceMeasure extends InternalPerformanceEntry { - constructor(name, start, duration, detail) { - super(name, 'measure', start, duration, detail); - } -} - -InternalPerformanceMeasure.prototype.constructor = - PerformanceMeasure.prototype.constructor; -ObjectSetPrototypeOf(InternalPerformanceMeasure.prototype, - PerformanceMeasure.prototype); - function mark(name, options = {}) { const mark = new PerformanceMark(name, options); enqueue(mark); @@ -122,20 +100,23 @@ function calculateStartDuration(startOrMeasureOptions, endMark) { startOrMeasureOptions ??= 0; let start; let end; - const optionsValid = isValidStartOrMeasureOptions(startOrMeasureOptions); + let duration; + let optionsValid = false; + if (typeof startOrMeasureOptions === 'object') { + ({ start, end, duration } = startOrMeasureOptions); + optionsValid = start !== undefined || end !== undefined; + } if (optionsValid) { if (endMark !== undefined) { throw new ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS( 'endMark must not be specified'); } - if (!(startOrMeasureOptions.start !== undefined) && - !(startOrMeasureOptions.end !== undefined)) { + + if (start === undefined && end === undefined) { throw new ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS( 'One of options.start or options.end is required'); } - if (startOrMeasureOptions.start !== undefined && - startOrMeasureOptions.end !== undefined && - startOrMeasureOptions.duration !== undefined) { + if (start !== undefined && end !== undefined && duration !== undefined) { throw new ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS( 'Must not have options.start, options.end, and ' + 'options.duration specified'); @@ -144,32 +125,25 @@ function calculateStartDuration(startOrMeasureOptions, endMark) { if (endMark !== undefined) { end = getMark(endMark); - } else if (optionsValid && startOrMeasureOptions.end !== undefined) { - end = getMark(startOrMeasureOptions.end); - } else if (optionsValid && - startOrMeasureOptions.start !== undefined && - startOrMeasureOptions.duration !== undefined) { - end = getMark(startOrMeasureOptions.start) + - getMark(startOrMeasureOptions.duration); + } else if (optionsValid && end !== undefined) { + end = getMark(end); + } else if (optionsValid && start !== undefined && duration !== undefined) { + end = getMark(start) + getMark(duration); } else { end = now(); } - if (optionsValid && startOrMeasureOptions.start !== undefined) { - start = getMark(startOrMeasureOptions.start); - } else if (optionsValid && - startOrMeasureOptions.duration !== undefined && - startOrMeasureOptions.end !== undefined) { - start = getMark(startOrMeasureOptions.end) - - getMark(startOrMeasureOptions.duration); - } else if (typeof startOrMeasureOptions === 'string') { + if (typeof startOrMeasureOptions === 'string') { start = getMark(startOrMeasureOptions); + } else if (optionsValid && start !== undefined) { + start = getMark(start); + } else if (optionsValid && duration !== undefined && end !== undefined) { + start = end - getMark(duration); } else { start = 0; } - const duration = end - start; - + duration = end - start; return { start, duration }; } @@ -180,8 +154,8 @@ function measure(name, startOrMeasureOptions, endMark) { duration, } = calculateStartDuration(startOrMeasureOptions, endMark); let detail = startOrMeasureOptions?.detail; - detail = detail != null ? structuredClone(detail) : null; - const measure = new InternalPerformanceMeasure(name, start, duration, detail); + detail = detail != null ? structuredClone(detail) : detail; + const measure = new PerformanceMeasure(name, start, duration, detail); enqueue(measure); return measure; } diff --git a/test/parallel/test-perf-hooks-usertiming.js b/test/parallel/test-perf-hooks-usertiming.js index 401d0a6816481a..9539dbd80ae9de 100644 --- a/test/parallel/test-perf-hooks-usertiming.js +++ b/test/parallel/test-perf-hooks-usertiming.js @@ -42,7 +42,8 @@ assert.throws(() => mark(Symbol('a')), { const m = mark('a', { detail }); assert.strictEqual(m.name, 'a'); assert.strictEqual(m.entryType, 'mark'); - assert.strictEqual(m.detail, detail); + // Value of detail is structured cloned. + assert.deepStrictEqual(m.detail, detail); }); clearMarks();