Repository navigation
Fix iOS JS timers when the system clock changes - #59865
Conversation
|
Hi @FeliksLv01! Thank you for your pull request and welcome to our community. Action RequiredIn 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. ProcessIn 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 If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
| + (NSTimeInterval)monotonicTime | ||
| { | ||
| // Timer delays measure elapsed time, including time spent asleep. | ||
| return clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) / (NSTimeInterval)NSEC_PER_SEC; | ||
| } |
There was a problem hiding this comment.
Make this a static local function instead.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Should we use CLOCK_MONOTONIC or std::chrono::steady_clock instead?
There was a problem hiding this comment.
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
|
@javache has imported this pull request. If you are a Meta employee, you can view this in D131244913. |
| } else if (sleepTarget < _sleepTimerTarget) { | ||
| // Convert to a date only when setting the run loop's wakeup. | ||
| _sleepTimer.fireDate = [NSDate dateWithTimeIntervalSinceNow:delay]; | ||
| _sleepTimerTarget = sleepTarget; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
|
I investigated the failing iOS RNTester E2E check. Both attempts of the Pressable flow timed out waiting for I reran the original Could someone rerun the iOS RNTester E2E check when convenient? |
|
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? |
## 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
Summary:
On iOS,
RCTTiminguses wall-clock dates for pending JS timers. Moving the system clock backward can delaysetTimeoutandsetInterval, 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
RCTTimingClockChangeTeststhrough theRNTesterUnitTestsscheme on an iPhone 18 Pro simulator running iOS 27.2. The tests simulate a one-hour wall-clock jump and check that:setTimeoutearly.setTimeout.setInterval.All three tests failed on upstream commit
f480836and passed with the fix. Both runs used the same local build settings. I excluded the existingRCTImageLoaderTests.mbecause 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, withSIMULATOR_UDIDset to the simulator used for testing:The test run was limited to this regression suite. A physical-device clock change was not tested.