From 7280d7568f70bc688aa9b5926b9e89f3a8df4a1c Mon Sep 17 00:00:00 2001 From: Ruslan Shestopalyuk Date: Thu, 6 Apr 2023 06:35:09 -0700 Subject: [PATCH] Ability to always log certain performance entry types (#36820) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/36820 ## Changelog: [Internal] - A follow-up to the D44584166 (which was abandoned, but this change makes sense nevertheless). This allows to selectively enable logging of certain event types regardless of whether they are observed or not. For now it's marks and measures, but potentially it may be also e.g. navigation/resource entries. Also expands unit tests for the JS side of the `Performance` API correspondingly. Note that "always logged" and "observed" have different semantics. An "always logged" entry won't be sent back from native to JS, unless either: * explicitly requested via `Performance.getEntries*` * actually observed via `PerformanceObserver` Reviewed By: rubennorte Differential Revision: D44712550 fbshipit-source-id: 9557d29db3341ca662759385ccfbc34b6479763d --- .../NativePerformanceObserver.cpp | 10 ++ .../NativePerformanceObserver.h | 5 + .../NativePerformanceObserver.js | 4 + .../Libraries/WebPerformance/Performance.js | 34 +++-- .../WebPerformance/PerformanceEntry.js | 5 + .../PerformanceEntryReporter.cpp | 15 ++- .../WebPerformance/PerformanceEntryReporter.h | 6 +- .../__mocks__/NativePerformanceObserver.js | 24 +++- .../NativePerformanceObserverMock-test.js | 52 ++++++++ .../__tests__/Performance-test.js | 117 ++++++++++++++++++ .../PerformanceEntryReporterTest.cpp | 1 - 11 files changed, 252 insertions(+), 21 deletions(-) create mode 100644 packages/react-native/Libraries/WebPerformance/__tests__/Performance-test.js 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) {