Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Point;
import android.os.Debug;
import android.os.SystemClock;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
Expand Down Expand Up @@ -58,8 +59,11 @@
import com.facebook.react.fabric.internal.interop.InteropUIBlockListener;
import com.facebook.react.fabric.interop.UIBlock;
import com.facebook.react.fabric.interop.UIBlockViewResolver;
import com.facebook.react.fabric.mounting.DisplayFrameInterval;
import com.facebook.react.fabric.mounting.FrameIntervalSources;
import com.facebook.react.fabric.mounting.MountItemDispatcher;
import com.facebook.react.fabric.mounting.MountingManager;
import com.facebook.react.fabric.mounting.PreMountItemsBetweenFrames;
import com.facebook.react.fabric.mounting.SurfaceMountingManager;
import com.facebook.react.fabric.mounting.mountitems.BatchMountItem;
import com.facebook.react.fabric.mounting.mountitems.BatchedAnimatedPropsMountItem;
Expand Down Expand Up @@ -182,6 +186,8 @@ public class FabricUIManager
private final MountingManager mMountingManager;
private final FabricEventDispatcher mEventDispatcher;
private final MountItemDispatcher mMountItemDispatcher;
private final DisplayFrameInterval mDisplayFrameInterval;
private final PreMountItemsBetweenFrames mPreMountItemsBetweenFrames;
private final ViewManagerRegistry mViewManagerRegistry;

private final TextEffectRegistry mTextEffectRegistry = new TextEffectRegistry();
Expand Down Expand Up @@ -258,6 +264,15 @@ public FabricUIManager(
mMountingManager = new MountingManager(viewManagerRegistry, mMountItemExecutor);
mMountItemDispatcher =
new MountItemDispatcher(mMountingManager, new MountItemDispatchListener());
mDisplayFrameInterval = new DisplayFrameInterval(reactContext);
mPreMountItemsBetweenFrames =
new PreMountItemsBetweenFrames(
mMountItemDispatcher,
FrameIntervalSources.create(mDisplayFrameInterval),
Debug::threadCpuTimeNanos,
mDispatchUIFrameCallback::isMountingAllowed,
UiThreadUtil.getUiThreadHandler(),
mDispatchUIFrameCallback::handleBetweenFramesException);
mEventDispatcher = new FabricEventDispatcher(reactContext, new FabricEventEmitter(this));
mBatchEventDispatchedListener = batchEventDispatchedListener;
mReactApplicationContext.addLifecycleEventListener(this);
Expand Down Expand Up @@ -463,6 +478,9 @@ public void stopSurface(final int surfaceID) {
@Override
public void initialize() {
mEventDispatcher.addBatchEventDispatchedListener(mBatchEventDispatchedListener);
if (ReactNativeFeatureFlags.enablePremountItemsBetweenFramesAndroid()) {
mDisplayFrameInterval.start();
}
if (ReactNativeFeatureFlags.enableFabricLogs()) {
mDevToolsReactPerfLogger = new DevToolsReactPerfLogger();
mDevToolsReactPerfLogger.addDevToolsReactPerfLoggerListener(FABRIC_PERF_LOGGER);
Expand Down Expand Up @@ -498,6 +516,7 @@ public void invalidate() {

mEventDispatcher.removeBatchEventDispatchedListener(mBatchEventDispatchedListener);
mEventDispatcher.invalidate();
mDisplayFrameInterval.stop();

mReactApplicationContext.unregisterComponentCallbacks(mViewManagerRegistry);
mViewManagerRegistry.invalidate();
Expand Down Expand Up @@ -1680,15 +1699,33 @@ void resume() {
schedule();
}

@UiThread
@ThreadConfined(UI)
@AnyThread
void pause() {
ReactChoreographer.getInstance()
.removeFrameCallback(ReactChoreographer.CallbackType.DISPATCH_UI, this);
// invalidate() pauses from any thread; the slices it stops belong to the UI thread.
if (UiThreadUtil.isOnUiThread()) {
mPreMountItemsBetweenFrames.cancel();
} else {
UiThreadUtil.runOnUiThread(mPreMountItemsBetweenFrames::cancel);
}
mShouldSchedule = false;
mIsScheduled = false;
}

@AnyThread
boolean isMountingAllowed() {
return mIsMountingEnabled && !mDestroyed;
}

@UiThread
@ThreadConfined(UI)
void handleBetweenFramesException(Exception exception) {
FLog.e(TAG, "Exception thrown when pre-mounting or mounting between frames", exception);
mIsMountingEnabled = false;
mReactApplicationContext.getExceptionHandler().handleException(exception);
}

@Override
@UiThread
@ThreadConfined(UI)
Expand Down Expand Up @@ -1728,13 +1765,20 @@ public void doFrameGuarded(long frameTimeNanos) {
}

try {
// First, execute as many pre mount items as we can within frameTimeNanos time.
// If not all pre mount items were executed, following may happen:
// 1. In case there are view commands or mount items in MountItemDispatcher: execute
// remaining pre mount items.
// 2. In case there are no view commands or mount items, wait until next frame.
mMountItemDispatcher.dispatchPreMountItems(frameTimeNanos);
mMountItemDispatcher.tryDispatchMountItems();
// A dispatch runs every pending pre-mount item before its pulls and mount items. What is
// left runs either here, for half of a 60 Hz frame, or after this frame's traversal until
// the next vsync, and here only when the last frame left no time after it: for as long as
// its draw slept, or for half a frame once two frames in a row were busy for a whole frame
// interval. The slices are queued once this frame's mount items have run, and run only
// while mounting is enabled, so a frame that fails to mount leaves nothing to pre-mount.
if (ReactNativeFeatureFlags.enablePremountItemsBetweenFramesAndroid()) {
mPreMountItemsBetweenFrames.preMountBeforeTraversal();
mMountItemDispatcher.tryDispatchMountItems();
mPreMountItemsBetweenFrames.scheduleAfterFrame(frameTimeNanos);
} else {
mMountItemDispatcher.dispatchPreMountItems(frameTimeNanos);
mMountItemDispatcher.tryDispatchMountItems();
}
} catch (Exception ex) {
FLog.e(TAG, "Exception thrown when executing UIFrameGuarded", ex);
mIsMountingEnabled = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.fabric.mounting

import android.content.Context
import android.hardware.display.DisplayManager
import android.view.Display
import com.facebook.react.bridge.UiThreadUtil

/**
* The default display's frame interval at its current refresh rate. It is read again when the
* display reports a change, so a frame never reads the display itself.
*/
internal class DisplayFrameInterval(context: Context) :
PreMountItemsBetweenFrames.FrameIntervalSource, DisplayManager.DisplayListener {
private val displayManager: DisplayManager? = context.getSystemService(DisplayManager::class.java)

@Volatile private var frameIntervalNanos: Long? = null

fun start() {
displayManager?.registerDisplayListener(this, UiThreadUtil.getUiThreadHandler())
// Read once the listener is registered, so a change between the two is not lost, and on the
// listener's thread, so neither store overwrites the other with an older rate. From Android 16
// a listener also gets refresh-rate changes only once the process has read the rate.
if (UiThreadUtil.isOnUiThread()) {
frameIntervalNanos = readFrameIntervalNanos()
} else {
UiThreadUtil.runOnUiThread { frameIntervalNanos = readFrameIntervalNanos() }
}
}

fun stop() {
displayManager?.unregisterDisplayListener(this)
}

override fun frameIntervalNanos(): Long? = frameIntervalNanos

override fun onDisplayChanged(displayId: Int) {
if (displayId == Display.DEFAULT_DISPLAY) {
frameIntervalNanos = readFrameIntervalNanos()
}
}

override fun onDisplayAdded(displayId: Int): Unit = Unit

override fun onDisplayRemoved(displayId: Int): Unit = Unit

private fun readFrameIntervalNanos(): Long? {
val refreshRate = displayManager?.getDisplay(Display.DEFAULT_DISPLAY)?.refreshRate ?: 0f
return if (refreshRate > 0f) (NANOSECONDS_PER_SECOND / refreshRate).toLong() else null
}

private companion object {
private const val NANOSECONDS_PER_SECOND: Double = 1_000_000_000.0
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.fabric.mounting

import android.os.Build
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi

internal object FrameIntervalSources {
/**
* The frame interval source for the running Android version: from Android 13 the vsync's frame
* timelines, with [displayFrameInterval] behind them, and before it [displayFrameInterval] alone.
*/
@JvmStatic
fun create(
displayFrameInterval: DisplayFrameInterval
): PreMountItemsBetweenFrames.FrameIntervalSource =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Api33Utils.createVsyncFrameInterval(displayFrameInterval)
} else {
displayFrameInterval
}

/** Holds the only reference to the Android 13 class, so an older Android never verifies it. */
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private object Api33Utils {
@DoNotInline
fun createVsyncFrameInterval(
displayFrameInterval: DisplayFrameInterval
): PreMountItemsBetweenFrames.FrameIntervalSource = VsyncFrameInterval(displayFrameInterval)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

package com.facebook.react.fabric.mounting

import android.os.SystemClock
import android.view.View
import androidx.annotation.UiThread
import com.facebook.common.logging.FLog
Expand All @@ -17,6 +16,7 @@ import com.facebook.react.bridge.ReactIgnorableMountingException
import com.facebook.react.bridge.ReactNoCrashSoftException
import com.facebook.react.bridge.ReactSoftExceptionLogger
import com.facebook.react.bridge.RetryableMountingLayerException
import com.facebook.react.common.SystemClock
import com.facebook.react.fabric.FabricUIManager
import com.facebook.react.fabric.mounting.mountitems.DispatchCommandMountItem
import com.facebook.react.fabric.mounting.mountitems.MountItem
Expand All @@ -27,6 +27,9 @@ import java.util.Queue
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue

/** A 60 Hz frame: half of it is a frame's pre-mount budget from its start. */
internal const val SIXTY_HERTZ_FRAME_NANOS: Long = 1_000_000_000L / 60

internal class MountItemDispatcher(
private val mountingManager: MountingManager,
private val itemDispatchListener: ItemDispatchListener,
Expand All @@ -49,6 +52,12 @@ internal class MountItemDispatcher(
/** Pull model: @return true if any surface has a transaction waiting to be pulled. */
fun hasSurfaceIdsToPull(): Boolean = surfaceIdsToPull.isNotEmpty()

/**
* @return true if any mount items or view commands are waiting to be dispatched. A dispatch also
* pulls the surfaces waiting to be pulled, before the mount items.
*/
fun hasMountItems(): Boolean = !viewCommandMountItems.isEmpty() || !mountItems.isEmpty()

// Items can be queued from any thread while the DISPATCH_UI frame callback is disarmed at
// idle (see FabricUIManager's doFrameGuarded); the listener re-arms it.
private fun notifyItemsQueued() {
Expand All @@ -64,8 +73,6 @@ internal class MountItemDispatcher(
var runStartTime: Long = 0L
private set

private var lastFrameTimeNanos: Long = 0L

fun addViewCommandMountItem(mountItem: DispatchCommandMountItem) {
if (!ReactNativeFeatureFlags.disableEarlyViewCommandExecution()) {
viewCommandMountItems.add(mountItem)
Expand Down Expand Up @@ -355,15 +362,25 @@ internal class MountItemDispatcher(
@UiThread
@ThreadConfined(UI)
fun dispatchPreMountItems(frameTimeNanos: Long) {
lastFrameTimeNanos = frameTimeNanos
dispatchPreMountItemsUntil(frameTimeNanos + SIXTY_HERTZ_FRAME_NANOS / 2)
}

fun hasPreMountItems(): Boolean = !preMountItems.isEmpty()

/**
* Executes pre-mount items until [deadlineNanos], and returns whether any are left. An item
* starts only while the deadline has not passed, so the last one can run past it by its own
* duration.
*/
@UiThread
@ThreadConfined(UI)
fun dispatchPreMountItemsUntil(deadlineNanos: Long): Boolean {
if (preMountItems.isEmpty()) {
// Avoid starting systrace if there are no pre mount items.
return
return false
}

val deadline = lastFrameTimeNanos + FRAME_TIME_NS / 2
dispatchPreMountItemsImpl(deadline)
dispatchPreMountItemsImpl(deadlineNanos)
return !preMountItems.isEmpty()
}

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

try {
while (true) {
if (System.nanoTime() > deadline) {
if (SystemClock.nanoTime() > deadline) {
break
}

Expand Down Expand Up @@ -473,8 +490,6 @@ internal class MountItemDispatcher(
private companion object {
private const val TAG: String = "MountItemDispatcher"

private const val FRAME_TIME_NS: Long = (1000000000 / 60).toLong()

private fun <E> drainConcurrentItemQueue(queue: Queue<E>): List<E>? {
if (queue.isEmpty()) {
return null
Expand Down
Loading
Loading