Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/react-native/ReactCommon/react/timing/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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
34 changes: 31 additions & 3 deletions packages/react-native/ReactCommon/react/timing/primitives.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

#pragma once

#include <react/debug/flags.h>
#include <chrono>
#include <functional>

namespace facebook::react {

Expand Down Expand Up @@ -193,11 +195,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 {
Expand Down Expand Up @@ -228,6 +229,14 @@ class HighResTimeStamp {
return HighResTimeStamp(chronoTimePoint);
}

#ifdef REACT_NATIVE_DEBUG
static void setTimeStampProviderForTesting(
std::function<std::chrono::steady_clock::time_point()>&&
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()
Expand Down Expand Up @@ -275,6 +284,25 @@ class HighResTimeStamp {
: chronoTimePoint_(chronoTimePoint) {}

std::chrono::steady_clock::time_point chronoTimePoint_;

#ifdef REACT_NATIVE_DEBUG
static std::function<std::chrono::steady_clock::time_point()>&
getTimeStampProvider() {
static std::function<std::chrono::steady_clock::time_point()>
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-(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ interface Spec extends TurboModule {
shadowNode: mixed /* ShadowNode */,
): () => ?number;
saveJSMemoryHeapSnapshot: (filePath: string) => void;
forceHighResTimeStamp: (timeStamp: ?number) => void;
}

export default TurboModuleRegistry.getEnforcing<Spec>(
Expand Down
89 changes: 89 additions & 0 deletions private/react-native-fantom/src/HighResTimeStampMock.js
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<HighResTimeStampMock> = 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);
});
});
Original file line number Diff line number Diff line change
@@ -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',
);
});
});
2 changes: 2 additions & 0 deletions private/react-native-fantom/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,8 @@ export function saveJSMemoryHeapSnapshot(filePath: string): void {
NativeFantom.saveJSMemoryHeapSnapshot(filePath);
}

export * from './HighResTimeStampMock';

function runLogBoxCheck() {
if (isLogBoxCheckEnabled && LogBox.isInstalled()) {
const message =
Expand Down
25 changes: 25 additions & 0 deletions private/react-native-fantom/tester/src/NativeFantom.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include <jsi/JSIDynamic.h>
#include <react/bridging/Bridging.h>
#include <react/debug/flags.h>
#include <react/renderer/components/modal/ModalHostViewShadowNode.h>
#include <react/renderer/components/scrollview/ScrollViewShadowNode.h>
#include <react/renderer/uimanager/UIManagerBinding.h>
Expand Down Expand Up @@ -247,4 +248,28 @@ void NativeFantom::saveJSMemoryHeapSnapshot(
runtime.instrumentation().createSnapshotToFile(filePath);
}

#ifdef REACT_NATIVE_DEBUG

void NativeFantom::forceHighResTimeStamp(
jsi::Runtime& /*runtime*/,
std::optional<HighResTimeStamp> now) {
if (now) {
HighResTimeStamp::setTimeStampProviderForTesting(
[now] { return now->toChronoSteadyClockTimePoint(); });
} else {
HighResTimeStamp::setTimeStampProviderForTesting(nullptr);
}
}

#else

void NativeFantom::forceHighResTimeStamp(
jsi::Runtime& runtime,
std::optional<HighResTimeStamp> /*now*/) {
throw jsi::JSError(
runtime, "Mocking timers is not supported in optimized builds");
}

#endif

} // namespace facebook::react
4 changes: 4 additions & 0 deletions private/react-native-fantom/tester/src/NativeFantom.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ class NativeFantom : public NativeFantomCxxSpec<NativeFantom> {
jsi::Runtime& runtime,
const std::string& filePath);

void forceHighResTimeStamp(
jsi::Runtime& runtime,
std::optional<HighResTimeStamp> now);

private:
TesterAppDelegate& appDelegate_;
SurfaceId nextSurfaceId_ = 1;
Expand Down
Loading