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 @@ -58,6 +58,7 @@
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.MountItemDispatcher;
import com.facebook.react.fabric.mounting.MountingManager;
import com.facebook.react.fabric.mounting.SurfaceMountingManager;
Expand Down Expand Up @@ -182,6 +183,7 @@ public class FabricUIManager
private final ReactApplicationContext mReactApplicationContext;
private final MountingManager mMountingManager;
private final FabricEventDispatcher mEventDispatcher;
private final DisplayFrameInterval mDisplayFrameInterval;
private final MountItemDispatcher mMountItemDispatcher;
private final ViewManagerRegistry mViewManagerRegistry;

Expand Down Expand Up @@ -252,8 +254,10 @@ public FabricUIManager(
mDispatchUIFrameCallback = new DispatchUIFrameCallback(reactContext);
mReactApplicationContext = reactContext;
mMountingManager = new MountingManager(viewManagerRegistry, mMountItemExecutor);
mDisplayFrameInterval = new DisplayFrameInterval(reactContext);
mMountItemDispatcher =
new MountItemDispatcher(mMountingManager, new MountItemDispatchListener());
new MountItemDispatcher(
mMountingManager, new MountItemDispatchListener(), mDisplayFrameInterval);
mEventDispatcher = new FabricEventDispatcher(reactContext, new FabricEventEmitter(this));
mBatchEventDispatchedListener = batchEventDispatchedListener;
mReactApplicationContext.addLifecycleEventListener(this);
Expand Down Expand Up @@ -459,6 +463,7 @@ public void stopSurface(final int surfaceID) {
@Override
public void initialize() {
mEventDispatcher.addBatchEventDispatchedListener(mBatchEventDispatchedListener);
mDisplayFrameInterval.start();
if (ReactNativeFeatureFlags.enableFabricLogs()) {
mDevToolsReactPerfLogger = new DevToolsReactPerfLogger();
mDevToolsReactPerfLogger.addDevToolsReactPerfLoggerListener(FABRIC_PERF_LOGGER);
Expand Down Expand Up @@ -494,6 +499,7 @@ public void invalidate() {

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

mReactApplicationContext.unregisterComponentCallbacks(mViewManagerRegistry);
mViewManagerRegistry.invalidate();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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) :
MountItemDispatcher.FrameIntervalSource, DisplayManager.DisplayListener {
private val displayManager: DisplayManager? = context.getSystemService(DisplayManager::class.java)

// From Android 16 a listener gets refresh-rate changes only once the process has read the rate.
@Volatile private var frameIntervalNanos: Long? = readFrameIntervalNanos()

fun start() {
displayManager?.registerDisplayListener(this, UiThreadUtil.getUiThreadHandler())
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
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 @@ -29,6 +29,7 @@ import java.util.concurrent.ConcurrentLinkedQueue
internal class MountItemDispatcher(
private val mountingManager: MountingManager,
private val itemDispatchListener: ItemDispatchListener,
private val frameIntervalSource: FrameIntervalSource,
) {
private val viewCommandMountItems: Queue<DispatchCommandMountItem> = ConcurrentLinkedQueue()
private val mountItems: Queue<MountItem> = ConcurrentLinkedQueue()
Expand Down Expand Up @@ -336,10 +337,19 @@ internal class MountItemDispatcher(
return
}

val deadline = lastFrameTimeNanos + FRAME_TIME_NS / 2
val deadline = lastFrameTimeNanos + computePreMountBudgetNanos()
dispatchPreMountItemsImpl(deadline)
}

/**
* Half of the display's current frame, and never longer than [LONGEST_PRE_MOUNT_NANOS]. A display
* that reports no refresh rate is budgeted as a 60 Hz one.
*/
private fun computePreMountBudgetNanos(): Long {
val frameIntervalNanos = frameIntervalSource.frameIntervalNanos() ?: FRAME_TIME_NS
return minOf(frameIntervalNanos / 2, LONGEST_PRE_MOUNT_NANOS)
}

private fun dispatchPreMountItemsImpl(deadline: Long) {
Systrace.beginSection(Systrace.TRACE_TAG_REACT, "MountItemDispatcher::premountViews")

Expand All @@ -355,7 +365,7 @@ internal class MountItemDispatcher(

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

Expand Down Expand Up @@ -419,11 +429,22 @@ internal class MountItemDispatcher(
fun onItemsQueued()
}

fun interface FrameIntervalSource {
/** The display's current frame interval, or null while the display reports no refresh rate. */
fun frameIntervalNanos(): Long?
}

private companion object {
private const val TAG: String = "MountItemDispatcher"

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

/**
* The longest a frame pre-mounts for: half of a 30 Hz frame, so that a display below 30 Hz,
* such as an adaptive panel idling at 1 Hz, holds the UI thread no longer than at 30 Hz.
*/
private const val LONGEST_PRE_MOUNT_NANOS: Long = 1_000_000_000L / 30 / 2

private fun <E> drainConcurrentItemQueue(queue: Queue<E>): List<E>? {
if (queue.isEmpty()) {
return null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* 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.Looper
import android.view.Display
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.Shadows.shadowOf
import org.robolectric.shadows.ShadowDisplay
import org.robolectric.shadows.ShadowDisplayManager

@RunWith(RobolectricTestRunner::class)
class DisplayFrameIntervalTest {
private val display: Display = ShadowDisplay.getDefaultDisplay()

private fun setRefreshRate(refreshRate: Float) {
shadowOf(display).setRefreshRate(refreshRate)
ShadowDisplayManager.changeDisplay(Display.DEFAULT_DISPLAY, "")
shadowOf(Looper.getMainLooper()).idle()
}

@Test
fun readsTheDefaultDisplaysFrameAtEveryRefreshRate() {
val frameIntervalNanosByRefreshRate =
listOf(1f, 60f, 90f, 120f, 144f, 165f, 185f, 240f).associateWith { refreshRate ->
setRefreshRate(refreshRate)
DisplayFrameInterval(RuntimeEnvironment.getApplication()).frameIntervalNanos()
}

assertThat(frameIntervalNanosByRefreshRate)
.isEqualTo(
mapOf(
1f to 1_000_000_000L,
60f to 16_666_666L,
90f to 11_111_111L,
120f to 8_333_333L,
144f to 6_944_444L,
165f to 6_060_606L,
185f to 5_405_405L,
240f to 4_166_666L,
)
)
}

@Test
fun followsARefreshRateChangeTheDisplayReports() {
setRefreshRate(120f)
val frameInterval = DisplayFrameInterval(RuntimeEnvironment.getApplication())
frameInterval.start()

setRefreshRate(60f)

assertThat(frameInterval.frameIntervalNanos()).isEqualTo(SIXTY_HERTZ_FRAME_NANOS)
}

@Test
fun keepsItsFrameOnceStopped() {
setRefreshRate(120f)
val frameInterval = DisplayFrameInterval(RuntimeEnvironment.getApplication())
frameInterval.start()
frameInterval.stop()

setRefreshRate(60f)

assertThat(frameInterval.frameIntervalNanos()).isEqualTo(ONE_HUNDRED_TWENTY_HERTZ_FRAME_NANOS)
}

@Test
fun reportsNoFrameWhileTheDisplayReportsNoRefreshRate() {
setRefreshRate(0f)

assertThat(DisplayFrameInterval(RuntimeEnvironment.getApplication()).frameIntervalNanos())
.isNull()
}

private companion object {
const val SIXTY_HERTZ_FRAME_NANOS: Long = 1_000_000_000L / 60
const val ONE_HUNDRED_TWENTY_HERTZ_FRAME_NANOS: Long = 1_000_000_000L / 120
}
}
Loading
Loading