Skip to content

Commit 2f5a833

Browse files
Bartlomiej Bloniarzmeta-codesync[bot]
authored andcommitted
Defer the pull model's synchronous mount batch until the root view is attached (#58152)
Summary: Pull Request resolved: #58152 Two crashes can occur when a synchronous mount batch runs before its root view is attached. Both need the same bad state: a `ViewState` for a tag is present in `tagToViewState`, but its `view` field is null. ## Cause A surface can render before its root view is attached. While the root view is not attached, `MountItemDispatcher.executeOrEnqueue` defers every mount item into `SurfaceMountingManager.onViewAttachMountItems`. The pull model does not defer one of them: `FabricUIManager.scheduleMountItem(synchronous = true)` calls `mountItem.execute()` directly. That gives this sequence for a tag `T`: 1. **C++ claims the tag first.** `preallocateShadowView` puts `T` into `allocatedViewRegistry_`. It does this before it calls Java. 2. **Java does not create the view.** The `PreAllocateViewMountItem` for `T` is deferred, because `isWaitingForViewAttach` is true. No `ViewState` exists for `T`. 3. **C++ omits the Create instruction.** `executeMount` finds `T` in `allocatedViewTags`, so it does not add a Create for `T`. 4. **The mount batch runs too early.** The batch is not deferred, so it runs while the root view is still not attached. It has no Create for `T`, but it has an `UpdateEventEmitter`. `updateEventEmitter` calls `tagToViewState.getOrPut(T) { ViewState(T) }`, which makes a `ViewState` with a null `view`. 5. **The preallocation is cancelled.** The root view attaches and the deferred `PreAllocateViewMountItem` runs. `preallocateView` finds a `ViewState` for `T` and returns. `T` now has no view, and no Create will come. The next `updateState` or `updateOverflowInset` for `T` throws. ## Fix Apply the same attach barrier to the synchronous batch that every other mount item already obeys. If the root view is not attached, put the batch in the dispatcher queue instead of running it inline. The preallocations then run first, and the batch runs after the root view is attached. Only `pullAndExecuteTransaction` passes `synchronous = true`, so the push model never reaches this path. ## Changelog: [Android] [Fixed] - pull model mounting is now deferred until the root attaches Reviewed By: zeyap Differential Revision: D117519782 fbshipit-source-id: 5d5104eccbdd382b5be06417d6d31450ef86b697
1 parent eda9f45 commit 2f5a833

2 files changed

Lines changed: 76 additions & 3 deletions

File tree

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -971,9 +971,15 @@ private void scheduleMountItem(
971971
if (shouldSchedule) {
972972
Assertions.assertNotNull(mountItem, "MountItem is null");
973973
if (synchronous) {
974-
// Pull model: we are already on the UI thread, inside the dispatcher's loop executing
975-
// a PullTransactionMountItem. We don't schedule the item, we execute it directly.
976-
mountItem.execute(mMountingManager);
974+
if (mMountingManager.isWaitingForViewAttach(mountItem.getSurfaceId())) {
975+
// Regular mount items are still being deferred into the surface's attach queue.
976+
// Executing this batch inline would run it ahead of the preallocations queued there.
977+
mMountItemDispatcher.addMountItem(mountItem);
978+
} else {
979+
// Pull model: we are already on the UI thread, inside the dispatcher's loop executing
980+
// a PullTransactionMountItem. We don't schedule the item, we execute it directly.
981+
mountItem.execute(mMountingManager);
982+
}
977983
} else {
978984
mMountItemDispatcher.addMountItem(mountItem);
979985
if (UiThreadUtil.isOnUiThread()) {

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

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,30 @@
99

1010
package com.facebook.react.fabric
1111

12+
import com.facebook.react.ReactRootView
1213
import com.facebook.react.bridge.ReactApplicationContext
1314
import com.facebook.react.bridge.ReactTestHelper
15+
import com.facebook.react.fabric.mounting.MountingManager
16+
import com.facebook.react.fabric.mounting.mountitems.MountItem
17+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
18+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults
1419
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
20+
import com.facebook.react.uimanager.ThemedReactContext
1521
import com.facebook.react.uimanager.ViewManagerRegistry
1622
import com.facebook.testutils.fakes.FakeBatchEventDispatchedListener
1723
import com.facebook.testutils.shadows.ShadowFabricUIManagerBinding
1824
import com.facebook.testutils.shadows.ShadowNativeLoader
1925
import com.facebook.testutils.shadows.ShadowPerformanceTracer
2026
import com.facebook.testutils.shadows.ShadowSoLoader
2127
import org.assertj.core.api.Assertions.assertThat
28+
import org.junit.After
2229
import org.junit.Before
2330
import org.junit.Test
2431
import org.junit.runner.RunWith
2532
import org.robolectric.RobolectricTestRunner
2633
import org.robolectric.annotation.Config
2734
import org.robolectric.shadow.api.Shadow
35+
import org.robolectric.util.ReflectionHelpers
2836

2937
/**
3038
* Tests for the pull-model notification path: [FabricUIManager.onTransactionAvailable] enqueues a
@@ -55,6 +63,11 @@ class FabricUIManagerPullModelTest {
5563
@Before
5664
fun setup() {
5765
ReactNativeFeatureFlagsForTests.setUp()
66+
ReactNativeFeatureFlags.override(
67+
object : ReactNativeFeatureFlagsDefaults() {
68+
override fun enableMountingCoordinatorPullModelAndroid(): Boolean = true
69+
},
70+
)
5871
reactContext = ReactTestHelper.createCatalystContextForTest()
5972
underTest =
6073
FabricUIManager(
@@ -67,6 +80,11 @@ class FabricUIManagerPullModelTest {
6780
underTest.setBinding(binding)
6881
}
6982

83+
@After
84+
fun tearDown() {
85+
ReactNativeFeatureFlags.dangerouslyReset()
86+
}
87+
7088
private fun runOnBackgroundThread(block: () -> Unit) {
7189
var error: Throwable? = null
7290
val thread = Thread {
@@ -81,6 +99,28 @@ class FabricUIManagerPullModelTest {
8199
error?.let { throw it }
82100
}
83101

102+
private fun scheduleSynchronously(mountItem: MountItem) {
103+
val method =
104+
FabricUIManager::class
105+
.java
106+
.getDeclaredMethod(
107+
"scheduleMountItem",
108+
MountItem::class.java,
109+
Integer.TYPE,
110+
java.lang.Long.TYPE,
111+
java.lang.Long.TYPE,
112+
java.lang.Long.TYPE,
113+
java.lang.Long.TYPE,
114+
java.lang.Long.TYPE,
115+
java.lang.Long.TYPE,
116+
java.lang.Long.TYPE,
117+
Integer.TYPE,
118+
java.lang.Boolean.TYPE,
119+
)
120+
method.isAccessible = true
121+
method.invoke(underTest, mountItem, 0, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0, true)
122+
}
123+
84124
@Test
85125
fun onTransactionAvailable_onUiThread_pullsSynchronously() {
86126
underTest.onTransactionAvailable(1)
@@ -105,4 +145,31 @@ class FabricUIManagerPullModelTest {
105145

106146
assertThat(shadowBinding.pulledSurfaceIds).containsExactly(1, 2, 1, 3)
107147
}
148+
149+
@Test
150+
fun synchronousBatch_waitingForRootAttach_isDeferred() {
151+
val surfaceId = 12
152+
val themedReactContext = ThemedReactContext(reactContext, reactContext, "TestModule", surfaceId)
153+
val mountingManager = ReflectionHelpers.getField<MountingManager>(underTest, "mMountingManager")
154+
mountingManager.startSurface(surfaceId, themedReactContext, null)
155+
156+
var executionCount = 0
157+
val mountItem =
158+
object : MountItem {
159+
override fun execute(mountingManager: MountingManager) {
160+
executionCount++
161+
}
162+
163+
override fun getSurfaceId(): Int = surfaceId
164+
}
165+
166+
scheduleSynchronously(mountItem)
167+
168+
assertThat(executionCount).isZero()
169+
170+
mountingManager.attachRootView(surfaceId, ReactRootView(reactContext), themedReactContext)
171+
underTest.onTransactionAvailable(surfaceId)
172+
173+
assertThat(executionCount).isEqualTo(1)
174+
}
108175
}

0 commit comments

Comments
 (0)