Skip to content

Fix iOS JS timers when the system clock changes - #59865

Merged
meta-codesync[bot] merged 4 commits into
react:mainfrom
FeliksLv01:fix/ios-timer-clock-tests
Nov 13, 2026
Merged

meta-codesync[bot] merged 4 commits into
react:mainfrom
FeliksLv01:fix/ios-timer-clock-tests

Conversation

@FeliksLv01

Copy link
Copy Markdown
Contributor

Summary:

On iOS, RCTTiming uses wall-clock dates for pending JS timers. Moving the system clock backward can delay setTimeout and setInterval, and moving it forward can make them fire early.

Use a monotonic clock for timer deadlines and wakeup ordering so callbacks follow elapsed time, including time spent asleep.

Related to #59782.

Changelog:

[IOS] [FIXED] - Keep JavaScript timers on schedule when the system clock changes.

Test Plan:

Ran RCTTimingClockChangeTests through the RNTesterUnitTests scheme on an iPhone 18 Pro simulator running iOS 27.2. The tests simulate a one-hour wall-clock jump and check that:

  • A forward jump does not fire a pending setTimeout early.
  • A backward jump does not delay a due setTimeout.
  • A backward jump does not delay the next callback of a running setInterval.

All three tests failed on upstream commit f480836 and passed with the fix. Both runs used the same local build settings. I excluded the existing RCTImageLoaderTests.m because it references undefined macros in this checkout, and added the Xcode Swift compatibility library search path. These build overrides are not part of the PR.

From packages/rn-tester, with SIMULATOR_UDID set to the simulator used for testing:

swift_lib="$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphonesimulator"
xcodebuild test -workspace RNTesterPods.xcworkspace -scheme RNTesterUnitTests \
  -destination "platform=iOS Simulator,id=$SIMULATOR_UDID" \
  -only-testing:RNTesterUnitTests/RCTTimingClockChangeTests -parallel-testing-enabled NO \
  'EXCLUDED_SOURCE_FILE_NAMES=RCTImageLoaderTests.m' \
  "LIBRARY_SEARCH_PATHS=\$(inherited) $swift_lib" 2>&1 | xcbeautify

The test run was limited to this regression suite. A physical-device clock change was not tested.

@meta-cla

meta-cla Bot commented Nov 8, 2026

Copy link
Copy Markdown

Hi @FeliksLv01!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@FeliksLv01 FeliksLv01 closed this Nov 8, 2026
@FeliksLv01 FeliksLv01 reopened this Nov 8, 2026
@meta-cla

meta-cla Bot commented Nov 8, 2026

Copy link
Copy Markdown

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Nov 8, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Nov 8, 2026
Comment on lines +40 to +44
+ (NSTimeInterval)monotonicTime
{
// Timer delays measure elapsed time, including time spent asleep.
return clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) / (NSTimeInterval)NSEC_PER_SEC;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this a static local function instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I moved the clock read into a file-local static function.

+ (NSTimeInterval)monotonicTime
{
// Timer delays measure elapsed time, including time spent asleep.
return clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) / (NSTimeInterval)NSEC_PER_SEC;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use CLOCK_MONOTONIC or std::chrono::steady_clock instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I switched to std::chrono::steady_clock and convert its duration to fractional seconds for the existing NSTimeInterval calculations. The Apple implementation in libc++ uses CLOCK_MONOTONIC_RAW, which counts time spent asleep. All three wall-clock regression tests passed on the iOS simulator. Apple libc++ implementation

@meta-codesync

meta-codesync Bot commented Nov 11, 2026

Copy link
Copy Markdown

@javache has imported this pull request. If you are a Meta employee, you can view this in D131244913.

Comment on lines +324 to +327
} else if (sleepTarget < _sleepTimerTarget) {
// Convert to a date only when setting the run loop's wakeup.
_sleepTimer.fireDate = [NSDate dateWithTimeIntervalSinceNow:delay];
_sleepTimerTarget = sleepTarget;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we check this after device sleep? For example, schedule a 60-second timeout, sleep for 20 seconds, then resume before it's due. steady_clock includes sleep, but the run-loop timer appears to use uptime. Since the target hasn't changed, this check skips resetting fireDate.
Could that leave the callback late? A physical-device sleep/wake test would help confirm whether we need to rearm it on resume.

@FeliksLv01 FeliksLv01 Nov 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reran the 60-second setTimeout test on the iPhone 17 in airplane mode. The app returned to the foreground at 45.503 seconds, but the callback fired at 85.543 seconds of wall time, about 25.5 seconds late.

At the callback, performance.now() had advanced by 60.003 seconds. On iOS, React Native backs it with mach_absolute_time(), which pauses during system sleep. The 25.540-second difference is consistent with about 25.5 seconds of system sleep. This reproduces the delay you suspected.

codex-clipboard-573a4f33-c6e7-4463-890b-b5d85a7f9195

@FeliksLv01 FeliksLv01 Nov 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this. I updated scheduleSleepTimer: to reset the existing NSTimer’s fireDate when the target deadline is unchanged as well. This lets the existing scheduling path account for time spent asleep while keeping the original steady_clock deadline.

Before this PR, the existing scheduling path assigned fireDate even when the deadline was unchanged. The < check introduced in this PR skipped that assignment. With <=, the normal frame update after returning to the foreground rearms the existing NSTimer using the remaining time until the original steady_clock deadline, under the existing lock.

All three RCTTimingClockChangeTests passed on the iOS simulator. I also reran the same 60-second setTimeout test on the iPhone 17, unplugged and in airplane mode. Native clock logs measured 23.174 seconds of system sleep. The app became active at 41.093 seconds, and the callback fired at 60.001 seconds of wall time (performance.now() advanced by 36.827 seconds).

iphone17-timer-sleep-result

@FeliksLv01

Copy link
Copy Markdown
Contributor Author

I investigated the failing iOS RNTester E2E check. Both attempts of the Pressable flow timed out waiting for one_press_me_button to become visible, before any button interaction. The failure screenshot shows the RNTester Components screen rather than the Pressable example.

I reran the original pressable.yml locally twice using the CI-built Debug app and the JS code from this PR's head (b5bffc1f), on an iPhone 17 Pro simulator running iOS 26.2 with Maestro 2.6.1. Both runs passed (54s and 45s). The simulator model and iOS version now match CI, although the local host environment is not identical, so this does not conclusively rule out an issue with the change.

Could someone rerun the iOS RNTester E2E check when convenient?

@meta-codesync
meta-codesync Bot merged commit 85a285a into react:main Nov 13, 2026
166 of 167 checks passed
@react-native-bot

Copy link
Copy Markdown
Collaborator

This pull request was successfully merged by @FeliksLv01 in 85a285a

When will my fix make it into a release? | How to file a pick request?

@react-native-bot react-native-bot added the Merged This PR has been merged. label Nov 13, 2026
@FeliksLv01
FeliksLv01 deleted the fix/ios-timer-clock-tests branch November 13, 2026 13:45
usama-liaqat pushed a commit to usama-liaqat/react-native that referenced this pull request Nov 14, 2026
## Summary:

On iOS, `RCTTiming` uses wall-clock dates for pending JS timers. Moving the system clock backward can delay `setTimeout` and `setInterval`, and moving it forward can make them fire early.

Use a monotonic clock for timer deadlines and wakeup ordering so callbacks follow elapsed time, including time spent asleep.

Related to react#59782.

## Changelog:

[IOS] [FIXED] - Keep JavaScript timers on schedule when the system clock changes.

## Test Plan:

Ran `RCTTimingClockChangeTests` through the `RNTesterUnitTests` scheme on an iPhone 18 Pro simulator running iOS 27.2. The tests simulate a one-hour wall-clock jump and check that:

- A forward jump does not fire a pending `setTimeout` early.
- A backward jump does not delay a due `setTimeout`.
- A backward jump does not delay the next callback of a running `setInterval`.

All three tests failed on upstream commit `f480836` and passed with the fix. Both runs used the same local build settings. I excluded the existing `RCTImageLoaderTests.m` because it references undefined macros in this checkout, and added the Xcode Swift compatibility library search path. These build overrides are not part of the PR.

From `packages/rn-tester`, with `SIMULATOR_UDID` set to the simulator used for testing:

```sh
swift_lib="$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphonesimulator"
xcodebuild test -workspace RNTesterPods.xcworkspace -scheme RNTesterUnitTests \
  -destination "platform=iOS Simulator,id=$SIMULATOR_UDID" \
  -only-testing:RNTesterUnitTests/RCTTimingClockChangeTests -parallel-testing-enabled NO \
  'EXCLUDED_SOURCE_FILE_NAMES=RCTImageLoaderTests.m' \
  "LIBRARY_SEARCH_PATHS=\$(inherited) $swift_lib" 2>&1 | xcbeautify
```

The test run was limited to this regression suite. A physical-device clock change was not tested.

Differential Revision: D131244913

Pull Request resolved: react#59865
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Merged This PR has been merged. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants