diff --git a/packages/react-native/Libraries/WebPerformance/NativePerformanceObserver.cpp b/packages/react-native/Libraries/WebPerformance/NativePerformanceObserver.cpp index b37255119e7e9f..1e4b91204bf56c 100644 --- a/packages/react-native/Libraries/WebPerformance/NativePerformanceObserver.cpp +++ b/packages/react-native/Libraries/WebPerformance/NativePerformanceObserver.cpp @@ -45,6 +45,16 @@ void NativePerformanceObserver::stopReporting( static_cast(entryType)); } +void NativePerformanceObserver::setIsBuffered( + jsi::Runtime &rt, + std::vector entryTypes, + bool isBuffered) { + for (const int32_t entryType : entryTypes) { + PerformanceEntryReporter::getInstance().setAlwaysLogged( + static_cast(entryType), isBuffered); + } +} + GetPendingEntriesResult NativePerformanceObserver::popPendingEntries( jsi::Runtime &rt) { return PerformanceEntryReporter::getInstance().popPendingEntries(); diff --git a/packages/react-native/Libraries/WebPerformance/NativePerformanceObserver.h b/packages/react-native/Libraries/WebPerformance/NativePerformanceObserver.h index 7f1e4d9161cc76..ab2b0e68047609 100644 --- a/packages/react-native/Libraries/WebPerformance/NativePerformanceObserver.h +++ b/packages/react-native/Libraries/WebPerformance/NativePerformanceObserver.h @@ -63,6 +63,11 @@ class NativePerformanceObserver void stopReporting(jsi::Runtime &rt, int32_t entryType); + void setIsBuffered( + jsi::Runtime &rt, + std::vector entryTypes, + bool isBuffered); + GetPendingEntriesResult popPendingEntries(jsi::Runtime &rt); void setOnPerformanceEntryCallback( diff --git a/packages/react-native/Libraries/WebPerformance/NativePerformanceObserver.js b/packages/react-native/Libraries/WebPerformance/NativePerformanceObserver.js index f7a9031d06c8d7..e6e106e8ccea17 100644 --- a/packages/react-native/Libraries/WebPerformance/NativePerformanceObserver.js +++ b/packages/react-native/Libraries/WebPerformance/NativePerformanceObserver.js @@ -33,6 +33,10 @@ export type GetPendingEntriesResult = {| export interface Spec extends TurboModule { +startReporting: (entryType: RawPerformanceEntryType) => void; +stopReporting: (entryType: RawPerformanceEntryType) => void; + +setIsBuffered: ( + entryTypes: $ReadOnlyArray, + isBuffered: boolean, + ) => void; +popPendingEntries: () => GetPendingEntriesResult; +setOnPerformanceEntryCallback: (callback?: () => void) => void; +logRawEntry: (entry: RawPerformanceEntry) => void; diff --git a/packages/react-native/Libraries/WebPerformance/Performance.js b/packages/react-native/Libraries/WebPerformance/Performance.js index 384d5d3a895e41..26a3d464c17136 100644 --- a/packages/react-native/Libraries/WebPerformance/Performance.js +++ b/packages/react-native/Libraries/WebPerformance/Performance.js @@ -18,7 +18,7 @@ import EventCounts from './EventCounts'; import MemoryInfo from './MemoryInfo'; import NativePerformance from './NativePerformance'; import NativePerformanceObserver from './NativePerformanceObserver'; -import {PerformanceEntry} from './PerformanceEntry'; +import {ALWAYS_LOGGED_ENTRY_TYPES, PerformanceEntry} from './PerformanceEntry'; import {warnNoNativePerformanceObserver} from './PerformanceObserver'; import { performanceEntryTypeToRaw, @@ -43,6 +43,15 @@ const getCurrentTimeStamp: () => HighResTimeStamp = global.nativePerformanceNow ? global.nativePerformanceNow : () => Date.now(); +// We want some of the performance entry types to be always logged, +// even if they are not currently observed - this is either to be able to +// retrieve them at any time via Performance.getEntries* or to refer by other entries +// (such as when measures may refer to marks, even if the latter are not observed) +NativePerformanceObserver?.setIsBuffered( + ALWAYS_LOGGED_ENTRY_TYPES.map(performanceEntryTypeToRaw), + true, +); + export class PerformanceMark extends PerformanceEntry { detail: DetailType; @@ -261,7 +270,7 @@ export default class Performance { * https://www.w3.org/TR/performance-timeline/#extensions-to-the-performance-interface */ getEntries(): PerformanceEntryList { - if (!NativePerformanceObserver?.clearEntries) { + if (!NativePerformanceObserver?.getEntries) { warnNoNativePerformanceObserver(); return []; } @@ -269,14 +278,16 @@ export default class Performance { } getEntriesByType(entryType: PerformanceEntryType): PerformanceEntryList { - if (entryType !== 'mark' && entryType !== 'measure') { - console.log( - `Performance.getEntriesByType: Only valid for 'mark' and 'measure' entry types, got ${entryType}`, + if (!ALWAYS_LOGGED_ENTRY_TYPES.includes(entryType)) { + console.warn( + `Performance.getEntriesByType: Only valid for ${JSON.stringify( + ALWAYS_LOGGED_ENTRY_TYPES, + )} entry types, got ${entryType}`, ); return []; } - if (!NativePerformanceObserver?.clearEntries) { + if (!NativePerformanceObserver?.getEntries) { warnNoNativePerformanceObserver(); return []; } @@ -291,16 +302,17 @@ export default class Performance { ): PerformanceEntryList { if ( entryType !== undefined && - entryType !== 'mark' && - entryType !== 'measure' + !ALWAYS_LOGGED_ENTRY_TYPES.includes(entryType) ) { - console.log( - `Performance.getEntriesByName: Only valid for 'mark' and 'measure' entry types, got ${entryType}`, + console.warn( + `Performance.getEntriesByName: Only valid for ${JSON.stringify( + ALWAYS_LOGGED_ENTRY_TYPES, + )} entry types, got ${entryType}`, ); return []; } - if (!NativePerformanceObserver?.clearEntries) { + if (!NativePerformanceObserver?.getEntries) { warnNoNativePerformanceObserver(); return []; } diff --git a/packages/react-native/Libraries/WebPerformance/PerformanceEntry.js b/packages/react-native/Libraries/WebPerformance/PerformanceEntry.js index 51396663e44a4a..bc2cadff20209d 100644 --- a/packages/react-native/Libraries/WebPerformance/PerformanceEntry.js +++ b/packages/react-native/Libraries/WebPerformance/PerformanceEntry.js @@ -11,6 +11,11 @@ export type HighResTimeStamp = number; export type PerformanceEntryType = 'mark' | 'measure' | 'event'; +export const ALWAYS_LOGGED_ENTRY_TYPES: $ReadOnlyArray = [ + 'mark', + 'measure', +]; + export class PerformanceEntry { name: string; entryType: PerformanceEntryType; diff --git a/packages/react-native/Libraries/WebPerformance/PerformanceEntryReporter.cpp b/packages/react-native/Libraries/WebPerformance/PerformanceEntryReporter.cpp index 4bffe9e563f286..024178f344a39e 100644 --- a/packages/react-native/Libraries/WebPerformance/PerformanceEntryReporter.cpp +++ b/packages/react-native/Libraries/WebPerformance/PerformanceEntryReporter.cpp @@ -36,6 +36,13 @@ void PerformanceEntryReporter::startReporting(PerformanceEntryType entryType) { buffer.durationThreshold = DEFAULT_DURATION_THRESHOLD; } +void PerformanceEntryReporter::setAlwaysLogged( + PerformanceEntryType entryType, + bool isAlwaysLogged) { + auto &buffer = getBuffer(entryType); + buffer.isAlwaysLogged = isAlwaysLogged; +} + void PerformanceEntryReporter::setDurationThreshold( PerformanceEntryType entryType, double durationThreshold) { @@ -82,7 +89,7 @@ void PerformanceEntryReporter::logEntry(const RawPerformanceEntry &entry) { eventCounts_[entry.name]++; } - if (!isReporting(entryType)) { + if (!isReporting(entryType) && !isAlwaysLogged(entryType)) { return; } @@ -328,7 +335,7 @@ static const SupportedEventTypeRegistry &getSupportedEvents() { } EventTag PerformanceEntryReporter::onEventStart(const char *name) { - if (!isReportingEvents()) { + if (!isReporting(PerformanceEntryType::EVENT)) { return 0; } const auto &supportedEvents = getSupportedEvents(); @@ -355,7 +362,7 @@ EventTag PerformanceEntryReporter::onEventStart(const char *name) { } void PerformanceEntryReporter::onEventDispatch(EventTag tag) { - if (!isReportingEvents() || tag == 0) { + if (!isReporting(PerformanceEntryType::EVENT) || tag == 0) { return; } auto timeStamp = JSExecutor::performanceNow(); @@ -369,7 +376,7 @@ void PerformanceEntryReporter::onEventDispatch(EventTag tag) { } void PerformanceEntryReporter::onEventEnd(EventTag tag) { - if (!isReportingEvents() || tag == 0) { + if (!isReporting(PerformanceEntryType::EVENT) || tag == 0) { return; } auto timeStamp = JSExecutor::performanceNow(); diff --git a/packages/react-native/Libraries/WebPerformance/PerformanceEntryReporter.h b/packages/react-native/Libraries/WebPerformance/PerformanceEntryReporter.h index 0704cb0558071e..4ce7794078f8d8 100644 --- a/packages/react-native/Libraries/WebPerformance/PerformanceEntryReporter.h +++ b/packages/react-native/Libraries/WebPerformance/PerformanceEntryReporter.h @@ -49,6 +49,7 @@ constexpr size_t DEFAULT_MAX_BUFFER_SIZE = 1024; struct PerformanceEntryBuffer { BoundedConsumableBuffer entries{DEFAULT_MAX_BUFFER_SIZE}; bool isReporting{false}; + bool isAlwaysLogged{false}; double durationThreshold{DEFAULT_DURATION_THRESHOLD}; bool hasNameLookup{false}; PerformanceEntryRegistryType nameLookup; @@ -80,6 +81,7 @@ class PerformanceEntryReporter : public EventLogger { void startReporting(PerformanceEntryType entryType); void stopReporting(PerformanceEntryType entryType); void stopReporting(); + void setAlwaysLogged(PerformanceEntryType entryType, bool isAlwaysLogged); void setDurationThreshold( PerformanceEntryType entryType, double durationThreshold); @@ -101,8 +103,8 @@ class PerformanceEntryReporter : public EventLogger { return getBuffer(entryType).isReporting; } - bool isReportingEvents() const { - return isReporting(PerformanceEntryType::EVENT); + bool isAlwaysLogged(PerformanceEntryType entryType) const { + return getBuffer(entryType).isAlwaysLogged; } uint32_t getDroppedEntryCount() const { diff --git a/packages/react-native/Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js b/packages/react-native/Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js index 1b9c3142f387d2..dbf6eeebc12b15 100644 --- a/packages/react-native/Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js +++ b/packages/react-native/Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js @@ -18,6 +18,7 @@ import type { import {RawPerformanceEntryTypeValues} from '../RawPerformanceEntry'; const reportingType: Set = new Set(); +const isAlwaysLogged: Set = new Set(); const eventCounts: Map = new Map(); const durationThresholds: Map = new Map(); let entries: Array = []; @@ -33,6 +34,19 @@ const NativePerformanceObserverMock: NativePerformanceObserver = { durationThresholds.delete(entryType); }, + setIsBuffered: ( + entryTypes: $ReadOnlyArray, + isBuffered: boolean, + ) => { + for (const entryType of entryTypes) { + if (isBuffered) { + isAlwaysLogged.add(entryType); + } else { + isAlwaysLogged.delete(entryType); + } + } + }, + popPendingEntries: (): GetPendingEntriesResult => { const res = entries; entries = []; @@ -47,7 +61,10 @@ const NativePerformanceObserverMock: NativePerformanceObserver = { }, logRawEntry: (entry: RawPerformanceEntry) => { - if (reportingType.has(entry.entryType)) { + if ( + reportingType.has(entry.entryType) || + isAlwaysLogged.has(entry.entryType) + ) { const durationThreshold = durationThresholds.get(entry.entryType); if ( durationThreshold !== undefined && @@ -81,8 +98,9 @@ const NativePerformanceObserverMock: NativePerformanceObserver = { clearEntries: (entryType: RawPerformanceEntryType, entryName?: string) => { entries = entries.filter( e => - e.entryType === entryType && - (entryName == null || e.name === entryName), + (entryType !== RawPerformanceEntryTypeValues.UNDEFINED && + e.entryType !== entryType) || + (entryName != null && e.name !== entryName), ); }, diff --git a/packages/react-native/Libraries/WebPerformance/__tests__/NativePerformanceObserverMock-test.js b/packages/react-native/Libraries/WebPerformance/__tests__/NativePerformanceObserverMock-test.js index 0fe3b7720f1a1e..a4d8a575d74e3f 100644 --- a/packages/react-native/Libraries/WebPerformance/__tests__/NativePerformanceObserverMock-test.js +++ b/packages/react-native/Libraries/WebPerformance/__tests__/NativePerformanceObserverMock-test.js @@ -53,4 +53,56 @@ describe('NativePerformanceObserver', () => { NativePerformanceObserverMock.stopReporting('mark'); }); + + it('correctly clears/gets entries', async () => { + NativePerformanceObserverMock.logRawEntry({ + name: 'mark1', + entryType: RawPerformanceEntryTypeValues.MARK, + startTime: 0, + duration: 0, + }); + + NativePerformanceObserverMock.logRawEntry({ + name: 'event1', + entryType: RawPerformanceEntryTypeValues.EVENT, + startTime: 0, + duration: 0, + }); + + NativePerformanceObserverMock.clearEntries( + RawPerformanceEntryTypeValues.UNDEFINED, + ); + + expect(NativePerformanceObserverMock.getEntries()).toStrictEqual([]); + + NativePerformanceObserverMock.logRawEntry({ + name: 'entry1', + entryType: RawPerformanceEntryTypeValues.MARK, + startTime: 0, + duration: 0, + }); + + NativePerformanceObserverMock.logRawEntry({ + name: 'entry2', + entryType: RawPerformanceEntryTypeValues.MARK, + startTime: 0, + duration: 0, + }); + + NativePerformanceObserverMock.logRawEntry({ + name: 'entry1', + entryType: RawPerformanceEntryTypeValues.EVENT, + startTime: 0, + duration: 0, + }); + + NativePerformanceObserverMock.clearEntries( + RawPerformanceEntryTypeValues.UNDEFINED, + 'entry1', + ); + + expect( + NativePerformanceObserverMock.getEntries().map(e => e.name), + ).toStrictEqual(['entry2']); + }); }); diff --git a/packages/react-native/Libraries/WebPerformance/__tests__/Performance-test.js b/packages/react-native/Libraries/WebPerformance/__tests__/Performance-test.js new file mode 100644 index 00000000000000..a5e85831ae3a5f --- /dev/null +++ b/packages/react-native/Libraries/WebPerformance/__tests__/Performance-test.js @@ -0,0 +1,117 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @oncall react_native + */ + +const Performance = require('../Performance').default; + +jest.mock( + '../NativePerformance', + () => require('../__mocks__/NativePerformance').default, +); + +jest.mock( + '../NativePerformanceObserver', + () => require('../__mocks__/NativePerformanceObserver').default, +); + +describe('Performance', () => { + it('clearEntries removes correct entry types', async () => { + const performance = new Performance(); + performance.mark('entry1', 0, 0); + performance.mark('mark2', 0, 0); + + performance.measure('entry1', {start: 0, duration: 0}); + performance.measure('measure2', {start: 0, duration: 0}); + + performance.clearMarks(); + + expect(performance.getEntries().map(e => e.name)).toStrictEqual([ + 'entry1', + 'measure2', + ]); + + performance.mark('entry2', 0, 0); + performance.mark('mark3', 0, 0); + + performance.clearMeasures(); + + expect(performance.getEntries().map(e => e.name)).toStrictEqual([ + 'entry2', + 'mark3', + ]); + + performance.clearMarks(); + + expect(performance.getEntries().map(e => e.name)).toStrictEqual([]); + }); + + it('getEntries only works with allowed entry types', async () => { + const performance = new Performance(); + performance.clearMarks(); + performance.clearMeasures(); + + performance.mark('entry1', 0, 0); + performance.mark('mark2', 0, 0); + + jest.spyOn(console, 'warn').mockImplementation(); + + performance.getEntriesByType('mark'); + expect(console.warn).not.toHaveBeenCalled(); + + performance.getEntriesByType('measure'); + expect(console.warn).not.toHaveBeenCalled(); + + performance.getEntriesByName('entry1'); + expect(console.warn).not.toHaveBeenCalled(); + + performance.getEntriesByName('entry1', 'event'); + expect(console.warn).toHaveBeenCalled(); + + performance.getEntriesByName('entry1', 'mark'); + expect(console.warn).toHaveBeenCalled(); + + performance.getEntriesByType('event'); + expect(console.warn).toHaveBeenCalled(); + }); + + it('getEntries works with marks and measures', async () => { + const performance = new Performance(); + performance.clearMarks(); + performance.clearMeasures(); + + performance.mark('entry1', 0, 0); + performance.mark('mark2', 0, 0); + + performance.measure('entry1', {start: 0, duration: 0}); + performance.measure('measure2', {start: 0, duration: 0}); + + expect(performance.getEntries().map(e => e.name)).toStrictEqual([ + 'entry1', + 'mark2', + 'entry1', + 'measure2', + ]); + + expect(performance.getEntriesByType('mark').map(e => e.name)).toStrictEqual( + ['entry1', 'mark2'], + ); + + expect( + performance.getEntriesByType('measure').map(e => e.name), + ).toStrictEqual(['entry1', 'measure2']); + + expect( + performance.getEntriesByName('entry1').map(e => e.entryType), + ).toStrictEqual(['mark', 'measure']); + + expect( + performance.getEntriesByName('entry1', 'measure').map(e => e.entryType), + ).toStrictEqual(['measure']); + }); +}); diff --git a/packages/react-native/Libraries/WebPerformance/__tests__/PerformanceEntryReporterTest.cpp b/packages/react-native/Libraries/WebPerformance/__tests__/PerformanceEntryReporterTest.cpp index 40a3affe0f220e..9a6842eec2c623 100644 --- a/packages/react-native/Libraries/WebPerformance/__tests__/PerformanceEntryReporterTest.cpp +++ b/packages/react-native/Libraries/WebPerformance/__tests__/PerformanceEntryReporterTest.cpp @@ -44,7 +44,6 @@ TEST(PerformanceEntryReporter, PerformanceEntryReporterTestStartReporting) { ASSERT_TRUE(reporter.isReporting(PerformanceEntryType::MEASURE)); ASSERT_FALSE(reporter.isReporting(PerformanceEntryType::EVENT)); - ASSERT_FALSE(reporter.isReportingEvents()); } TEST(PerformanceEntryReporter, PerformanceEntryReporterTestStopReporting) {