Skip to content

Budget Android pre-mounting by the display's frame, not a 60 Hz one - #59057

Open
YevheniiKotyrlo wants to merge 3 commits into
react:mainfrom
YevheniiKotyrlo:fix/android-premount-frame-budget
Open

YevheniiKotyrlo wants to merge 3 commits into
react:mainfrom
YevheniiKotyrlo:fix/android-premount-frame-budget

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

MountItemDispatcher.dispatchPreMountItems stops pre-mounting at frameTimeNanos + FRAME_TIME_NS / 2, where FRAME_TIME_NS is a 60 Hz frame. That budget is 8.33 ms on every display: all of a 120 Hz frame and more than a 144 Hz one, so a frame that pre-mounts a large backlog on a fast display spends its whole budget before it traverses.

The dispatcher now pre-mounts for half of the default display's current frame, and never longer than half of a 60 Hz frame: a display running below 60 Hz, such as an adaptive panel idling at 1 Hz, keeps today's 8.33 ms instead of half of its long frame. DisplayFrameInterval reads the refresh rate from DisplayManager when FabricUIManager initializes and again whenever the display reports a change, so a frame reads a cached interval rather than the display; a display that reports no refresh rate gets 8.33 ms as well. The loop reads com.facebook.react.common.SystemClock so a test can drive it.

Changelog:

[ANDROID] [FIXED] - Budget view pre-mounting by the display's current refresh rate instead of a 60 Hz frame

Test Plan:

MountItemDispatcherTest (new) covers the budget at 60, 90, 120, 144, 165, 185 and 240 Hz, at 1, 10, 24, 30 and 48 Hz, while the display reports no refresh rate, after the display drops from 120 Hz back to 60 Hz, and across a skipped frame. DisplayFrameIntervalTest (new) covers reading the default display's frame from 1 Hz to 240 Hz, following a refresh-rate change the display reports, keeping the last frame once stopped, and reporting no frame while the display reports no refresh rate.

Every queued item takes 1 ms. With the 60 Hz budget, the cases above 60 Hz fail: expected:<{60=9, 90=[6, 120=5, 144=4, 165=4, 185=3, 240=3]}> but was:<{60=9, 90=[9, 120=9, 144=9, 165=9, 185=9, 240=9]}>. Without the 8.33 ms ceiling, the cases below 60 Hz fail: expected:<{1=[9, 10=9, 24=9, 30=9, 48=9]}> but was:<{1=[20, 10=20, 24=20, 30=17, 48=11]}>.

  • ./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest -PenableWarningsAsErrors=true: 730 tests, 0 failures
  • ./gradlew :packages:react-native:ReactAndroid:compileReleaseKotlin :packages:react-native:ReactAndroid:compileReleaseJavaWithJavac: clean
  • node ./scripts/format-kotlin.js --check and node ./scripts/format-java.js --check: clean

I didn't run the full Android build, the template and HelloWorld app builds, or the Android e2e jobs locally.

MountItemDispatcher stopped pre-mounting views at half of a 60 Hz frame
(8.33 ms) on every display: all of a 120 Hz frame and more than a 144 Hz
one. It now pre-mounts for half of the shortest interval between the frames
it has run on, which is the display's frame at its fastest refresh rate, and
never for longer than before. The loop reads React Native's SystemClock so
MountItemDispatcherTest can drive it.
@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 Oct 14, 2026
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review October 14, 2026 11:36
@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 Oct 14, 2026
@javache

javache commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

The refresh rate can change dynamically, so forever locking to the smallest time may be suboptimal - can we try to use the screen's current refresh rate?

The shortest frame interval seen so far stays at the display's highest
rate, so after a drop from 120 Hz back to 60 Hz pre-mounting keeps a
120 Hz budget, half of what the frame allows.

DisplayFrameInterval reads the default display's refresh rate from
DisplayManager when the display reports a change, never per frame, and
MountItemDispatcher pre-mounts for half of that frame.
@YevheniiKotyrlo

Copy link
Copy Markdown
Contributor Author

Updated in 35f8355: pre-mounting now takes half of the default display's current frame, read from DisplayManager when the display reports a change rather than on every frame, so a drop from 120 Hz back to 60 Hz restores the 60 Hz budget. MountItemDispatcherTest covers that switch and DisplayFrameIntervalTest the listener.

Half of the display's frame grows below 60 Hz, to 500 ms at the 1 Hz an
adaptive panel can idle at, so the budget is now the shorter of half the
display's frame and half of a 60 Hz frame. A display that reports no
refresh rate gets half of a 60 Hz frame rather than an assumed rate.
@YevheniiKotyrlo

Copy link
Copy Markdown
Contributor Author

Follow-up in 3d780fb: below 60 Hz the budget stays at today's 8.33 ms instead of half of the longer frame, since an adaptive panel idling at 1 Hz would otherwise pre-mount for 500 ms, and a display that reports no refresh rate gets 8.33 ms as well.

This branch has not been deployed

No deployments
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. 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.

2 participants