diff --git a/Libraries/WebPerformance/NativePerformanceObserver.cpp b/Libraries/WebPerformance/NativePerformanceObserver.cpp index e0701acfe6..200140fdae 100644 --- a/Libraries/WebPerformance/NativePerformanceObserver.cpp +++ b/Libraries/WebPerformance/NativePerformanceObserver.cpp @@ -59,4 +59,12 @@ NativePerformanceObserver::getEventCounts(jsi::Runtime &rt) { eventCounts.begin(), eventCounts.end()); } +void NativePerformanceObserver::setDurationThreshold( + jsi::Runtime &rt, + int32_t entryType, + double durationThreshold) { + PerformanceEntryReporter::getInstance().setDurationThreshold( + static_cast(entryType), durationThreshold); +} + } // namespace facebook::react diff --git a/Libraries/WebPerformance/NativePerformanceObserver.h b/Libraries/WebPerformance/NativePerformanceObserver.h index 5254628d46..577bdee0f7 100644 --- a/Libraries/WebPerformance/NativePerformanceObserver.h +++ b/Libraries/WebPerformance/NativePerformanceObserver.h @@ -74,6 +74,11 @@ class NativePerformanceObserver std::vector> getEventCounts( jsi::Runtime &rt); + void setDurationThreshold( + jsi::Runtime &rt, + int32_t entryType, + double durationThreshold); + private: }; diff --git a/Libraries/WebPerformance/NativePerformanceObserver.js b/Libraries/WebPerformance/NativePerformanceObserver.js index 964c4e9993..86634b6021 100644 --- a/Libraries/WebPerformance/NativePerformanceObserver.js +++ b/Libraries/WebPerformance/NativePerformanceObserver.js @@ -37,6 +37,10 @@ export interface Spec extends TurboModule { +setOnPerformanceEntryCallback: (callback?: () => void) => void; +logRawEntry: (entry: RawPerformanceEntry) => void; +getEventCounts: () => $ReadOnlyArray<[string, number]>; + +setDurationThreshold: ( + entryType: RawPerformanceEntryType, + durationThreshold: number, + ) => void; } export default (TurboModuleRegistry.get( diff --git a/Libraries/WebPerformance/PerformanceEntryReporter.cpp b/Libraries/WebPerformance/PerformanceEntryReporter.cpp index 7c524ae0ec..a741857de9 100644 --- a/Libraries/WebPerformance/PerformanceEntryReporter.cpp +++ b/Libraries/WebPerformance/PerformanceEntryReporter.cpp @@ -31,8 +31,17 @@ void PerformanceEntryReporter::setReportingCallback( } void PerformanceEntryReporter::startReporting(PerformanceEntryType entryType) { - reportingType_[static_cast(entryType)] = true; + int entryTypeIdx = static_cast(entryType); + reportingType_[entryTypeIdx] = true; + durationThreshold_[entryTypeIdx] = DEFAULT_DURATION_THRESHOLD; } + +void PerformanceEntryReporter::setDurationThreshold( + PerformanceEntryType entryType, + double durationThreshold) { + durationThreshold_[static_cast(entryType)] = durationThreshold; +} + void PerformanceEntryReporter::stopReporting(PerformanceEntryType entryType) { reportingType_[static_cast(entryType)] = false; } @@ -56,6 +65,11 @@ void PerformanceEntryReporter::logEntry(const RawPerformanceEntry &entry) { return; } + if (entry.duration < durationThreshold_[entry.entryType]) { + // The entries duration is lower than the desired reporting threshold, skip + return; + } + std::lock_guard lock(entriesMutex_); if (entries_.size() == MAX_ENTRY_BUFFER_SIZE) { diff --git a/Libraries/WebPerformance/PerformanceEntryReporter.h b/Libraries/WebPerformance/PerformanceEntryReporter.h index 655f58ceab..721a9ac698 100644 --- a/Libraries/WebPerformance/PerformanceEntryReporter.h +++ b/Libraries/WebPerformance/PerformanceEntryReporter.h @@ -44,6 +44,8 @@ using PerformanceMarkRegistryType = std:: // memory for the sake of the "Performance.measure" mark name lookup constexpr size_t MARKS_BUFFER_SIZE = 1024; +constexpr double DEFAULT_DURATION_THRESHOLD = 0.0; + enum class PerformanceEntryType { UNDEFINED = 0, MARK = 1, @@ -66,6 +68,9 @@ class PerformanceEntryReporter : public EventLogger { void setReportingCallback(std::optional> callback); void startReporting(PerformanceEntryType entryType); void stopReporting(PerformanceEntryType entryType); + void setDurationThreshold( + PerformanceEntryType entryType, + double durationThreshold); GetPendingEntriesResult popPendingEntries(); void logEntry(const RawPerformanceEntry &entry); @@ -122,6 +127,8 @@ class PerformanceEntryReporter : public EventLogger { std::mutex entriesMutex_; std::array reportingType_{false}; std::unordered_map eventCounts_; + std::array durationThreshold_{ + DEFAULT_DURATION_THRESHOLD}; // Mark registry for "measure" lookup PerformanceMarkRegistryType marksRegistry_; diff --git a/Libraries/WebPerformance/PerformanceObserver.js b/Libraries/WebPerformance/PerformanceObserver.js index ca7d081a0f..776c9f0ced 100644 --- a/Libraries/WebPerformance/PerformanceObserver.js +++ b/Libraries/WebPerformance/PerformanceObserver.js @@ -8,7 +8,7 @@ * @flow strict */ -import type {PerformanceEntryType} from './PerformanceEntry'; +import type {HighResTimeStamp, PerformanceEntryType} from './PerformanceEntry'; import warnOnce from '../Utilities/warnOnce'; import NativePerformanceObserver from './NativePerformanceObserver'; @@ -62,11 +62,13 @@ export type PerformanceObserverInit = } | { type: PerformanceEntryType, + durationThreshold?: HighResTimeStamp, }; type PerformanceObserverConfig = {| callback: PerformanceObserverCallback, - entryTypes: $ReadOnlySet, + // Map of {entryType: durationThreshold} + entryTypes: $ReadOnlyMap, |}; const observerCountPerEntryType: Map = new Map(); @@ -87,9 +89,13 @@ const onPerformanceEntry = () => { } const entries = rawEntries.map(rawToPerformanceEntry); for (const [observer, observerConfig] of registeredObservers.entries()) { - const entriesForObserver: PerformanceEntryList = entries.filter( - entry => observerConfig.entryTypes.has(entry.entryType) !== -1, - ); + const entriesForObserver: PerformanceEntryList = entries.filter(entry => { + if (!observerConfig.entryTypes.has(entry.entryType)) { + return false; + } + const durationThreshold = observerConfig.entryTypes.get(entry.entryType); + return entry.duration >= (durationThreshold ?? 0); + }); observerConfig.callback( new PerformanceObserverEntryList(entriesForObserver), observer, @@ -105,6 +111,24 @@ export function warnNoNativePerformanceObserver() { ); } +function applyDurationThresholds() { + const durationThresholds: Map = Array.from( + registeredObservers.values(), + ) + .map(config => config.entryTypes) + .reduce( + (accumulator, currentValue) => union(accumulator, currentValue), + new Map(), + ); + + for (const [entryType, durationThreshold] of durationThresholds) { + NativePerformanceObserver?.setDurationThreshold( + performanceEntryTypeToRaw(entryType), + durationThreshold ?? 0, + ); + } +} + /** * Implementation of the PerformanceObserver interface for RN, * corresponding to the standard in https://www.w3.org/TR/performance-timeline/ @@ -145,10 +169,14 @@ export default class PerformanceObserver { if (options.entryTypes) { this._type = 'multiple'; - requestedEntryTypes = new Set(options.entryTypes); + requestedEntryTypes = new Map( + options.entryTypes.map(t => [t, undefined]), + ); } else { this._type = 'single'; - requestedEntryTypes = new Set([options.type]); + requestedEntryTypes = new Map([ + [options.type, options.durationThreshold], + ]); } // The same observer may receive multiple calls to "observe", so we need @@ -178,19 +206,22 @@ export default class PerformanceObserver { // We only need to start listenening to new entry types being observed in // this observer. const newEntryTypes = currentEntryTypes - ? difference(requestedEntryTypes, currentEntryTypes) - : requestedEntryTypes; + ? difference( + new Set(requestedEntryTypes.keys()), + new Set(currentEntryTypes.keys()), + ) + : new Set(requestedEntryTypes.keys()); for (const type of newEntryTypes) { if (!observerCountPerEntryType.has(type)) { - NativePerformanceObserver.startReporting( - performanceEntryTypeToRaw(type), - ); + const rawType = performanceEntryTypeToRaw(type); + NativePerformanceObserver.startReporting(rawType); } observerCountPerEntryType.set( type, (observerCountPerEntryType.get(type) ?? 0) + 1, ); } + applyDurationThresholds(); } disconnect(): void { @@ -205,7 +236,7 @@ export default class PerformanceObserver { } // Disconnect this observer - for (const type of observerConfig.entryTypes) { + for (const type of observerConfig.entryTypes.keys()) { const numberOfObserversForThisType = observerCountPerEntryType.get(type) ?? 0; if (numberOfObserversForThisType === 1) { @@ -224,10 +255,12 @@ export default class PerformanceObserver { NativePerformanceObserver.setOnPerformanceEntryCallback(undefined); isOnPerformanceEntryCallbackSet = false; } + + applyDurationThresholds(); } _validateObserveOptions(options: PerformanceObserverInit): void { - const {type, entryTypes} = options; + const {type, entryTypes, durationThreshold} = options; if (!type && !entryTypes) { throw new TypeError( @@ -252,14 +285,32 @@ export default class PerformanceObserver { "Failed to execute 'observe' on 'PerformanceObserver': This PerformanceObserver has performed observe({type:...}, therefore it cannot perform observe({entryTypes:...})", ); } + + if (entryTypes && durationThreshold !== undefined) { + throw new TypeError( + "Failed to execute 'observe' on 'PerformanceObserver': An observe() call must not include both entryTypes and durationThreshold arguments.", + ); + } } static supportedEntryTypes: $ReadOnlyArray = Object.freeze(['mark', 'measure', 'event']); } -function union(a: $ReadOnlySet, b: $ReadOnlySet): Set { - return new Set([...a, ...b]); +// As a Set union, except if value exists in both, we take minimum +function union( + a: $ReadOnlyMap, + b: $ReadOnlyMap, +): Map { + const res = new Map(); + for (const [k, v] of a) { + if (!b.has(k)) { + res.set(k, v); + } else { + res.set(k, Math.min(v ?? 0, b.get(k) ?? 0)); + } + } + return res; } function difference(a: $ReadOnlySet, b: $ReadOnlySet): Set { diff --git a/Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js b/Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js index 4379e30ab1..98c3313bf0 100644 --- a/Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js +++ b/Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js @@ -19,6 +19,7 @@ import {RawPerformanceEntryTypeValues} from '../RawPerformanceEntry'; const reportingType: Set = new Set(); const eventCounts: Map = new Map(); +const durationThresholds: Map = new Map(); let entries: Array = []; let onPerformanceEntryCallback: ?() => void; @@ -29,6 +30,7 @@ const NativePerformanceObserverMock: NativePerformanceObserver = { stopReporting: (entryType: RawPerformanceEntryType) => { reportingType.delete(entryType); + durationThresholds.delete(entryType); }, popPendingEntries: (): GetPendingEntriesResult => { @@ -46,6 +48,13 @@ const NativePerformanceObserverMock: NativePerformanceObserver = { logRawEntry: (entry: RawPerformanceEntry) => { if (reportingType.has(entry.entryType)) { + const durationThreshold = durationThresholds.get(entry.entryType); + if ( + durationThreshold !== undefined && + entry.duration < durationThreshold + ) { + return; + } entries.push(entry); // $FlowFixMe[incompatible-call] global.queueMicrotask(() => { @@ -61,6 +70,13 @@ const NativePerformanceObserverMock: NativePerformanceObserver = { getEventCounts: (): $ReadOnlyArray<[string, number]> => { return Array.from(eventCounts.entries()); }, + + setDurationThreshold: ( + entryType: RawPerformanceEntryType, + durationThreshold: number, + ) => { + durationThresholds.set(entryType, durationThreshold); + }, }; export default NativePerformanceObserverMock; diff --git a/Libraries/WebPerformance/__tests__/PerformanceObserver-test.js b/Libraries/WebPerformance/__tests__/PerformanceObserver-test.js index 060b9a9ab9..a6e6a37be9 100644 --- a/Libraries/WebPerformance/__tests__/PerformanceObserver-test.js +++ b/Libraries/WebPerformance/__tests__/PerformanceObserver-test.js @@ -26,6 +26,7 @@ describe('PerformanceObserver', () => { let totalEntries = 0; const observer = new PerformanceObserver((list, _observer) => { + expect(_observer).toBe(observer); const entries = list.getEntries(); expect(entries).toHaveLength(1); const entry = entries[0]; @@ -46,4 +47,162 @@ describe('PerformanceObserver', () => { expect(totalEntries).toBe(1); observer.disconnect(); }); + + it('prevents durationThreshold to be used together with entryTypes', async () => { + const observer = new PerformanceObserver((list, _observer) => {}); + + expect(() => + observer.observe({entryTypes: ['mark'], durationThreshold: 100}), + ).toThrow(); + }); + + it('handles durationThreshold argument as expected', async () => { + let entries = []; + const observer = new PerformanceObserver((list, _observer) => { + entries = [...entries, ...list.getEntries()]; + }); + + observer.observe({type: 'mark', durationThreshold: 100}); + + NativePerformanceObserver.logRawEntry({ + name: 'mark1', + entryType: RawPerformanceEntryTypeValues.MARK, + startTime: 0, + duration: 200, + }); + + NativePerformanceObserver.logRawEntry({ + name: 'mark2', + entryType: RawPerformanceEntryTypeValues.MARK, + startTime: 0, + duration: 20, + }); + + NativePerformanceObserver.logRawEntry({ + name: 'mark3', + entryType: RawPerformanceEntryTypeValues.MARK, + startTime: 0, + duration: 100, + }); + + NativePerformanceObserver.logRawEntry({ + name: 'mark4', + entryType: RawPerformanceEntryTypeValues.MARK, + startTime: 0, + duration: 500, + }); + + await jest.runAllTicks(); + expect(entries).toHaveLength(3); + expect(entries.map(e => e.name)).toStrictEqual(['mark1', 'mark3', 'mark4']); + }); + + it('correctly works with multiple PerformanceObservers with durationThreshold', async () => { + let entries1 = []; + const observer1 = new PerformanceObserver((list, _observer) => { + entries1 = [...entries1, ...list.getEntries()]; + }); + + let entries2 = []; + const observer2 = new PerformanceObserver((list, _observer) => { + entries2 = [...entries2, ...list.getEntries()]; + }); + + let entries3 = []; + const observer3 = new PerformanceObserver((list, _observer) => { + entries3 = [...entries3, ...list.getEntries()]; + }); + + let entries4 = []; + const observer4 = new PerformanceObserver((list, _observer) => { + entries4 = [...entries4, ...list.getEntries()]; + }); + + observer2.observe({type: 'mark', durationThreshold: 200}); + observer1.observe({type: 'mark', durationThreshold: 100}); + observer3.observe({type: 'mark', durationThreshold: 300}); + observer3.observe({type: 'measure', durationThreshold: 500}); + observer4.observe({entryTypes: ['mark', 'measure']}); + + NativePerformanceObserver.logRawEntry({ + name: 'mark1', + entryType: RawPerformanceEntryTypeValues.MARK, + startTime: 0, + duration: 200, + }); + + NativePerformanceObserver.logRawEntry({ + name: 'mark2', + entryType: RawPerformanceEntryTypeValues.MARK, + startTime: 0, + duration: 20, + }); + + NativePerformanceObserver.logRawEntry({ + name: 'mark3', + entryType: RawPerformanceEntryTypeValues.MARK, + startTime: 0, + duration: 100, + }); + + NativePerformanceObserver.logRawEntry({ + name: 'mark4', + entryType: RawPerformanceEntryTypeValues.MARK, + startTime: 0, + duration: 500, + }); + + await jest.runAllTicks(); + observer1.disconnect(); + + NativePerformanceObserver.logRawEntry({ + name: 'mark5', + entryType: RawPerformanceEntryTypeValues.MARK, + startTime: 0, + duration: 200, + }); + + NativePerformanceObserver.logRawEntry({ + name: 'mark6', + entryType: RawPerformanceEntryTypeValues.MARK, + startTime: 0, + duration: 300, + }); + + await jest.runAllTicks(); + observer3.disconnect(); + + NativePerformanceObserver.logRawEntry({ + name: 'mark7', + entryType: RawPerformanceEntryTypeValues.MARK, + startTime: 0, + duration: 200, + }); + + await jest.runAllTicks(); + observer4.disconnect(); + + expect(entries1.map(e => e.name)).toStrictEqual([ + 'mark1', + 'mark3', + 'mark4', + ]); + expect(entries2.map(e => e.name)).toStrictEqual([ + 'mark1', + 'mark4', + 'mark5', + 'mark6', + 'mark7', + ]); + expect(entries3.map(e => e.name)).toStrictEqual(['mark4', 'mark6']); + expect(entries4.map(e => e.name)).toStrictEqual([ + 'mark1', + 'mark2', + 'mark3', + 'mark4', + 'mark5', + 'mark6', + 'mark7', + ]); + }); });