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/3] 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/3] 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 11f222b33652bbbb325e5eb8dc6c42089bea2ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Mon, 4 Aug 2025 08:35:06 -0700 Subject: [PATCH 3/3] Implement HighResTimeStamp mocking in Fantom (#53019) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/53019 Changelog: [internal] This adds support for mocking `HighResTimeStamp` values in Fantom tests via a new `Fantom.installHighResTimeStampMock` function. See new tests for more details on how it works. Reviewed By: rshest 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;