Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ interface Spec extends TurboModule {
saveJSMemoryHeapSnapshot: (filePath: string) => void;
forceHighResTimeStamp: (timeStamp: ?number) => void;
setTimerMockEnabled: (enabled: boolean) => void;
advanceTimers: (deltaMs: number) => void;
runAllTimers: () => void;
advanceTimersToNextDue: (deltaMs: number) => number;
runNextTimer: () => boolean;
getPendingTimerCount: () => number;
startJSSamplingProfiler: () => void;
stopJSSamplingProfilerAndSaveToFile: (filePath: string) => void;
Expand Down
31 changes: 24 additions & 7 deletions private/react-native-fantom/src/TimerMock.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ import {runWorkLoop} from './index';
*/
export interface TimerMock {
// Advances the virtual clock by `deltaMs`, firing every timer that becomes
// due (in order), then runs the work loop so the callbacks execute.
// due in order and running the work loop after each one, so each callback
// runs at its due time and timers it schedules can fire in the same advance.
advanceTimersByTime(deltaMs: number): void;
// Fires all pending timers (bounded to avoid infinite loops), then runs the
// work loop so the callbacks execute.
// Fires all pending timers in order, running the work loop after each one
// (bounded to avoid infinite loops).
runAllTimers(): void;
// Returns the number of currently pending (scheduled but not yet fired)
// timers.
Expand All @@ -29,6 +30,10 @@ export interface TimerMock {

let activeMock: ?TimerMock;

// Safety bound to avoid spinning forever on self-rescheduling or zero-interval
// recurring timers (mirrors jest fake-timer safeguards).
const MAX_TIMER_FIRES = 100000;

/**
* Installs a deterministic timer mock. While installed, `setTimeout` and
* `setInterval` callbacks do not fire on their own; they only fire when the
Expand Down Expand Up @@ -68,12 +73,24 @@ export function installTimerMock(): TimerMock {

const mock: TimerMock = {
advanceTimersByTime: deltaMs => {
NativeFantom.advanceTimers(deltaMs);
runWorkLoop();
let remainingMs = deltaMs;
for (
let fires = 0;
fires < MAX_TIMER_FIRES && remainingMs >= 0;
fires++
) {
remainingMs = NativeFantom.advanceTimersToNextDue(remainingMs);
runWorkLoop();
}
},
runAllTimers: () => {
NativeFantom.runAllTimers();
runWorkLoop();
for (let fires = 0; fires < MAX_TIMER_FIRES; fires++) {
const fired = NativeFantom.runNextTimer();
runWorkLoop();
if (!fired) {
break;
}
}
},
getPendingTimerCount: () => NativeFantom.getPendingTimerCount(),
uninstall: () => {
Expand Down
33 changes: 33 additions & 0 deletions private/react-native-fantom/src/__tests__/FantomTimerMock-itest.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,39 @@ describe('Fantom timer mocks', () => {
expect(timers.getPendingTimerCount()).toBe(0);
});

it('runs each callback at its due time when advancing the clock', () => {
const timers = installTimerMock();
const calls: Array<string> = [];

setTimeout(() => {
calls.push('A at 100');
setTimeout(() => calls.push('B at 150'), 50);
}, 100);
setTimeout(() => calls.push('C at 200'), 200);

timers.advanceTimersByTime(200);

expect(calls).toEqual(['A at 100', 'B at 150', 'C at 200']);
});

it('runs a recurring timer once per interval when advancing the clock', () => {
const timers = installTimerMock();
const calls: Array<number> = [];
let count = 0;

const id = setInterval(() => {
count++;
calls.push(count);
// A timer scheduled from the callback runs before the next interval.
setTimeout(() => calls.push(-count), 0);
}, 100);

timers.advanceTimersByTime(350);
clearInterval(id);

expect(calls).toEqual([1, -1, 2, -2, 3, -3]);
});

it('fires all pending timers with runAllTimers regardless of delay', () => {
const timers = installTimerMock();
const first = jest.fn();
Expand Down
103 changes: 41 additions & 62 deletions private/react-native-fantom/tester/src/FantomTimerRegistry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,6 @@

namespace facebook::react {

namespace {
// Safety bound to avoid spinning forever on self-rescheduling or zero-interval
// recurring timers (mirrors jest fake-timer safeguards).
constexpr uint32_t kMaxTimerFires = 100000;
} // namespace

void FantomTimerRegistry::createTimer(uint32_t timerId, double delayMs) {
scheduleTimer(timerId, delayMs, /* isRecurring */ false);
}
Expand Down Expand Up @@ -82,74 +76,59 @@ void FantomTimerRegistry::fireTimer(uint32_t timerId) {
}
}

void FantomTimerRegistry::advanceTimersByTime(double deltaMs) {
double targetMs = nowMs_ + std::max(deltaMs, 0.0);

for (uint32_t fires = 0; fires < kMaxTimerFires; ++fires) {
// Find the earliest timer due at or before the target time, breaking ties
// by timer id (insertion order / FIFO per the timer index).
std::optional<uint32_t> nextId;
double nextDueMs = 0.0;
for (const auto& [id, timer] : timers_) {
if (timer.dueTimeMs <= targetMs) {
if (!nextId.has_value() || timer.dueTimeMs < nextDueMs ||
(timer.dueTimeMs == nextDueMs && id < *nextId)) {
nextId = id;
nextDueMs = timer.dueTimeMs;
}
}
}

if (!nextId.has_value()) {
break;
std::optional<uint32_t> FantomTimerRegistry::findNextTimer(
double maxDueTimeMs) const {
// The earliest timer due at or before `maxDueTimeMs`, breaking ties by timer
// id (insertion order / FIFO per the timer index).
std::optional<uint32_t> nextId;
double nextDueMs = 0.0;
for (const auto& [id, timer] : timers_) {
if (timer.dueTimeMs <= maxDueTimeMs &&
(!nextId.has_value() || timer.dueTimeMs < nextDueMs ||
(timer.dueTimeMs == nextDueMs && id < *nextId))) {
nextId = id;
nextDueMs = timer.dueTimeMs;
}
}
return nextId;
}

auto it = timers_.find(*nextId);
Timer timer = it->second;
void FantomTimerRegistry::fireAndAdvanceTo(uint32_t timerId) {
auto it = timers_.find(timerId);
Timer timer = it->second;

// Advance virtual time to this timer's due time before firing.
nowMs_ = timer.dueTimeMs;
nowMs_ = timer.dueTimeMs;

if (timer.isRecurring) {
it->second.dueTimeMs = timer.dueTimeMs + timer.intervalMs;
} else {
timers_.erase(it);
}

fireTimer(timer.timerId);
if (timer.isRecurring) {
it->second.dueTimeMs = timer.dueTimeMs + timer.intervalMs;
} else {
timers_.erase(it);
}

nowMs_ = targetMs;
fireTimer(timer.timerId);
}

void FantomTimerRegistry::runAllTimers() {
for (uint32_t fires = 0; fires < kMaxTimerFires && !timers_.empty();
++fires) {
// Fire the earliest pending timer (by due time, then id).
uint32_t nextId = 0;
double nextDueMs = 0.0;
bool found = false;
for (const auto& [id, timer] : timers_) {
if (!found || timer.dueTimeMs < nextDueMs ||
(timer.dueTimeMs == nextDueMs && id < nextId)) {
nextId = id;
nextDueMs = timer.dueTimeMs;
found = true;
}
}
double FantomTimerRegistry::advanceTimersToNextDue(double deltaMs) {
double targetMs = nowMs_ + std::max(deltaMs, 0.0);

auto it = timers_.find(nextId);
Timer timer = it->second;
nowMs_ = timer.dueTimeMs;
auto nextId = findNextTimer(targetMs);
if (!nextId.has_value()) {
nowMs_ = targetMs;
return -1;
}

if (timer.isRecurring) {
it->second.dueTimeMs = timer.dueTimeMs + timer.intervalMs;
} else {
timers_.erase(it);
}
fireAndAdvanceTo(*nextId);
return targetMs - nowMs_;
}

fireTimer(timer.timerId);
bool FantomTimerRegistry::runNextTimer() {
auto nextId = findNextTimer(std::numeric_limits<double>::infinity());
if (!nextId.has_value()) {
return false;
}

fireAndAdvanceTo(*nextId);
return true;
}

} // namespace facebook::react
25 changes: 18 additions & 7 deletions private/react-native-fantom/tester/src/FantomTimerRegistry.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
#pragma once

#include <react/runtime/PlatformTimerRegistry.h>

#include <cstdint>
#include <limits>
#include <memory>
#include <optional>
#include <unordered_map>

namespace facebook::react {
Expand All @@ -33,7 +36,7 @@ namespace facebook::react {
*
* Firing a timer calls `TimerManager::callTimer`, which enqueues the JS
* callback on the RuntimeScheduler; the caller is expected to run the work loop
* afterwards to execute the callbacks.
* after each fired timer to execute its callback.
*
* All methods are expected to be called on the JS thread (Fantom is
* single-threaded), so no locking is required.
Expand All @@ -58,13 +61,19 @@ class FantomTimerRegistry : public PlatformTimerRegistry {
void setMockEnabled(bool enabled);
bool isMockEnabled() const noexcept;

// Advances the virtual clock by `deltaMs`, firing every timer that becomes
// due (in due-time order), re-arming recurring timers along the way.
void advanceTimersByTime(double deltaMs);
// Advances the virtual clock to the earliest timer due within `deltaMs` and
// fires it, re-arming it if it is recurring. Returns the part of `deltaMs`
// left after that timer's due time, or -1 if no timer was due, in which case
// the clock has advanced by the whole `deltaMs`.
//
// It fires one timer per call so that the caller can run the work loop in
// between: a timer's callback then runs at its due time, before any later
// timer fires, and timers it schedules can fire within the same advance.
double advanceTimersToNextDue(double deltaMs);

// Fires all currently pending timers until none remain (bounded to avoid
// infinite loops with self-rescheduling/recurring timers).
void runAllTimers();
// Fires the earliest pending timer regardless of its due time, advancing the
// virtual clock to it. Returns false if no timer was pending.
bool runNextTimer();

uint32_t getPendingTimerCount() const noexcept;

Expand All @@ -77,6 +86,8 @@ class FantomTimerRegistry : public PlatformTimerRegistry {
};

void scheduleTimer(uint32_t timerId, double delayMs, bool isRecurring);
std::optional<uint32_t> findNextTimer(double maxDueTimeMs) const;
void fireAndAdvanceTo(uint32_t timerId);
void fireTimer(uint32_t timerId);

std::weak_ptr<TimerManager> timerManager_;
Expand Down
10 changes: 6 additions & 4 deletions private/react-native-fantom/tester/src/NativeFantom.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,14 @@ void NativeFantom::setTimerMockEnabled(
appDelegate_.setTimerMockEnabled(enabled);
}

void NativeFantom::advanceTimers(jsi::Runtime& /*runtime*/, double deltaMs) {
appDelegate_.advanceTimers(deltaMs);
double NativeFantom::advanceTimersToNextDue(
jsi::Runtime& /*runtime*/,
double deltaMs) {
return appDelegate_.advanceTimersToNextDue(deltaMs);
}

void NativeFantom::runAllTimers(jsi::Runtime& /*runtime*/) {
appDelegate_.runAllTimers();
bool NativeFantom::runNextTimer(jsi::Runtime& /*runtime*/) {
return appDelegate_.runNextTimer();
}

double NativeFantom::getPendingTimerCount(jsi::Runtime& /*runtime*/) {
Expand Down
4 changes: 2 additions & 2 deletions private/react-native-fantom/tester/src/NativeFantom.h
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ class NativeFantom : public NativeFantomCxxSpec<NativeFantom> {
void forceHighResTimeStamp(jsi::Runtime &runtime, std::optional<HighResTimeStamp> now);

void setTimerMockEnabled(jsi::Runtime &runtime, bool enabled);
void advanceTimers(jsi::Runtime &runtime, double deltaMs);
void runAllTimers(jsi::Runtime &runtime);
double advanceTimersToNextDue(jsi::Runtime &runtime, double deltaMs);
bool runNextTimer(jsi::Runtime &runtime);
double getPendingTimerCount(jsi::Runtime &runtime);

void startJSSamplingProfiler(jsi::Runtime &runtime);
Expand Down
14 changes: 6 additions & 8 deletions private/react-native-fantom/tester/src/TesterAppDelegate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -358,16 +358,14 @@ void TesterAppDelegate::setTimerMockEnabled(bool enabled) {
}
}

void TesterAppDelegate::advanceTimers(double deltaMs) {
if (timerRegistry_ != nullptr) {
timerRegistry_->advanceTimersByTime(deltaMs);
}
double TesterAppDelegate::advanceTimersToNextDue(double deltaMs) {
return timerRegistry_ != nullptr
? timerRegistry_->advanceTimersToNextDue(deltaMs)
: -1;
}

void TesterAppDelegate::runAllTimers() {
if (timerRegistry_ != nullptr) {
timerRegistry_->runAllTimers();
}
bool TesterAppDelegate::runNextTimer() {
return timerRegistry_ != nullptr && timerRegistry_->runNextTimer();
}

uint32_t TesterAppDelegate::getPendingTimerCount() {
Expand Down
4 changes: 2 additions & 2 deletions private/react-native-fantom/tester/src/TesterAppDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ class TesterAppDelegate {

// Deterministic timer control (driven from JS via NativeFantom).
void setTimerMockEnabled(bool enabled);
void advanceTimers(double deltaMs);
void runAllTimers();
double advanceTimersToNextDue(double deltaMs);
bool runNextTimer();
uint32_t getPendingTimerCount();

void flushMessageQueue();
Expand Down
Loading