diff --git a/Libraries/WebPerformance/EventCounts.js b/Libraries/WebPerformance/EventCounts.js new file mode 100644 index 00000000000..27ece857093 --- /dev/null +++ b/Libraries/WebPerformance/EventCounts.js @@ -0,0 +1,78 @@ +/** + * 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. + * + * @flow strict + * @format + */ + +import NativePerformanceObserver from './NativePerformanceObserver'; +import {warnNoNativePerformanceObserver} from './PerformanceObserver'; + +type EventCountsForEachCallbackType = + | (() => void) + | ((value: number) => void) + | ((value: number, key: string) => void) + | ((value: number, key: string, map: Map) => void); + +let cachedEventCounts: ?Map; + +function getCachedEventCounts(): Map { + if (cachedEventCounts) { + return cachedEventCounts; + } + if (!NativePerformanceObserver) { + warnNoNativePerformanceObserver(); + return new Map(); + } + + cachedEventCounts = new Map( + NativePerformanceObserver.getEventCounts(), + ); + // $FlowFixMe[incompatible-call] + global.queueMicrotask(() => { + // To be consistent with the calls to the API from the same task, + // but also not to refetch the data from native too often, + // schedule to invalidate the cache later, + // after the current task is guaranteed to have finished. + cachedEventCounts = null; + }); + return cachedEventCounts ?? new Map(); +} +/** + * Implementation of the EventCounts Web Performance API + * corresponding to the standard in + * https://www.w3.org/TR/event-timing/#eventcounts + */ +export default class EventCounts { + // flowlint unsafe-getters-setters:off + get size(): number { + return getCachedEventCounts().size; + } + + entries(): Iterator<[string, number]> { + return getCachedEventCounts().entries(); + } + + forEach(callback: EventCountsForEachCallbackType): void { + return getCachedEventCounts().forEach(callback); + } + + get(key: string): ?number { + return getCachedEventCounts().get(key); + } + + has(key: string): boolean { + return getCachedEventCounts().has(key); + } + + keys(): Iterator { + return getCachedEventCounts().keys(); + } + + values(): Iterator { + return getCachedEventCounts().values(); + } +} diff --git a/Libraries/WebPerformance/NativePerformanceObserver.cpp b/Libraries/WebPerformance/NativePerformanceObserver.cpp index c1b6265536b..e0701acfe6a 100644 --- a/Libraries/WebPerformance/NativePerformanceObserver.cpp +++ b/Libraries/WebPerformance/NativePerformanceObserver.cpp @@ -51,4 +51,12 @@ void NativePerformanceObserver::logRawEntry( PerformanceEntryReporter::getInstance().logEntry(entry); } +std::vector> +NativePerformanceObserver::getEventCounts(jsi::Runtime &rt) { + const auto &eventCounts = + PerformanceEntryReporter::getInstance().getEventCounts(); + return std::vector>( + eventCounts.begin(), eventCounts.end()); +} + } // namespace facebook::react diff --git a/Libraries/WebPerformance/NativePerformanceObserver.h b/Libraries/WebPerformance/NativePerformanceObserver.h index 5305a6ce35b..5254628d460 100644 --- a/Libraries/WebPerformance/NativePerformanceObserver.h +++ b/Libraries/WebPerformance/NativePerformanceObserver.h @@ -71,6 +71,9 @@ class NativePerformanceObserver void logRawEntry(jsi::Runtime &rt, RawPerformanceEntry entry); + std::vector> getEventCounts( + jsi::Runtime &rt); + private: }; diff --git a/Libraries/WebPerformance/NativePerformanceObserver.js b/Libraries/WebPerformance/NativePerformanceObserver.js index 4af889249a7..964c4e99933 100644 --- a/Libraries/WebPerformance/NativePerformanceObserver.js +++ b/Libraries/WebPerformance/NativePerformanceObserver.js @@ -35,6 +35,8 @@ export interface Spec extends TurboModule { +stopReporting: (entryType: RawPerformanceEntryType) => void; +popPendingEntries: () => GetPendingEntriesResult; +setOnPerformanceEntryCallback: (callback?: () => void) => void; + +logRawEntry: (entry: RawPerformanceEntry) => void; + +getEventCounts: () => $ReadOnlyArray<[string, number]>; } export default (TurboModuleRegistry.get( diff --git a/Libraries/WebPerformance/Performance.js b/Libraries/WebPerformance/Performance.js index 81ded123bc2..07c1872b6f3 100644 --- a/Libraries/WebPerformance/Performance.js +++ b/Libraries/WebPerformance/Performance.js @@ -13,6 +13,7 @@ import type {HighResTimeStamp} from './PerformanceEntry'; import warnOnce from '../Utilities/warnOnce'; +import EventCounts from './EventCounts'; import MemoryInfo from './MemoryInfo'; import NativePerformance from './NativePerformance'; import {PerformanceEntry} from './PerformanceEntry'; @@ -86,9 +87,11 @@ function warnNoNativePerformance() { /** * Partial implementation of the Performance interface for RN, * corresponding to the standard in - * https://www.w3.org/TR/user-timing/#extensions-performance-interface + * https://www.w3.org/TR/user-timing/#extensions-performance-interface */ export default class Performance { + eventCounts: EventCounts = new EventCounts(); + // Get the current JS memory information. get memory(): MemoryInfo { if (NativePerformance?.getSimpleMemoryInfo) { diff --git a/Libraries/WebPerformance/PerformanceEntryReporter.cpp b/Libraries/WebPerformance/PerformanceEntryReporter.cpp index 2cee8e67169..7c524ae0ec1 100644 --- a/Libraries/WebPerformance/PerformanceEntryReporter.cpp +++ b/Libraries/WebPerformance/PerformanceEntryReporter.cpp @@ -47,7 +47,12 @@ GetPendingEntriesResult PerformanceEntryReporter::popPendingEntries() { } void PerformanceEntryReporter::logEntry(const RawPerformanceEntry &entry) { - if (!isReportingType(static_cast(entry.entryType))) { + const auto entryType = static_cast(entry.entryType); + if (entryType == PerformanceEntryType::EVENT) { + eventCounts_[entry.name]++; + } + + if (!isReportingType(entryType)) { return; } diff --git a/Libraries/WebPerformance/PerformanceEntryReporter.h b/Libraries/WebPerformance/PerformanceEntryReporter.h index 5a0cd35c7c1..655f58ceab3 100644 --- a/Libraries/WebPerformance/PerformanceEntryReporter.h +++ b/Libraries/WebPerformance/PerformanceEntryReporter.h @@ -102,6 +102,10 @@ class PerformanceEntryReporter : public EventLogger { void onEventDispatch(EventTag tag) override; void onEventEnd(EventTag tag) override; + const std::unordered_map &getEventCounts() const { + return eventCounts_; + } + private: PerformanceEntryReporter() {} @@ -117,6 +121,7 @@ class PerformanceEntryReporter : public EventLogger { std::vector entries_; std::mutex entriesMutex_; std::array reportingType_{false}; + std::unordered_map eventCounts_; // Mark registry for "measure" lookup PerformanceMarkRegistryType marksRegistry_; diff --git a/Libraries/WebPerformance/PerformanceObserver.js b/Libraries/WebPerformance/PerformanceObserver.js index 9f47ec7d513..ca7d081a0f8 100644 --- a/Libraries/WebPerformance/PerformanceObserver.js +++ b/Libraries/WebPerformance/PerformanceObserver.js @@ -98,7 +98,7 @@ const onPerformanceEntry = () => { } }; -function warnNoNativePerformanceObserver() { +export function warnNoNativePerformanceObserver() { warnOnce( 'missing-native-performance-observer', 'Missing native implementation of PerformanceObserver', diff --git a/Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js b/Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js index 617a9122196..4379e30ab1d 100644 --- a/Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js +++ b/Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js @@ -15,7 +15,10 @@ import type { Spec as NativePerformanceObserver, } from '../NativePerformanceObserver'; +import {RawPerformanceEntryTypeValues} from '../RawPerformanceEntry'; + const reportingType: Set = new Set(); +const eventCounts: Map = new Map(); let entries: Array = []; let onPerformanceEntryCallback: ?() => void; @@ -50,6 +53,13 @@ const NativePerformanceObserverMock: NativePerformanceObserver = { onPerformanceEntryCallback?.(); }); } + if (entry.entryType === RawPerformanceEntryTypeValues.EVENT) { + eventCounts.set(entry.name, (eventCounts.get(entry.name) ?? 0) + 1); + } + }, + + getEventCounts: (): $ReadOnlyArray<[string, number]> => { + return Array.from(eventCounts.entries()); }, }; diff --git a/Libraries/WebPerformance/__tests__/EventCounts-test.js b/Libraries/WebPerformance/__tests__/EventCounts-test.js new file mode 100644 index 00000000000..7be1b9c1fd8 --- /dev/null +++ b/Libraries/WebPerformance/__tests__/EventCounts-test.js @@ -0,0 +1,116 @@ +/** + * 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 + */ + +import {RawPerformanceEntryTypeValues} from '../RawPerformanceEntry'; + +// NOTE: Jest mocks of transitive dependencies don't appear to work with +// ES6 module imports, therefore forced to use commonjs style imports here. +const NativePerformanceObserver = require('../NativePerformanceObserver'); +const Performance = require('../Performance').default; + +jest.mock( + '../NativePerformanceObserver', + () => require('../__mocks__/NativePerformanceObserver').default, +); + +describe('EventCounts', () => { + it('defines EventCounts for Performance', () => { + const eventCounts = new Performance().eventCounts; + expect(eventCounts).not.toBeUndefined(); + }); + + it('consistently implements the API for EventCounts', async () => { + NativePerformanceObserver.logRawEntry({ + name: 'click', + entryType: RawPerformanceEntryTypeValues.EVENT, + }); + + NativePerformanceObserver.logRawEntry({ + name: 'input', + entryType: RawPerformanceEntryTypeValues.EVENT, + }); + + NativePerformanceObserver.logRawEntry({ + name: 'input', + entryType: RawPerformanceEntryTypeValues.EVENT, + }); + + NativePerformanceObserver.logRawEntry({ + name: 'keyup', + entryType: RawPerformanceEntryTypeValues.EVENT, + }); + + NativePerformanceObserver.logRawEntry({ + name: 'keyup', + entryType: RawPerformanceEntryTypeValues.EVENT, + }); + + NativePerformanceObserver.logRawEntry({ + name: 'keyup', + entryType: RawPerformanceEntryTypeValues.EVENT, + }); + + const eventCounts = new Performance().eventCounts; + expect(eventCounts.size).toBe(3); + expect(Array.from(eventCounts.entries())).toStrictEqual([ + ['click', 1], + ['input', 2], + ['keyup', 3], + ]); + + expect(eventCounts.get('click')).toEqual(1); + expect(eventCounts.get('input')).toEqual(2); + expect(eventCounts.get('keyup')).toEqual(3); + + expect(eventCounts.has('click')).toEqual(true); + expect(eventCounts.has('input')).toEqual(true); + expect(eventCounts.has('keyup')).toEqual(true); + + expect(Array.from(eventCounts.keys())).toStrictEqual([ + 'click', + 'input', + 'keyup', + ]); + expect(Array.from(eventCounts.values())).toStrictEqual([1, 2, 3]); + + await jest.runAllTicks(); + NativePerformanceObserver.logRawEntry({ + name: 'input', + entryType: RawPerformanceEntryTypeValues.EVENT, + }); + + NativePerformanceObserver.logRawEntry({ + name: 'keyup', + entryType: RawPerformanceEntryTypeValues.EVENT, + }); + + NativePerformanceObserver.logRawEntry({ + name: 'keyup', + entryType: RawPerformanceEntryTypeValues.EVENT, + }); + expect(Array.from(eventCounts.values())).toStrictEqual([1, 3, 5]); + + await jest.runAllTicks(); + + NativePerformanceObserver.logRawEntry({ + name: 'click', + entryType: RawPerformanceEntryTypeValues.EVENT, + }); + + await jest.runAllTicks(); + + NativePerformanceObserver.logRawEntry({ + name: 'keyup', + entryType: RawPerformanceEntryTypeValues.EVENT, + }); + + expect(Array.from(eventCounts.values())).toStrictEqual([2, 3, 6]); + }); +});