Skip to content

Commit 8573f91

Browse files
Pull model: test the dispatch order the pull path keeps
./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest \ --tests com.facebook.react.fabric.FabricUIManagerPullModelTest These pass on main and pin what any scheduling of pulls must keep: - pre-mount items (view preallocations) queued before the dispatch run before the pull; - a pull runs the mount hooks (UIManagerListener.didMountItems and reportMount); - with disableIdleMountItemFrameCallbackRearmAndroid, a surface that commits before its root view attaches lets the frame callback idle, the attach schedules a frame, and that frame mounts the commit after the preallocation queued for the attach. The shadow binding gains reportMount and a no-op drainPreallocateViewsQueue so the last test can run the frame callback.
1 parent a4ad15b commit 8573f91

2 files changed

Lines changed: 108 additions & 0 deletions

File tree

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

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,21 @@
99

1010
package com.facebook.react.fabric
1111

12+
import android.os.Looper
1213
import com.facebook.react.ReactRootView
1314
import com.facebook.react.bridge.ReactApplicationContext
1415
import com.facebook.react.bridge.ReactTestHelper
16+
import com.facebook.react.bridge.UIManager
17+
import com.facebook.react.bridge.UIManagerListener
18+
import com.facebook.react.common.annotations.UnstableReactNativeAPI
1519
import com.facebook.react.fabric.mounting.MountItemDispatcher
1620
import com.facebook.react.fabric.mounting.MountingManager
1721
import com.facebook.react.fabric.mounting.mountitems.MountItem
1822
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
1923
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults
2024
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
25+
import com.facebook.react.modules.core.ReactChoreographer
26+
import com.facebook.react.uimanager.GuardedFrameCallback
2127
import com.facebook.react.uimanager.ThemedReactContext
2228
import com.facebook.react.uimanager.ViewManagerRegistry
2329
import com.facebook.testutils.fakes.FakeBatchEventDispatchedListener
@@ -30,7 +36,9 @@ import org.junit.After
3036
import org.junit.Before
3137
import org.junit.Test
3238
import org.junit.runner.RunWith
39+
import org.mockito.kotlin.mock
3340
import org.robolectric.RobolectricTestRunner
41+
import org.robolectric.Shadows.shadowOf
3442
import org.robolectric.annotation.Config
3543
import org.robolectric.shadow.api.Shadow
3644
import org.robolectric.util.ReflectionHelpers
@@ -44,6 +52,7 @@ import org.robolectric.util.ReflectionHelpers
4452
* queued mount items synchronously; notifications posted from a background thread stay queued until
4553
* the next UI-thread dispatch (in production, the dispatcher's frame callback).
4654
*/
55+
@OptIn(UnstableReactNativeAPI::class)
4756
@RunWith(RobolectricTestRunner::class)
4857
@Config(
4958
shadows =
@@ -60,6 +69,7 @@ class FabricUIManagerPullModelTest {
6069
private lateinit var underTest: FabricUIManager
6170
private lateinit var binding: FabricUIManagerBinding
6271
private lateinit var shadowBinding: ShadowFabricUIManagerBinding
72+
private var didMountItemsCount = 0
6373

6474
@Before
6575
fun setup() {
@@ -79,6 +89,21 @@ class FabricUIManagerPullModelTest {
7989
binding = FabricUIManagerBinding()
8090
shadowBinding = Shadow.extract(binding)
8191
underTest.setBinding(binding)
92+
underTest.addUIManagerEventListener(
93+
object : UIManagerListener {
94+
override fun willDispatchViewUpdates(uiManager: UIManager) = Unit
95+
96+
override fun willMountItems(uiManager: UIManager) = Unit
97+
98+
override fun didMountItems(uiManager: UIManager) {
99+
didMountItemsCount++
100+
}
101+
102+
override fun didDispatchMountItems(uiManager: UIManager) = Unit
103+
104+
override fun didScheduleMountItems(uiManager: UIManager) = Unit
105+
}
106+
)
82107
}
83108

84109
@After
@@ -238,4 +263,79 @@ class FabricUIManagerPullModelTest {
238263

239264
assertThat(mountedRevisions(appliedBatches)).containsExactly(1, 2, 3)
240265
}
266+
267+
@Test
268+
fun preallocationQueuedBeforeDispatch_runsBeforeThePull() {
269+
val log = mutableListOf<String>()
270+
val surface = PulledSurface(11, log)
271+
shadowBinding.onPull = { surface.pull() }
272+
273+
surface.commit()
274+
runOnBackgroundThread {
275+
dispatcher.addPreAllocateMountItem(recordingMountItem(11, "preallocate", log))
276+
}
277+
dispatcher.tryDispatchMountItems()
278+
279+
assertThat(log).containsExactly("preallocate", "0->1")
280+
}
281+
282+
@Test
283+
fun pull_runsMountHooks() {
284+
val surface = PulledSurface(11, mutableListOf())
285+
shadowBinding.onPull = { surface.pull() }
286+
287+
surface.commit()
288+
dispatcher.tryDispatchMountItems()
289+
shadowOf(Looper.getMainLooper()).idle()
290+
291+
assertThat(didMountItemsCount).isEqualTo(1)
292+
assertThat(shadowBinding.reportedMountSurfaceIds).containsExactly(11)
293+
}
294+
295+
@Test
296+
fun pullWaitingForRootAttach_isMountedByTheFrameAfterTheAttach() {
297+
// With the idle frame callback, nothing may keep frames running while the root is detached, so
298+
// the attach must schedule the frame that mounts the surface.
299+
ReactNativeFeatureFlags.dangerouslyReset()
300+
ReactNativeFeatureFlags.override(
301+
object : ReactNativeFeatureFlagsDefaults() {
302+
override fun enableMountingCoordinatorPullModelAndroid(): Boolean = true
303+
304+
override fun disableIdleMountItemFrameCallbackRearmAndroid(): Boolean = true
305+
},
306+
)
307+
val originalChoreographer = ReactChoreographer.overrideInstanceForTest(mock())
308+
try {
309+
val frameCallback: GuardedFrameCallback =
310+
ReflectionHelpers.getField(underTest, "mDispatchUIFrameCallback")
311+
ReflectionHelpers.setField(frameCallback, "mShouldSchedule", true)
312+
fun isFrameScheduled(): Boolean = ReflectionHelpers.getField(frameCallback, "mIsScheduled")
313+
val surfaceId = 12
314+
val themedReactContext =
315+
ThemedReactContext(reactContext, reactContext, "TestModule", surfaceId)
316+
mountingManager.startSurface(surfaceId, themedReactContext, null)
317+
val log = mutableListOf<String>()
318+
val surface = PulledSurface(surfaceId, log)
319+
shadowBinding.onPull = { surface.pull() }
320+
321+
surface.commit()
322+
runOnBackgroundThread {
323+
dispatcher.addPreAllocateMountItem(recordingMountItem(surfaceId, "preallocate", log))
324+
}
325+
frameCallback.doFrame(System.nanoTime())
326+
327+
assertThat(log).isEmpty()
328+
assertThat(isFrameScheduled()).isFalse()
329+
330+
mountingManager.attachRootView(surfaceId, ReactRootView(reactContext), themedReactContext)
331+
332+
assertThat(isFrameScheduled()).isTrue()
333+
334+
frameCallback.doFrame(System.nanoTime())
335+
336+
assertThat(log).containsExactly("preallocate", "0->1")
337+
} finally {
338+
ReactChoreographer.overrideInstanceForTest(originalChoreographer)
339+
}
340+
}
241341
}

‎packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows/ShadowFabricUIManagerBinding.kt‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import org.robolectric.annotation.Implements
1818
@Implements(className = "com.facebook.react.fabric.FabricUIManagerBinding")
1919
class ShadowFabricUIManagerBinding {
2020
val pulledSurfaceIds: MutableList<Int> = mutableListOf()
21+
val reportedMountSurfaceIds: MutableList<Int> = mutableListOf()
2122
/** Stands in for the C++ pull and mount. */
2223
var onPull: (surfaceId: Int) -> Unit = {}
2324

@@ -26,4 +27,11 @@ class ShadowFabricUIManagerBinding {
2627
pulledSurfaceIds.add(surfaceId)
2728
onPull(surfaceId)
2829
}
30+
31+
@Implementation
32+
fun reportMount(surfaceId: Int) {
33+
reportedMountSurfaceIds.add(surfaceId)
34+
}
35+
36+
@Implementation fun drainPreallocateViewsQueue() = Unit
2937
}

0 commit comments

Comments
 (0)