From ae77591b5523d69db6de09e0ac3f013de9023710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Mon, 4 Aug 2025 08:25:36 -0700 Subject: [PATCH 1/5] Small refactor of HighResTimeStamp Differential Revision: D79554726 --- .../react-native/ReactCommon/react/timing/primitives.h | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/react-native/ReactCommon/react/timing/primitives.h b/packages/react-native/ReactCommon/react/timing/primitives.h index 0bb3311dea54..18c4011908b5 100644 --- a/packages/react-native/ReactCommon/react/timing/primitives.h +++ b/packages/react-native/ReactCommon/react/timing/primitives.h @@ -193,11 +193,10 @@ class HighResTimeStamp { const HighResDuration& rhs); public: - HighResTimeStamp() noexcept - : chronoTimePoint_(std::chrono::steady_clock::now()) {} + HighResTimeStamp() noexcept : chronoTimePoint_(chronoNow()) {} static HighResTimeStamp now() noexcept { - return HighResTimeStamp(std::chrono::steady_clock::now()); + return HighResTimeStamp(chronoNow()); } static constexpr HighResTimeStamp min() noexcept { @@ -275,6 +274,10 @@ class HighResTimeStamp { : chronoTimePoint_(chronoTimePoint) {} std::chrono::steady_clock::time_point chronoTimePoint_; + + inline static std::chrono::steady_clock::time_point chronoNow() { + return std::chrono::steady_clock::now(); + } }; inline constexpr HighResDuration operator-( From 0b0878fdfaba7af3de4545be53be3a254b90369f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Mon, 4 Aug 2025 08:25:36 -0700 Subject: [PATCH 2/5] Allow mocking HighResTimeStamp in debug builds Differential Revision: D79554725 --- .../ReactCommon/react/timing/CMakeLists.txt | 2 ++ .../react/timing/React-timing.podspec | 2 ++ .../ReactCommon/react/timing/primitives.h | 25 +++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/packages/react-native/ReactCommon/react/timing/CMakeLists.txt b/packages/react-native/ReactCommon/react/timing/CMakeLists.txt index c356cb182d14..93f99af2645c 100644 --- a/packages/react-native/ReactCommon/react/timing/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/timing/CMakeLists.txt @@ -11,5 +11,7 @@ include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake) add_library(react_timing INTERFACE) target_include_directories(react_timing INTERFACE ${REACT_COMMON_DIR}) +target_link_libraries(react_timing INTERFACE + react_debug) target_compile_reactnative_options(react_timing INTERFACE) target_compile_options(react_timing INTERFACE -Wpedantic) diff --git a/packages/react-native/ReactCommon/react/timing/React-timing.podspec b/packages/react-native/ReactCommon/react/timing/React-timing.podspec index cb781783c819..e3d90ca2ec4b 100644 --- a/packages/react-native/ReactCommon/react/timing/React-timing.podspec +++ b/packages/react-native/ReactCommon/react/timing/React-timing.podspec @@ -41,4 +41,6 @@ Pod::Spec.new do |s| s.module_name = "React_timing" s.header_mappings_dir = "./" end + + add_dependency(s, "React-debug") end diff --git a/packages/react-native/ReactCommon/react/timing/primitives.h b/packages/react-native/ReactCommon/react/timing/primitives.h index 18c4011908b5..d3e3097674b6 100644 --- a/packages/react-native/ReactCommon/react/timing/primitives.h +++ b/packages/react-native/ReactCommon/react/timing/primitives.h @@ -7,7 +7,9 @@ #pragma once +#include #include +#include namespace facebook::react { @@ -227,6 +229,14 @@ class HighResTimeStamp { return HighResTimeStamp(chronoTimePoint); } +#ifdef REACT_NATIVE_DEBUG + static void setTimeStampProviderForTesting( + std::function&& + timeStampProvider) { + getTimeStampProvider() = std::move(timeStampProvider); + } +#endif + // This method is provided for convenience, if you need to convert // HighResTimeStamp to some common epoch with time stamps from other sources. constexpr std::chrono::steady_clock::time_point toChronoSteadyClockTimePoint() @@ -275,9 +285,24 @@ class HighResTimeStamp { std::chrono::steady_clock::time_point chronoTimePoint_; +#ifdef REACT_NATIVE_DEBUG + static std::function& + getTimeStampProvider() { + static std::function + timeStampProvider = nullptr; + return timeStampProvider; + } + + static std::chrono::steady_clock::time_point chronoNow() { + auto& timeStampProvider = getTimeStampProvider(); + return timeStampProvider != nullptr ? timeStampProvider() + : std::chrono::steady_clock::now(); + } +#else inline static std::chrono::steady_clock::time_point chronoNow() { return std::chrono::steady_clock::now(); } +#endif }; inline constexpr HighResDuration operator-( From 200e6c996e8fc8897d6e93a5a5af9ed61392d827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Mon, 4 Aug 2025 08:25:36 -0700 Subject: [PATCH 3/5] Implement HighResTimeStamp mocking in Fantom Differential Revision: D79554723 --- .../testing/fantom/specs/NativeFantom.js | 1 + .../src/HighResTimeStampMock.js | 89 ++++++++++++ .../FantomHighResTimeStampMock-itest.js | 137 ++++++++++++++++++ .../FantomHighResTimeStampMockOpt-itest.js | 22 +++ private/react-native-fantom/src/index.js | 2 + .../tester/src/NativeFantom.cpp | 25 ++++ .../tester/src/NativeFantom.h | 4 + 7 files changed, 280 insertions(+) create mode 100644 private/react-native-fantom/src/HighResTimeStampMock.js create mode 100644 private/react-native-fantom/src/__tests__/FantomHighResTimeStampMock-itest.js create mode 100644 private/react-native-fantom/src/__tests__/FantomHighResTimeStampMockOpt-itest.js diff --git a/packages/react-native/src/private/testing/fantom/specs/NativeFantom.js b/packages/react-native/src/private/testing/fantom/specs/NativeFantom.js index e9698d723dc8..859d5dda46d3 100644 --- a/packages/react-native/src/private/testing/fantom/specs/NativeFantom.js +++ b/packages/react-native/src/private/testing/fantom/specs/NativeFantom.js @@ -116,6 +116,7 @@ interface Spec extends TurboModule { shadowNode: mixed /* ShadowNode */, ): () => ?number; saveJSMemoryHeapSnapshot: (filePath: string) => void; + forceHighResTimeStamp: (timeStamp: ?number) => void; } export default TurboModuleRegistry.getEnforcing( diff --git a/private/react-native-fantom/src/HighResTimeStampMock.js b/private/react-native-fantom/src/HighResTimeStampMock.js new file mode 100644 index 000000000000..15943b6cb979 --- /dev/null +++ b/private/react-native-fantom/src/HighResTimeStampMock.js @@ -0,0 +1,89 @@ +/** + * 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-local + * @format + */ + +import NativeFantom from 'react-native/src/private/testing/fantom/specs/NativeFantom'; + +/** + * Represents a mocked clock for `HighResTimeStamp` values. + */ +export interface HighResTimeStampMock { + setTime(now: number): void; + advanceTimeBy(deltaMs: number): void; + uninstall(): void; +} + +let activeMock: ?HighResTimeStampMock; + +/** + * Installs a mock clock for `HighResTimeStamp` values and returns an object + * that allows controlling the returned values. + * + * @example + * ``` + * let mockClock; + * + * afterEach(() => { + * mockClock.uninstall(); + * mockClock = null; + * }); + * + * it('should do something when time passes', () => { + * mockClock = Fantom.installHighResTimeStampMock(); + * mockClock.setTime(10); + * + * doSomething(); + * + * mockClock.advanceTimeBy(100); + * + * doSomethingElse(); + * + * expect(someSideEffectProduced).toBe(true); + * }); + * ``` + */ +export function installHighResTimeStampMock(): HighResTimeStampMock { + if (activeMock != null) { + throw new Error( + 'Cannot install HighResTimeStamp mock because there is another mock installed already. Reuse the same mock or uninstall the previous one first.', + ); + } + + let mockedTimeStamp = 0; + + const mock: HighResTimeStampMock = { + setTime: now => { + if (now < mockedTimeStamp) { + throw new Error('The mocked time cannot be decreased'); + } + mockedTimeStamp = now; + NativeFantom.forceHighResTimeStamp(mockedTimeStamp); + }, + advanceTimeBy: delta => { + if (delta < 0) { + throw new Error('The mocked time cannot be decreased'); + } + mockedTimeStamp += delta; + mock.setTime(mockedTimeStamp); + }, + uninstall: () => { + if (activeMock === mock) { + NativeFantom.forceHighResTimeStamp(undefined); + activeMock = null; + } + }, + }; + + // Set default value + mock.setTime(mockedTimeStamp); + + activeMock = mock; + + return mock; +} diff --git a/private/react-native-fantom/src/__tests__/FantomHighResTimeStampMock-itest.js b/private/react-native-fantom/src/__tests__/FantomHighResTimeStampMock-itest.js new file mode 100644 index 000000000000..643a0331c14b --- /dev/null +++ b/private/react-native-fantom/src/__tests__/FantomHighResTimeStampMock-itest.js @@ -0,0 +1,137 @@ +/** + * 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. + * + * @fantom_mode dev + * @flow strict-local + * @format + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import type {HighResTimeStampMock} from '../HighResTimeStampMock'; + +import * as Fantom from '@react-native/fantom'; + +const pendingMocks: Set = new Set(); + +function installHighResTimeStampMock(): HighResTimeStampMock { + const mock = Fantom.installHighResTimeStampMock(); + pendingMocks.add(mock); + return mock; +} + +describe('Fantom HighResTimeStamp mocks', () => { + afterEach(() => { + for (const mock of pendingMocks) { + mock.uninstall(); + } + pendingMocks.clear(); + }); + + it('sets a default value', () => { + expect(performance.now()).toBeGreaterThan(0); + + installHighResTimeStampMock(); + + expect(performance.now()).toBe(0); + }); + + it('sets custom values', () => { + const mock = installHighResTimeStampMock(); + + expect(performance.now()).toBe(0); + + mock.setTime(50); + + expect(performance.now()).toBe(50); + + mock.setTime(70); + + expect(performance.now()).toBe(70); + + mock.advanceTimeBy(5); + + expect(performance.now()).toBe(75); + + mock.setTime(90); + + expect(performance.now()).toBe(90); + }); + + it('throws an error when trying to set a time in the past', () => { + const mock = installHighResTimeStampMock(); + + expect(performance.now()).toBe(0); + + mock.setTime(50); + + expect(performance.now()).toBe(50); + + expect(() => { + mock.setTime(40); + }).toThrow('The mocked time cannot be decreased'); + + expect(performance.now()).toBe(50); + + expect(() => { + mock.advanceTimeBy(-1); + }).toThrow('The mocked time cannot be decreased'); + + expect(performance.now()).toBe(50); + }); + + it('allows uninstalling', () => { + expect(performance.now()).toBeGreaterThan(0); + + const mock = installHighResTimeStampMock(); + + expect(performance.now()).toBe(0); + + mock.uninstall(); + + expect(performance.now()).toBeGreaterThan(0); + }); + + it('does nothing when uninstalling multiple times', () => { + expect(performance.now()).toBeGreaterThan(0); + + const mock = installHighResTimeStampMock(); + + expect(performance.now()).toBe(0); + + mock.uninstall(); + mock.uninstall(); + mock.uninstall(); + + expect(performance.now()).toBeGreaterThan(0); + }); + + it('throws an error when installing multiple mocks at the same time', () => { + installHighResTimeStampMock(); + expect(() => installHighResTimeStampMock()).toThrow( + 'Cannot install HighResTimeStamp mock because there is another mock installed already. Reuse the same mock or uninstall the previous one first.', + ); + }); + + it('does not uninstall other mocks', () => { + const initialMock = installHighResTimeStampMock(); + + expect(performance.now()).toBe(0); + + initialMock.uninstall(); + + expect(performance.now()).toBeGreaterThan(0); + + installHighResTimeStampMock(); + + expect(performance.now()).toBe(0); + + initialMock.uninstall(); + + // Has no effect on the current mock + expect(performance.now()).toBe(0); + }); +}); diff --git a/private/react-native-fantom/src/__tests__/FantomHighResTimeStampMockOpt-itest.js b/private/react-native-fantom/src/__tests__/FantomHighResTimeStampMockOpt-itest.js new file mode 100644 index 000000000000..ec39a2fec8af --- /dev/null +++ b/private/react-native-fantom/src/__tests__/FantomHighResTimeStampMockOpt-itest.js @@ -0,0 +1,22 @@ +/** + * 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. + * + * @fantom_mode opt + * @flow strict-local + * @format + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import * as Fantom from '@react-native/fantom'; + +describe('Fantom HighResTimeStamp mocks (optimized builds)', () => { + it('does not allow mocking the time in optimized builds', () => { + expect(() => Fantom.installHighResTimeStampMock()).toThrow( + 'Mocking timers is not supported in optimized builds', + ); + }); +}); diff --git a/private/react-native-fantom/src/index.js b/private/react-native-fantom/src/index.js index 1be3fd4af5b3..fcfeec2ea22a 100644 --- a/private/react-native-fantom/src/index.js +++ b/private/react-native-fantom/src/index.js @@ -682,6 +682,8 @@ export function saveJSMemoryHeapSnapshot(filePath: string): void { NativeFantom.saveJSMemoryHeapSnapshot(filePath); } +export * from './HighResTimeStampMock'; + function runLogBoxCheck() { if (isLogBoxCheckEnabled && LogBox.isInstalled()) { const message = diff --git a/private/react-native-fantom/tester/src/NativeFantom.cpp b/private/react-native-fantom/tester/src/NativeFantom.cpp index 0223f5b9063b..a7fae8660291 100644 --- a/private/react-native-fantom/tester/src/NativeFantom.cpp +++ b/private/react-native-fantom/tester/src/NativeFantom.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -247,4 +248,28 @@ void NativeFantom::saveJSMemoryHeapSnapshot( runtime.instrumentation().createSnapshotToFile(filePath); } +#ifdef REACT_NATIVE_DEBUG + +void NativeFantom::forceHighResTimeStamp( + jsi::Runtime& /*runtime*/, + std::optional now) { + if (now) { + HighResTimeStamp::setTimeStampProviderForTesting( + [now] { return now->toChronoSteadyClockTimePoint(); }); + } else { + HighResTimeStamp::setTimeStampProviderForTesting(nullptr); + } +} + +#else + +void NativeFantom::forceHighResTimeStamp( + jsi::Runtime& runtime, + std::optional /*now*/) { + throw jsi::JSError( + runtime, "Mocking timers is not supported in optimized builds"); +} + +#endif + } // namespace facebook::react diff --git a/private/react-native-fantom/tester/src/NativeFantom.h b/private/react-native-fantom/tester/src/NativeFantom.h index 923ad92037b8..67eca0d15d4a 100644 --- a/private/react-native-fantom/tester/src/NativeFantom.h +++ b/private/react-native-fantom/tester/src/NativeFantom.h @@ -146,6 +146,10 @@ class NativeFantom : public NativeFantomCxxSpec { jsi::Runtime& runtime, const std::string& filePath); + void forceHighResTimeStamp( + jsi::Runtime& runtime, + std::optional now); + private: TesterAppDelegate& appDelegate_; SurfaceId nextSurfaceId_ = 1; From 88612e1faeec621054a7b5b54c31832e69d4cad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Mon, 4 Aug 2025 08:25:36 -0700 Subject: [PATCH 4/5] Make test for LongTasks API deterministic and re-enable on Github Differential Revision: D79554724 --- .../__tests__/LongTasksAPI-itest.js | 102 ++++++++---------- 1 file changed, 46 insertions(+), 56 deletions(-) diff --git a/packages/react-native/src/private/webapis/performance/__tests__/LongTasksAPI-itest.js b/packages/react-native/src/private/webapis/performance/__tests__/LongTasksAPI-itest.js index 11154605bb22..80ff9cf54f04 100644 --- a/packages/react-native/src/private/webapis/performance/__tests__/LongTasksAPI-itest.js +++ b/packages/react-native/src/private/webapis/performance/__tests__/LongTasksAPI-itest.js @@ -16,18 +16,12 @@ import type { } from 'react-native/src/private/webapis/performance/PerformanceObserver'; import * as Fantom from '@react-native/fantom'; -import nullthrows from 'nullthrows'; import setUpPerformanceObserver from 'react-native/src/private/setup/setUpPerformanceObserver'; import {PerformanceLongTaskTiming} from 'react-native/src/private/webapis/performance/LongTasks'; import {PerformanceObserver} from 'react-native/src/private/webapis/performance/PerformanceObserver'; setUpPerformanceObserver(); -function sleep(ms: number) { - const end = performance.now() + ms; - while (performance.now() < end) {} -} - function ensurePerformanceLongTaskTiming( value: mixed, ): PerformanceLongTaskTiming { @@ -41,16 +35,12 @@ function ensurePerformanceLongTaskTiming( } let observer: ?PerformanceObserver; +let pendingHighResTimeStampMock: ?Fantom.HighResTimeStampMock; -const constants = Fantom.getConstants(); -const isGithubCI = constants.isOSS && constants.isRunningFromCI; - -// Skip the test on Github CI. -// In that environment, execution speed is unreliable so some of the tasks -// we schedule as part of the test run (even the task to report long tasks) are -// also being reported as long, and we don't have a way to distinguish those -// from the intentionally long tasks we're scheduling for testing. -const describe = isGithubCI ? global.describe.skip : global.describe; +function installHighResTimeStampMock() { + pendingHighResTimeStampMock = Fantom.installHighResTimeStampMock(); + return pendingHighResTimeStampMock; +} describe('LongTasks API', () => { afterEach(() => { @@ -60,54 +50,62 @@ describe('LongTasks API', () => { observer = null; }); } + + if (pendingHighResTimeStampMock) { + pendingHighResTimeStampMock.uninstall(); + pendingHighResTimeStampMock = null; + } }); it('does NOT report short tasks (under 50ms)', () => { const callback = jest.fn(); + const mockClock = installHighResTimeStampMock(); + + mockClock.setTime(0); + Fantom.runTask(() => { observer = new PerformanceObserver(callback); observer.observe({entryTypes: ['longtask']}); }); - const initialCallCount = callback.mock.calls.length; + expect(callback).not.toHaveBeenCalled(); Fantom.runTask(() => { // Short task. + mockClock.advanceTimeBy(10); }); - expect(callback).toHaveBeenCalledTimes(initialCallCount); + expect(callback).not.toHaveBeenCalled(); Fantom.runTask(() => { // Slightly longer task, but still not long. - sleep(40); + mockClock.advanceTimeBy(40); }); - expect(callback).toHaveBeenCalledTimes(initialCallCount); + expect(callback).not.toHaveBeenCalled(); }); it('reports long tasks (over 50ms)', () => { const callback = jest.fn(); + const mockClock = installHighResTimeStampMock(); + Fantom.runTask(() => { observer = new PerformanceObserver(callback); observer.observe({entryTypes: ['longtask']}); }); - const initialCallCount = callback.mock.calls.length; + expect(callback).not.toHaveBeenCalled(); - const beforeTaskStartTime = performance.now(); - let afterTaskStartTime; + mockClock.setTime(10); Fantom.runTask(() => { - afterTaskStartTime = performance.now(); // Long task. - sleep(51); + mockClock.advanceTimeBy(51); }); - const afterTaskEndTime = performance.now(); - - expect(callback).toHaveBeenCalledTimes(initialCallCount + 1); + expect(callback).toHaveBeenCalledTimes(1); const [entries, _observer, options] = callback.mock .lastCall as $FlowFixMe as [ @@ -127,12 +125,8 @@ describe('LongTasks API', () => { expect(entry.name).toBe('self'); expect(entry.entryType).toBe('longtask'); - expect(entry.startTime).toBeGreaterThanOrEqual(beforeTaskStartTime); - expect(entry.startTime).toBeLessThanOrEqual(nullthrows(afterTaskStartTime)); - expect(entry.duration).toBeGreaterThanOrEqual(51); - expect(entry.duration).toBeLessThanOrEqual( - afterTaskEndTime - beforeTaskStartTime, - ); + expect(entry.startTime).toBe(10); + expect(entry.duration).toBe(51); expect(entry.attribution).toEqual([]); }); @@ -140,53 +134,55 @@ describe('LongTasks API', () => { it('should NOT be reported if they are longer than 50ms but had yielding opportunities in intervals shorter than 50ms', () => { const callback = jest.fn(); + const mockClock = installHighResTimeStampMock(); + Fantom.runTask(() => { observer = new PerformanceObserver(callback); observer.observe({entryTypes: ['longtask']}); }); - const initialCallCount = callback.mock.calls.length; + expect(callback).not.toHaveBeenCalled(); const shouldYield = global.nativeRuntimeScheduler.unstable_shouldYield; + mockClock.setTime(10); + Fantom.runTask(() => { - sleep(30); + mockClock.advanceTimeBy(30); shouldYield(); - sleep(30); + mockClock.advanceTimeBy(30); shouldYield(); - sleep(30); + mockClock.advanceTimeBy(30); }); - expect(callback).toHaveBeenCalledTimes(initialCallCount); + expect(callback).not.toHaveBeenCalled(); }); it('should be reported if running for longer than 50ms between yielding opportunities', () => { const callback = jest.fn(); + const mockClock = installHighResTimeStampMock(); + Fantom.runTask(() => { observer = new PerformanceObserver(callback); observer.observe({entryTypes: ['longtask']}); }); - const initialCallCount = callback.mock.calls.length; + expect(callback).not.toHaveBeenCalled(); const shouldYield = global.nativeRuntimeScheduler.unstable_shouldYield; - const beforeTaskStartTime = performance.now(); - let afterTaskStartTime; + mockClock.setTime(10); Fantom.runTask(() => { - afterTaskStartTime = performance.now(); - sleep(40); + mockClock.advanceTimeBy(40); shouldYield(); - sleep(51); // long interval without yielding + mockClock.advanceTimeBy(51); // long interval without yielding shouldYield(); - sleep(40); + mockClock.advanceTimeBy(40); }); - const afterTaskEndTime = performance.now(); - - expect(callback).toHaveBeenCalledTimes(initialCallCount + 1); + expect(callback).toHaveBeenCalledTimes(1); const entries = callback.mock.lastCall[0] as PerformanceObserverEntryList; const allEntries = entries.getEntries(); @@ -195,14 +191,8 @@ describe('LongTasks API', () => { const entry = ensurePerformanceLongTaskTiming(allEntries[0]); expect(entry.name).toBe('self'); expect(entry.entryType).toBe('longtask'); - expect(entry.startTime).toBeGreaterThanOrEqual(beforeTaskStartTime); - expect(entry.startTime).toBeLessThanOrEqual( - nullthrows(afterTaskStartTime), - ); - expect(entry.duration).toBeGreaterThanOrEqual(131); // just the sum of the sleep times in the task - expect(entry.duration).toBeLessThanOrEqual( - afterTaskEndTime - beforeTaskStartTime, - ); + expect(entry.startTime).toBe(10); + expect(entry.duration).toBe(131); expect(entry.attribution).toEqual([]); }); }); From 19ab9245263fa5556fa11157f8911b2efdc5d944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Mon, 4 Aug 2025 08:33:55 -0700 Subject: [PATCH 5/5] Remove redundant methods to mock timers from PerformanceEntryReporter and NativePerformance (#53028) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/53028 Changelog: [internal] Now that we have mocking at a more fundamental level (`HighResTimeStamp` API) we can replace other timing mocks with that one. This does it for `PerformanceEntryReporter` and the `NativePerformance` module. Differential Revision: D79557640 --- .../webperformance/NativePerformance.cpp | 8 +---- .../webperformance/NativePerformance.h | 4 --- .../timeline/PerformanceEntryReporter.cpp | 4 +-- .../timeline/PerformanceEntryReporter.h | 5 --- .../performance/__tests__/UserTiming-itest.js | 31 +++++++++++-------- .../performance/specs/NativePerformance.js | 1 - scripts/cxx-api/ReactNativeCPP.api | 4 --- 7 files changed, 21 insertions(+), 36 deletions(-) diff --git a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.cpp b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.cpp index 6259ff0bbbe1..afdf0d88f685 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.cpp @@ -131,7 +131,7 @@ NativePerformance::NativePerformance(std::shared_ptr jsInvoker) : NativePerformanceCxxSpec(std::move(jsInvoker)) {} HighResTimeStamp NativePerformance::now(jsi::Runtime& /*rt*/) { - return forcedCurrentTimeStamp_.value_or(HighResTimeStamp::now()); + return HighResTimeStamp::now(); } void NativePerformance::reportMark( @@ -395,12 +395,6 @@ NativePerformance::getSupportedPerformanceEntryTypes(jsi::Runtime& /*rt*/) { #pragma mark - Testing -void NativePerformance::setCurrentTimeStampForTesting( - jsi::Runtime& /*rt*/, - HighResTimeStamp ts) { - forcedCurrentTimeStamp_ = ts; -} - void NativePerformance::clearEventCountsForTesting(jsi::Runtime& /*rt*/) { PerformanceEntryReporter::getInstance()->clearEventCounts(); } diff --git a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.h b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.h index 8dcf591f855c..d5be089cc07f 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.h +++ b/packages/react-native/ReactCommon/react/nativemodule/webperformance/NativePerformance.h @@ -186,11 +186,7 @@ class NativePerformance : public NativePerformanceCxxSpec { #pragma mark - Testing - void setCurrentTimeStampForTesting(jsi::Runtime& rt, HighResTimeStamp ts); void clearEventCountsForTesting(jsi::Runtime& rt); - - private: - std::optional forcedCurrentTimeStamp_; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.cpp b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.cpp index 8344312a982c..09c6e21ddf15 100644 --- a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.cpp +++ b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.cpp @@ -89,9 +89,9 @@ PerformanceEntryReporter::PerformanceEntryReporter() } HighResTimeStamp PerformanceEntryReporter::getCurrentTimeStamp() const { - return timeStampProvider_ != nullptr ? timeStampProvider_() - : HighResTimeStamp::now(); + return HighResTimeStamp::now(); } + void PerformanceEntryReporter::addEventTimingListener( PerformanceEntryReporterEventTimingListener* listener) { std::unique_lock lock(listenersMutex_); diff --git a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.h b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.h index a42d43fdd7a4..6edbb089f679 100644 --- a/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.h +++ b/packages/react-native/ReactCommon/react/performance/timeline/PerformanceEntryReporter.h @@ -70,10 +70,6 @@ class PerformanceEntryReporter { HighResTimeStamp getCurrentTimeStamp() const; - void setTimeStampProvider(std::function provider) { - timeStampProvider_ = std::move(provider); - } - void addEventTimingListener( PerformanceEntryReporterEventTimingListener* listener); void removeEventTimingListener( @@ -138,7 +134,6 @@ class PerformanceEntryReporter { std::unordered_map eventCounts_; - std::function timeStampProvider_ = nullptr; mutable std::shared_mutex listenersMutex_; std::vector eventTimingListeners_{}; diff --git a/packages/react-native/src/private/webapis/performance/__tests__/UserTiming-itest.js b/packages/react-native/src/private/webapis/performance/__tests__/UserTiming-itest.js index e1a469740062..748a8bacb599 100644 --- a/packages/react-native/src/private/webapis/performance/__tests__/UserTiming-itest.js +++ b/packages/react-native/src/private/webapis/performance/__tests__/UserTiming-itest.js @@ -18,11 +18,8 @@ import type { import ensureInstance from '../../../__tests__/utilities/ensureInstance'; import DOMException from '../../errors/DOMException'; -import MaybeNativePerformance from '../specs/NativePerformance'; import {PerformanceMark, PerformanceMeasure} from '../UserTiming'; -import nullthrows from 'nullthrows'; - -const NativePerformance = nullthrows(MaybeNativePerformance); +import * as Fantom from '@react-native/fantom'; declare var performance: Performance; @@ -39,15 +36,23 @@ function toJSON(entries: PerformanceEntryList): Array { return entries.map(entry => entry.toJSON()); } +let mockClock: Fantom.HighResTimeStampMock; + describe('User Timing', () => { beforeEach(() => { performance.clearMarks(); performance.clearMeasures(); + + mockClock = Fantom.installHighResTimeStampMock(); + }); + + afterEach(() => { + mockClock.uninstall(); }); describe('mark', () => { it('works with default timestamp', () => { - NativePerformance.setCurrentTimeStampForTesting?.(25); + mockClock.setTime(25); const mark = performance.mark('mark-now'); @@ -147,7 +152,7 @@ describe('User Timing', () => { describe('measure', () => { describe('with measureOptions', () => { it('uses 0 as default start and now as default end', () => { - NativePerformance.setCurrentTimeStampForTesting?.(25); + mockClock.setTime(25); const measure = performance.measure('measure-with-defaults', {}); @@ -160,7 +165,7 @@ describe('User Timing', () => { }); it('works with a start timestamp', () => { - NativePerformance.setCurrentTimeStampForTesting?.(25); + mockClock.setTime(25); const measure = performance.measure('measure-with-start-timestamp', { start: 10, @@ -175,7 +180,7 @@ describe('User Timing', () => { }); it('works with start mark', () => { - NativePerformance.setCurrentTimeStampForTesting?.(25); + mockClock.setTime(25); performance.mark('start-mark', { startTime: 10, @@ -194,7 +199,7 @@ describe('User Timing', () => { }); it('works with end mark', () => { - NativePerformance.setCurrentTimeStampForTesting?.(25); + mockClock.setTime(25); performance.mark('end-mark', { startTime: 50, @@ -213,7 +218,7 @@ describe('User Timing', () => { }); it('works with start mark and end mark', () => { - NativePerformance.setCurrentTimeStampForTesting?.(25); + mockClock.setTime(25); performance.mark('start-mark', { startTime: 10, @@ -378,7 +383,7 @@ describe('User Timing', () => { describe('with startMark / endMark', () => { it('uses 0 as default start and now as default end', () => { - NativePerformance.setCurrentTimeStampForTesting?.(25); + mockClock.setTime(25); const measure = performance.measure('measure-with-defaults'); @@ -391,7 +396,7 @@ describe('User Timing', () => { }); it('works with startMark', () => { - NativePerformance.setCurrentTimeStampForTesting?.(25); + mockClock.setTime(25); performance.mark('start-mark', { startTime: 10, @@ -411,7 +416,7 @@ describe('User Timing', () => { }); it('works with startMark and endMark', () => { - NativePerformance.setCurrentTimeStampForTesting?.(25); + mockClock.setTime(25); performance.mark('start-mark', { startTime: 10, diff --git a/packages/react-native/src/private/webapis/performance/specs/NativePerformance.js b/packages/react-native/src/private/webapis/performance/specs/NativePerformance.js index 640138cee0ab..879a957bd53e 100644 --- a/packages/react-native/src/private/webapis/performance/specs/NativePerformance.js +++ b/packages/react-native/src/private/webapis/performance/specs/NativePerformance.js @@ -93,7 +93,6 @@ export interface Spec extends TurboModule { +getSupportedPerformanceEntryTypes: () => $ReadOnlyArray; - +setCurrentTimeStampForTesting?: (timeStamp: number) => void; +clearEventCountsForTesting?: () => void; } diff --git a/scripts/cxx-api/ReactNativeCPP.api b/scripts/cxx-api/ReactNativeCPP.api index baee7e9f4045..f770c4a64d3d 100644 --- a/scripts/cxx-api/ReactNativeCPP.api +++ b/scripts/cxx-api/ReactNativeCPP.api @@ -17307,7 +17307,6 @@ class NativePerformance : public NativePerformanceCxxSpec { std::unordered_map getSimpleMemoryInfo(jsi::Runtime& rt); std::unordered_map getReactNativeStartupTiming( jsi::Runtime& rt); - void setCurrentTimeStampForTesting(jsi::Runtime& rt, HighResTimeStamp ts); void clearEventCountsForTesting(jsi::Runtime& rt); }; } // namespace facebook::react @@ -17639,9 +17638,6 @@ class PerformanceEntryReporter { PerformanceEntryType entryType, const std::string& entryName); HighResTimeStamp getCurrentTimeStamp() const; - void setTimeStampProvider(std::function provider) { - timeStampProvider_ = std::move(provider); - } static std::vector getSupportedEntryTypes(); uint32_t getDroppedEntriesCount(PerformanceEntryType type) const noexcept; const std::unordered_map& getEventCounts() const {