Skip to content

Commit e71fb78

Browse files
Spend the time before a traversal only in the frame after the one measured
With disableIdleMountItemFrameCallbackRearmAndroid on, the frame callback stops re-arming once nothing is left to mount, and the sleep a slice had measured after the last frame waited for the next burst's first frame, which spent it before its traversal however many vsyncs had passed. The measured time now lapses once a frame interval has passed since it was measured, so only the frame right after the measured one spends it.
1 parent 8c23b27 commit e71fb78

2 files changed

Lines changed: 39 additions & 10 deletions

File tree

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/PreMountItemsBetweenFrames.kt‎

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ import com.facebook.react.common.SystemClock
2828
* frame's draw until the render thread takes the frame, and the frame leaves no time after it. A
2929
* frame then pre-mounts before its traversal for as long as the last frame's UI thread slept after
3030
* its callbacks, less one slice and at most half a frame: the work shortens that sleep instead of
31-
* delaying the frame, and a frame that no longer sleeps ends it.
31+
* delaying the frame, and a frame that no longer sleeps ends it. A vsync that passes with no frame
32+
* callback, as while the callback is not re-armed with nothing to mount, ends it too.
3233
*/
3334
internal class PreMountItemsBetweenFrames(
3435
private val dispatcher: MountItemDispatcher,
@@ -38,12 +39,13 @@ internal class PreMountItemsBetweenFrames(
3839
exceptionHandler: JSExceptionHandler,
3940
) {
4041
private var nextFrameNanos: Long = 0L
41-
private var halfFrameNanos: Long = SIXTY_HERTZ_FRAME_NANOS / 2
42+
private var frameIntervalNanos: Long = SIXTY_HERTZ_FRAME_NANOS
4243
private var isSliceQueued: Boolean = false
4344
private var isFrameSleepUnmeasured: Boolean = false
4445
private var callbacksEndNanos: Long = 0L
4546
private var callbacksEndThreadCpuNanos: Long = 0L
4647
private var beforeTraversalBudgetNanos: Long = 0L
48+
private var beforeTraversalBudgetMeasuredAtNanos: Long = 0L
4749
private var preMountedBeforeTraversalNanos: Long = 0L
4850
private val slice: Runnable =
4951
object : GuardedRunnable(exceptionHandler) {
@@ -53,7 +55,10 @@ internal class PreMountItemsBetweenFrames(
5355
}
5456
}
5557

56-
/** Pre-mounts before this frame's traversal for the budget the last frame's sleep left it. */
58+
/**
59+
* Pre-mounts before this frame's traversal for the budget the last frame's sleep left it, unless
60+
* a vsync has passed since that sleep was measured.
61+
*/
5762
@UiThread
5863
@ThreadConfined(UI)
5964
fun preMountBeforeTraversal() {
@@ -64,6 +69,9 @@ internal class PreMountItemsBetweenFrames(
6469
return
6570
}
6671
val startNanos = SystemClock.nanoTime()
72+
if (startNanos - beforeTraversalBudgetMeasuredAtNanos > frameIntervalNanos) {
73+
return
74+
}
6775
dispatcher.dispatchPreMountItemsUntil(startNanos + budgetNanos)
6876
preMountedBeforeTraversalNanos = SystemClock.nanoTime() - startNanos
6977
}
@@ -72,9 +80,8 @@ internal class PreMountItemsBetweenFrames(
7280
@UiThread
7381
@ThreadConfined(UI)
7482
fun scheduleAfterFrame(frameTimeNanos: Long) {
75-
val frameIntervalNanos = frameIntervalSource.frameIntervalNanos() ?: SIXTY_HERTZ_FRAME_NANOS
83+
frameIntervalNanos = frameIntervalSource.frameIntervalNanos() ?: SIXTY_HERTZ_FRAME_NANOS
7684
nextFrameNanos = frameTimeNanos + frameIntervalNanos
77-
halfFrameNanos = frameIntervalNanos / 2
7885
if (dispatcher.hasPreMountItems() || preMountedBeforeTraversalNanos > 0L) {
7986
callbacksEndNanos = SystemClock.nanoTime()
8087
callbacksEndThreadCpuNanos = threadCpuClock.threadCpuTimeNanos()
@@ -97,23 +104,24 @@ internal class PreMountItemsBetweenFrames(
97104
* callbacks and its end, plus what it pre-mounted before its traversal, which that sleep
98105
* absorbed, less one slice. A frame that slept less than half a slice gets none.
99106
*/
100-
private fun measureBeforeTraversalBudget(): Long {
107+
private fun measureBeforeTraversalBudget(nowNanos: Long): Long {
101108
val threadCpuNanos = threadCpuClock.threadCpuTimeNanos()
102109
if (threadCpuNanos < 0L || callbacksEndThreadCpuNanos < 0L) {
103110
return 0L
104111
}
105-
val asleepNanos =
106-
(SystemClock.nanoTime() - callbacksEndNanos) - (threadCpuNanos - callbacksEndThreadCpuNanos)
112+
val asleepNanos = (nowNanos - callbacksEndNanos) - (threadCpuNanos - callbacksEndThreadCpuNanos)
107113
if (asleepNanos < SLICE_NANOS / 2) {
108114
return 0L
109115
}
110-
return minOf(halfFrameNanos, asleepNanos + preMountedBeforeTraversalNanos - SLICE_NANOS)
116+
return minOf(frameIntervalNanos / 2, asleepNanos + preMountedBeforeTraversalNanos - SLICE_NANOS)
111117
}
112118

113119
private fun runSlice() {
114120
if (isFrameSleepUnmeasured) {
115121
isFrameSleepUnmeasured = false
116-
beforeTraversalBudgetNanos = measureBeforeTraversalBudget()
122+
beforeTraversalBudgetMeasuredAtNanos = SystemClock.nanoTime()
123+
beforeTraversalBudgetNanos =
124+
measureBeforeTraversalBudget(beforeTraversalBudgetMeasuredAtNanos)
117125
}
118126
if (dispatcher.hasMountItems()) {
119127
dispatcher.tryDispatchMountItems()

‎packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/mounting/PreMountItemsBetweenFramesTest.kt‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,27 @@ class PreMountItemsBetweenFramesTest {
390390
assertThat(runFrame(frames[2], MILLISECOND_NANOS)).isEqualTo(9)
391391
}
392392

393+
@Test
394+
fun aFrameAfterVsyncsWithNoFrameCallbackLeavesItsTraversalAlone() {
395+
displayFrameIntervalNanos = SIXTY_HERTZ_FRAME_NANOS
396+
val frames = (0 until 2).map { frame -> FIRST_FRAME_NANOS + frame * SIXTY_HERTZ_FRAME_NANOS }
397+
queuePreMountItems(count = 9)
398+
runFrame(frames[0], MILLISECOND_NANOS, renderThreadReadyAtNanos = frames[1])
399+
runBetweenFrames()
400+
runFrame(
401+
frames[1],
402+
MILLISECOND_NANOS,
403+
renderThreadReadyAtNanos = frames[1] + SIXTY_HERTZ_FRAME_NANOS,
404+
)
405+
runBetweenFrames()
406+
407+
// With nothing left to mount, the frame callback stops re-arming, and the next burst's first
408+
// frame comes ten vsyncs later: the sleep measured before them says nothing about its frame.
409+
queuePreMountItems(count = 9)
410+
411+
assertThat(runFrame(frames[1] + 11 * SIXTY_HERTZ_FRAME_NANOS, MILLISECOND_NANOS)).isZero()
412+
}
413+
393414
@Test
394415
fun framesThatKeepWaitingQueueNothingOnceTheItemsRunOut() {
395416
displayFrameIntervalNanos = SIXTY_HERTZ_FRAME_NANOS

0 commit comments

Comments
 (0)