Skip to content

Fix DOMHighResTimeStamp round-trip truncation in timing primitives - #57975

Closed
GijsWeterings wants to merge 1 commit into
react:mainfrom
GijsWeterings:export-D116286868
Closed

Fix DOMHighResTimeStamp round-trip truncation in timing primitives#57975
GijsWeterings wants to merge 1 commit into
react:mainfrom
GijsWeterings:export-D116286868

Conversation

@GijsWeterings

@GijsWeterings GijsWeterings commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Summary:
HighResDuration::fromDOMHighResTimeStamp and HighResTimeStamp::fromDOMHighResTimeStamp converted milliseconds back to nanoseconds with static_cast<int64_t>(units * 1e6), which truncates toward zero.

toDOMHighResTimeStamp() divides the nanosecond count by 1e6 (one rounding) and multiplying back by 1e6 rounds again, so the product frequently lands a hair below the original integer (e.g. 537648854729249.97). Truncation then chops off a whole nanosecond, so fromDOMHighResTimeStamp(toDOMHighResTimeStamp(x)) != x for roughly 2% of random now() values.

That is the source of an intermittent BridgingTest/highResTimeStampTest failure, which reported:

Expected equality of these values:
  timestamp
    Which is: 8-byte object <22-02 00-21 FD-E8 01-00>
  bridging::fromJs<HighResTimeStamp>( rt, bridging::toJs(rt, timestamp), invoker)
    Which is: 8-byte object <21-02 00-21 FD-E8 01-00>

Round to the nearest nanosecond instead of truncating. Nanosecond values below 2^53 are exactly representable in a double, so the round trip is now exact for every value below 2.25e15 ns (26 days of monotonic clock); beyond that the double's ULP exceeds 0.5 ns and residual error is at most 2 ns, which is a floor of the DOM representation itself.

Rounding is also the correct semantic independently of the round trip — truncation gives a systematic downward bias and is asymmetric across zero, which affects the other callers (RCTHighResTimeStampFromSeconds for touch timestamps, RuntimeTargetConsole console.timeStamp, and PerformanceTracer).

The rounding is std::llround, which costs both overloads their constexpr: <cmath> rounding functions do not become usable in a constant expression until C++23 (P0533), and this header is compiled as C++20 everywhere (-std=c++20 in rn_defs.bzl, react-native-flags.cmake, the podspecs and Package.swift). Both carry a TODO to restore constexpr once the C++23 rollout reaches them.

Dropping it is safe here. All 14 call sites are plain runtime calls — none is in a constant-expression context, none assigns to a constexpr variable or feeds a static_assert — and both functions are defined inside the class body, so they stay implicitly inline and neither linkage nor ABI changes. fromDOMHighResTimeStamp converts a double arriving from JS, so constant-evaluating it was never meaningful in the first place.

This does narrow the published API surface, so the C++ API snapshots are regenerated: the only change across all nine .api files is the constexpr keyword dropping off these two declarations, 18 lines in total.

HighResTimeStamp::fromDOMHighResTimeStamp now delegates to HighResDuration's so there is a single implementation.

highResTimeStampTest previously asserted on HighResTimeStamp::now(), whose magnitude is host-uptime-dependent, so it only tripped the bug on about 2% of runs. It now round-trips five fixed nanosecond values (including the exact value from the failing run), which exercises the bug on every run. No test was skipped, disabled, or loosened.

Changelog:
[General][Fixed] - Round instead of truncate when converting a DOMHighResTimeStamp back to nanoseconds, so HighResTimeStamp and HighResDuration round trips are exact

Reviewed By: javache

Differential Revision: D116286868

@meta-codesync

meta-codesync Bot commented Aug 17, 2026

Copy link
Copy Markdown

@GijsWeterings has exported this pull request. If you are a Meta employee, you can view the originating Diff in D116286868.

@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 Aug 17, 2026
Summary:
`HighResDuration::fromDOMHighResTimeStamp` and `HighResTimeStamp::fromDOMHighResTimeStamp` converted milliseconds back to nanoseconds with `static_cast<int64_t>(units * 1e6)`, which truncates toward zero.

`toDOMHighResTimeStamp()` divides the nanosecond count by 1e6 (one rounding) and multiplying back by 1e6 rounds again, so the product frequently lands a hair below the original integer (e.g. `537648854729249.97`). Truncation then chops off a whole nanosecond, so `fromDOMHighResTimeStamp(toDOMHighResTimeStamp(x)) != x` for roughly 2% of random `now()` values.

That is the source of an intermittent `BridgingTest/highResTimeStampTest` failure, which reported:

```
Expected equality of these values:
  timestamp
    Which is: 8-byte object <22-02 00-21 FD-E8 01-00>
  bridging::fromJs<HighResTimeStamp>( rt, bridging::toJs(rt, timestamp), invoker)
    Which is: 8-byte object <21-02 00-21 FD-E8 01-00>
```

Round to the nearest nanosecond instead of truncating. Nanosecond values below 2^53 are exactly representable in a double, so the round trip is now exact for every value below 2.25e15 ns (26 days of monotonic clock); beyond that the double's ULP exceeds 0.5 ns and residual error is at most 2 ns, which is a floor of the DOM representation itself.

Rounding is also the correct semantic independently of the round trip — truncation gives a systematic downward bias and is asymmetric across zero, which affects the other callers (`RCTHighResTimeStampFromSeconds` for touch timestamps, `RuntimeTargetConsole` `console.timeStamp`, and `PerformanceTracer`).

The rounding is `std::llround`, which costs both overloads their `constexpr`: `<cmath>` rounding functions do not become usable in a constant expression until C++23 (P0533), and this header is compiled as C++20 everywhere (`-std=c++20` in `rn_defs.bzl`, `react-native-flags.cmake`, the podspecs and `Package.swift`). Both carry a `TODO` to restore `constexpr` once the C++23 rollout reaches them.

Dropping it is safe here. All 14 call sites are plain runtime calls — none is in a constant-expression context, none assigns to a `constexpr` variable or feeds a `static_assert` — and both functions are defined inside the class body, so they stay implicitly `inline` and neither linkage nor ABI changes. `fromDOMHighResTimeStamp` converts a `double` arriving from JS, so constant-evaluating it was never meaningful in the first place.

This does narrow the published API surface, so the C++ API snapshots are regenerated: the only change across all nine `.api` files is the `constexpr` keyword dropping off these two declarations, 18 lines in total.

`HighResTimeStamp::fromDOMHighResTimeStamp` now delegates to `HighResDuration`'s so there is a single implementation.

`highResTimeStampTest` previously asserted on `HighResTimeStamp::now()`, whose magnitude is host-uptime-dependent, so it only tripped the bug on about 2% of runs. It now round-trips five fixed nanosecond values (including the exact value from the failing run), which exercises the bug on every run. No test was skipped, disabled, or loosened.

Changelog:
[General][Fixed] - Round instead of truncate when converting a `DOMHighResTimeStamp` back to nanoseconds, so `HighResTimeStamp` and `HighResDuration` round trips are exact

Reviewed By: javache

Differential Revision: D116286868
@meta-codesync meta-codesync Bot closed this in c467843 Aug 17, 2026
@meta-codesync meta-codesync Bot added the Merged This PR has been merged. label Aug 17, 2026
@meta-codesync

meta-codesync Bot commented Aug 17, 2026

Copy link
Copy Markdown

This pull request has been merged in c467843.

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. meta-exported p: Facebook Partner: Facebook Partner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant