Skip to content

Commit 50115fd

Browse files
Pre-mount Android views in the time each frame leaves free, behind a flag
With enablePremountItemsBetweenFramesAndroid on, the DISPATCH_UI frame callback no longer pre-mounts for half of a 60 Hz frame before the frame's traversal. PreMountItemsBetweenFrames pre-mounts after the frame, in slices posted to the UI thread's looper as asynchronous messages, each starting items for about 1 ms, until the next vsync: from Android 13 the spacing of the frame timelines the Choreographer hands a vsync callback, otherwise the display's refresh rate. After a frame that left no time, it also pre-mounts before the next traversal: for as long as that frame's draw slept, or for half a frame once two frames in a row each took a frame interval of their own work, at most half a frame and half of a 30 Hz frame either way. A commit whose mount items arrive while the slices run is mounted by the next slice, which first runs every pre-mount item left; one the pull model queues as a surface to pull waits for the frame, and mount items wait with it. Slices run only while the frame callback would mount.
1 parent 79a5629 commit 50115fd

31 files changed

Lines changed: 2211 additions & 88 deletions

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java‎

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import android.annotation.SuppressLint;
2222
import android.content.Context;
2323
import android.graphics.Point;
24+
import android.os.Debug;
2425
import android.os.SystemClock;
2526
import android.view.View;
2627
import android.view.accessibility.AccessibilityEvent;
@@ -58,8 +59,11 @@
5859
import com.facebook.react.fabric.internal.interop.InteropUIBlockListener;
5960
import com.facebook.react.fabric.interop.UIBlock;
6061
import com.facebook.react.fabric.interop.UIBlockViewResolver;
62+
import com.facebook.react.fabric.mounting.DisplayFrameInterval;
63+
import com.facebook.react.fabric.mounting.FrameIntervalSources;
6164
import com.facebook.react.fabric.mounting.MountItemDispatcher;
6265
import com.facebook.react.fabric.mounting.MountingManager;
66+
import com.facebook.react.fabric.mounting.PreMountItemsBetweenFrames;
6367
import com.facebook.react.fabric.mounting.SurfaceMountingManager;
6468
import com.facebook.react.fabric.mounting.mountitems.BatchMountItem;
6569
import com.facebook.react.fabric.mounting.mountitems.BatchedAnimatedPropsMountItem;
@@ -182,6 +186,8 @@ public class FabricUIManager
182186
private final MountingManager mMountingManager;
183187
private final FabricEventDispatcher mEventDispatcher;
184188
private final MountItemDispatcher mMountItemDispatcher;
189+
private final DisplayFrameInterval mDisplayFrameInterval;
190+
private final PreMountItemsBetweenFrames mPreMountItemsBetweenFrames;
185191
private final ViewManagerRegistry mViewManagerRegistry;
186192

187193
private final TextEffectRegistry mTextEffectRegistry = new TextEffectRegistry();
@@ -258,6 +264,15 @@ public FabricUIManager(
258264
mMountingManager = new MountingManager(viewManagerRegistry, mMountItemExecutor);
259265
mMountItemDispatcher =
260266
new MountItemDispatcher(mMountingManager, new MountItemDispatchListener());
267+
mDisplayFrameInterval = new DisplayFrameInterval(reactContext);
268+
mPreMountItemsBetweenFrames =
269+
new PreMountItemsBetweenFrames(
270+
mMountItemDispatcher,
271+
FrameIntervalSources.create(mDisplayFrameInterval),
272+
Debug::threadCpuTimeNanos,
273+
mDispatchUIFrameCallback::isMountingAllowed,
274+
UiThreadUtil.getUiThreadHandler(),
275+
mDispatchUIFrameCallback::handleBetweenFramesException);
261276
mEventDispatcher = new FabricEventDispatcher(reactContext, new FabricEventEmitter(this));
262277
mBatchEventDispatchedListener = batchEventDispatchedListener;
263278
mReactApplicationContext.addLifecycleEventListener(this);
@@ -463,6 +478,9 @@ public void stopSurface(final int surfaceID) {
463478
@Override
464479
public void initialize() {
465480
mEventDispatcher.addBatchEventDispatchedListener(mBatchEventDispatchedListener);
481+
if (ReactNativeFeatureFlags.enablePremountItemsBetweenFramesAndroid()) {
482+
mDisplayFrameInterval.start();
483+
}
466484
if (ReactNativeFeatureFlags.enableFabricLogs()) {
467485
mDevToolsReactPerfLogger = new DevToolsReactPerfLogger();
468486
mDevToolsReactPerfLogger.addDevToolsReactPerfLoggerListener(FABRIC_PERF_LOGGER);
@@ -498,6 +516,7 @@ public void invalidate() {
498516

499517
mEventDispatcher.removeBatchEventDispatchedListener(mBatchEventDispatchedListener);
500518
mEventDispatcher.invalidate();
519+
mDisplayFrameInterval.stop();
501520

502521
mReactApplicationContext.unregisterComponentCallbacks(mViewManagerRegistry);
503522
mViewManagerRegistry.invalidate();
@@ -1680,15 +1699,33 @@ void resume() {
16801699
schedule();
16811700
}
16821701

1683-
@UiThread
1684-
@ThreadConfined(UI)
1702+
@AnyThread
16851703
void pause() {
16861704
ReactChoreographer.getInstance()
16871705
.removeFrameCallback(ReactChoreographer.CallbackType.DISPATCH_UI, this);
1706+
// invalidate() pauses from any thread; the slices it stops belong to the UI thread.
1707+
if (UiThreadUtil.isOnUiThread()) {
1708+
mPreMountItemsBetweenFrames.cancel();
1709+
} else {
1710+
UiThreadUtil.runOnUiThread(mPreMountItemsBetweenFrames::cancel);
1711+
}
16881712
mShouldSchedule = false;
16891713
mIsScheduled = false;
16901714
}
16911715

1716+
@AnyThread
1717+
boolean isMountingAllowed() {
1718+
return mIsMountingEnabled && !mDestroyed;
1719+
}
1720+
1721+
@UiThread
1722+
@ThreadConfined(UI)
1723+
void handleBetweenFramesException(Exception exception) {
1724+
FLog.e(TAG, "Exception thrown when pre-mounting or mounting between frames", exception);
1725+
mIsMountingEnabled = false;
1726+
mReactApplicationContext.getExceptionHandler().handleException(exception);
1727+
}
1728+
16921729
@Override
16931730
@UiThread
16941731
@ThreadConfined(UI)
@@ -1728,13 +1765,20 @@ public void doFrameGuarded(long frameTimeNanos) {
17281765
}
17291766

17301767
try {
1731-
// First, execute as many pre mount items as we can within frameTimeNanos time.
1732-
// If not all pre mount items were executed, following may happen:
1733-
// 1. In case there are view commands or mount items in MountItemDispatcher: execute
1734-
// remaining pre mount items.
1735-
// 2. In case there are no view commands or mount items, wait until next frame.
1736-
mMountItemDispatcher.dispatchPreMountItems(frameTimeNanos);
1737-
mMountItemDispatcher.tryDispatchMountItems();
1768+
// A dispatch runs every pending pre-mount item before its pulls and mount items. What is
1769+
// left runs either here, for half of a 60 Hz frame, or after this frame's traversal until
1770+
// the next vsync, and here only when the last frame left no time after it: for as long as
1771+
// its draw slept, or for half a frame once two frames in a row were busy for a whole frame
1772+
// interval. The slices are queued once this frame's mount items have run, and run only
1773+
// while mounting is enabled, so a frame that fails to mount leaves nothing to pre-mount.
1774+
if (ReactNativeFeatureFlags.enablePremountItemsBetweenFramesAndroid()) {
1775+
mPreMountItemsBetweenFrames.preMountBeforeTraversal();
1776+
mMountItemDispatcher.tryDispatchMountItems();
1777+
mPreMountItemsBetweenFrames.scheduleAfterFrame(frameTimeNanos);
1778+
} else {
1779+
mMountItemDispatcher.dispatchPreMountItems(frameTimeNanos);
1780+
mMountItemDispatcher.tryDispatchMountItems();
1781+
}
17381782
} catch (Exception ex) {
17391783
FLog.e(TAG, "Exception thrown when executing UIFrameGuarded", ex);
17401784
mIsMountingEnabled = false;
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.fabric.mounting
9+
10+
import android.content.Context
11+
import android.hardware.display.DisplayManager
12+
import android.view.Display
13+
import com.facebook.react.bridge.UiThreadUtil
14+
15+
/**
16+
* The default display's frame interval at its current refresh rate. It is read again when the
17+
* display reports a change, so a frame never reads the display itself.
18+
*/
19+
internal class DisplayFrameInterval(context: Context) :
20+
PreMountItemsBetweenFrames.FrameIntervalSource, DisplayManager.DisplayListener {
21+
private val displayManager: DisplayManager? = context.getSystemService(DisplayManager::class.java)
22+
23+
@Volatile private var frameIntervalNanos: Long? = null
24+
25+
fun start() {
26+
displayManager?.registerDisplayListener(this, UiThreadUtil.getUiThreadHandler())
27+
// Read once the listener is registered, so a change between the two is not lost, and on the
28+
// listener's thread, so neither store overwrites the other with an older rate. From Android 16
29+
// a listener also gets refresh-rate changes only once the process has read the rate.
30+
if (UiThreadUtil.isOnUiThread()) {
31+
frameIntervalNanos = readFrameIntervalNanos()
32+
} else {
33+
UiThreadUtil.runOnUiThread { frameIntervalNanos = readFrameIntervalNanos() }
34+
}
35+
}
36+
37+
fun stop() {
38+
displayManager?.unregisterDisplayListener(this)
39+
}
40+
41+
override fun frameIntervalNanos(): Long? = frameIntervalNanos
42+
43+
override fun onDisplayChanged(displayId: Int) {
44+
if (displayId == Display.DEFAULT_DISPLAY) {
45+
frameIntervalNanos = readFrameIntervalNanos()
46+
}
47+
}
48+
49+
override fun onDisplayAdded(displayId: Int): Unit = Unit
50+
51+
override fun onDisplayRemoved(displayId: Int): Unit = Unit
52+
53+
private fun readFrameIntervalNanos(): Long? {
54+
val refreshRate = displayManager?.getDisplay(Display.DEFAULT_DISPLAY)?.refreshRate ?: 0f
55+
return if (refreshRate > 0f) (NANOSECONDS_PER_SECOND / refreshRate).toLong() else null
56+
}
57+
58+
private companion object {
59+
private const val NANOSECONDS_PER_SECOND: Double = 1_000_000_000.0
60+
}
61+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.fabric.mounting
9+
10+
import android.os.Build
11+
import androidx.annotation.DoNotInline
12+
import androidx.annotation.RequiresApi
13+
14+
internal object FrameIntervalSources {
15+
/**
16+
* The frame interval source for the running Android version: from Android 13 the vsync's frame
17+
* timelines, with [displayFrameInterval] behind them, and before it [displayFrameInterval] alone.
18+
*/
19+
@JvmStatic
20+
fun create(
21+
displayFrameInterval: DisplayFrameInterval
22+
): PreMountItemsBetweenFrames.FrameIntervalSource =
23+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
24+
Api33Utils.createVsyncFrameInterval(displayFrameInterval)
25+
} else {
26+
displayFrameInterval
27+
}
28+
29+
/** Holds the only reference to the Android 13 class, so an older Android never verifies it. */
30+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
31+
private object Api33Utils {
32+
@DoNotInline
33+
fun createVsyncFrameInterval(
34+
displayFrameInterval: DisplayFrameInterval
35+
): PreMountItemsBetweenFrames.FrameIntervalSource = VsyncFrameInterval(displayFrameInterval)
36+
}
37+
}

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

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
package com.facebook.react.fabric.mounting
99

10-
import android.os.SystemClock
1110
import android.view.View
1211
import androidx.annotation.UiThread
1312
import com.facebook.common.logging.FLog
@@ -17,6 +16,7 @@ import com.facebook.react.bridge.ReactIgnorableMountingException
1716
import com.facebook.react.bridge.ReactNoCrashSoftException
1817
import com.facebook.react.bridge.ReactSoftExceptionLogger
1918
import com.facebook.react.bridge.RetryableMountingLayerException
19+
import com.facebook.react.common.SystemClock
2020
import com.facebook.react.fabric.FabricUIManager
2121
import com.facebook.react.fabric.mounting.mountitems.DispatchCommandMountItem
2222
import com.facebook.react.fabric.mounting.mountitems.MountItem
@@ -27,6 +27,9 @@ import java.util.Queue
2727
import java.util.concurrent.ConcurrentHashMap
2828
import java.util.concurrent.ConcurrentLinkedQueue
2929

30+
/** A 60 Hz frame: half of it is a frame's pre-mount budget from its start. */
31+
internal const val SIXTY_HERTZ_FRAME_NANOS: Long = 1_000_000_000L / 60
32+
3033
internal class MountItemDispatcher(
3134
private val mountingManager: MountingManager,
3235
private val itemDispatchListener: ItemDispatchListener,
@@ -49,6 +52,12 @@ internal class MountItemDispatcher(
4952
/** Pull model: @return true if any surface has a transaction waiting to be pulled. */
5053
fun hasSurfaceIdsToPull(): Boolean = surfaceIdsToPull.isNotEmpty()
5154

55+
/**
56+
* @return true if any mount items or view commands are waiting to be dispatched. A dispatch also
57+
* pulls the surfaces waiting to be pulled, before the mount items.
58+
*/
59+
fun hasMountItems(): Boolean = !viewCommandMountItems.isEmpty() || !mountItems.isEmpty()
60+
5261
// Items can be queued from any thread while the DISPATCH_UI frame callback is disarmed at
5362
// idle (see FabricUIManager's doFrameGuarded); the listener re-arms it.
5463
private fun notifyItemsQueued() {
@@ -64,8 +73,6 @@ internal class MountItemDispatcher(
6473
var runStartTime: Long = 0L
6574
private set
6675

67-
private var lastFrameTimeNanos: Long = 0L
68-
6976
fun addViewCommandMountItem(mountItem: DispatchCommandMountItem) {
7077
if (!ReactNativeFeatureFlags.disableEarlyViewCommandExecution()) {
7178
viewCommandMountItems.add(mountItem)
@@ -355,15 +362,25 @@ internal class MountItemDispatcher(
355362
@UiThread
356363
@ThreadConfined(UI)
357364
fun dispatchPreMountItems(frameTimeNanos: Long) {
358-
lastFrameTimeNanos = frameTimeNanos
365+
dispatchPreMountItemsUntil(frameTimeNanos + SIXTY_HERTZ_FRAME_NANOS / 2)
366+
}
367+
368+
fun hasPreMountItems(): Boolean = !preMountItems.isEmpty()
359369

370+
/**
371+
* Executes pre-mount items until [deadlineNanos], and returns whether any are left. An item
372+
* starts only while the deadline has not passed, so the last one can run past it by its own
373+
* duration.
374+
*/
375+
@UiThread
376+
@ThreadConfined(UI)
377+
fun dispatchPreMountItemsUntil(deadlineNanos: Long): Boolean {
360378
if (preMountItems.isEmpty()) {
361379
// Avoid starting systrace if there are no pre mount items.
362-
return
380+
return false
363381
}
364-
365-
val deadline = lastFrameTimeNanos + FRAME_TIME_NS / 2
366-
dispatchPreMountItemsImpl(deadline)
382+
dispatchPreMountItemsImpl(deadlineNanos)
383+
return !preMountItems.isEmpty()
367384
}
368385

369386
private fun dispatchPreMountItemsImpl(deadline: Long) {
@@ -381,7 +398,7 @@ internal class MountItemDispatcher(
381398

382399
try {
383400
while (true) {
384-
if (System.nanoTime() > deadline) {
401+
if (SystemClock.nanoTime() > deadline) {
385402
break
386403
}
387404

@@ -473,8 +490,6 @@ internal class MountItemDispatcher(
473490
private companion object {
474491
private const val TAG: String = "MountItemDispatcher"
475492

476-
private const val FRAME_TIME_NS: Long = (1000000000 / 60).toLong()
477-
478493
private fun <E> drainConcurrentItemQueue(queue: Queue<E>): List<E>? {
479494
if (queue.isEmpty()) {
480495
return null

0 commit comments

Comments
 (0)