Skip to content

Commit 9c216cd

Browse files
[repro] Pull model: the stop transaction is never pulled, and a pull of a registered surface that is not running crashes
FabricMountingManagerInstrumentationTest gains tests that drive a real FabricUIManagerBinding: the test helper supplies a RuntimeScheduler that never runs JavaScript, a View-only component registry, and commits views to a surface's shadow tree itself. native_stopSurfaceWithSurfaceHandler_pullModel_pullsDeletes fails: the transaction that stops a surface is never pulled, so mounting override delegates (LayoutAnimations, C++ Animated) never see its Deletes. The surface leaves the binding's registry before stop(), so the UI-thread pull finds nothing. The push-model twin passes. native_registeredSurfaceThatIsNotRunning_pullModel_skipsPullAndMountReport crashes the process: pullAndExecuteTransaction and reportMount take the MountingCoordinator of any registered surface, and a SurfaceHandler that is not running has no shadow tree (SIGSEGV in ShadowTree::getMountingCoordinator()). The helper's stopSurfaceHandler stops a handler that stays in the registry, the state a registered surface has before it starts. The Kotlin helper class is internal so that it can take a SurfaceHandlerBinding.
1 parent efa6834 commit 9c216cd

4 files changed

Lines changed: 310 additions & 2 deletions

File tree

‎packages/react-native/ReactAndroid/src/main/jni/react/fabric/test_helper/FabricMountingManagerTestHelper.cpp‎

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,54 @@
77

88
#include "FabricMountingManagerTestHelper.h"
99

10+
#include <react/fabric/FabricUIManagerBinding.h>
1011
#include <react/renderer/animationbackend/AnimatedProps.h>
12+
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
13+
#include <react/renderer/components/root/RootShadowNode.h>
1114
#include <react/renderer/components/view/ViewComponentDescriptor.h>
1215
#include <react/renderer/components/view/ViewProps.h>
1316
#include <react/renderer/core/ComponentDescriptor.h>
17+
#include <react/renderer/mounting/MountingOverrideDelegate.h>
1418
#include <react/renderer/mounting/ShadowView.h>
19+
#include <react/renderer/scheduler/Scheduler.h>
1520

1621
namespace facebook::react {
1722

23+
class FabricMountingManagerTestHelper::DeleteRecorder
24+
: public MountingOverrideDelegate {
25+
public:
26+
bool shouldOverridePullTransaction() const override {
27+
return true;
28+
}
29+
30+
std::optional<MountingTransaction> pullTransaction(
31+
SurfaceId surfaceId,
32+
MountingTransaction::Number number,
33+
const TransactionTelemetry& telemetry,
34+
ShadowViewMutationList mutations) const override {
35+
for (const auto& mutation : mutations) {
36+
if (mutation.type == ShadowViewMutation::Delete) {
37+
deletedTags.push_back(mutation.oldChildShadowView.tag);
38+
}
39+
}
40+
return MountingTransaction{
41+
surfaceId, number, std::move(mutations), telemetry};
42+
}
43+
44+
mutable std::vector<Tag> deletedTags;
45+
};
46+
47+
namespace {
48+
49+
void runNoJavaScript(std::function<void(jsi::Runtime& runtime)>&& /*unused*/) {}
50+
51+
std::vector<Tag> toTags(jni::alias_ref<jni::JArrayInt> tags) {
52+
auto region = tags->getRegion(0, static_cast<jsize>(tags->size()));
53+
return {region.get(), region.get() + tags->size()};
54+
}
55+
56+
} // namespace
57+
1858
FabricMountingManagerTestHelper::FabricMountingManagerTestHelper(
1959
jni::alias_ref<JFabricUIManager::javaobject> jFabricUIManager)
2060
: javaUIManager_(jni::make_global(jFabricUIManager)) {
@@ -81,6 +121,89 @@ void FabricMountingManagerTestHelper::synchronouslyUpdateAnimatedProps(
81121
mountingManager_->synchronouslyUpdateAnimatedProps(updates);
82122
}
83123

124+
jni::local_ref<JRuntimeExecutor::jhybridobject>
125+
FabricMountingManagerTestHelper::createRuntimeExecutor() {
126+
return JRuntimeExecutor::newObjectCxxArgs(runNoJavaScript);
127+
}
128+
129+
jni::local_ref<JRuntimeScheduler::jhybridobject>
130+
FabricMountingManagerTestHelper::createRuntimeScheduler() {
131+
runtimeScheduler_ = std::make_shared<RuntimeScheduler>(runNoJavaScript);
132+
return JRuntimeScheduler::newObjectCxxArgs(runtimeScheduler_);
133+
}
134+
135+
void FabricMountingManagerTestHelper::registerViewComponent(
136+
ComponentFactory* componentFactory) {
137+
componentFactory->buildRegistryFunction =
138+
[](const EventDispatcher::Weak& eventDispatcher,
139+
const std::shared_ptr<const ContextContainer>& contextContainer) {
140+
auto providerRegistry =
141+
std::make_shared<ComponentDescriptorProviderRegistry>();
142+
providerRegistry->add(
143+
concreteComponentDescriptorProvider<ViewComponentDescriptor>());
144+
return providerRegistry->createComponentDescriptorRegistry(
145+
{.eventDispatcher = eventDispatcher,
146+
.contextContainer = contextContainer,
147+
.flavor = nullptr});
148+
};
149+
}
150+
151+
void FabricMountingManagerTestHelper::stopSurfaceHandler(
152+
jni::alias_ref<SurfaceHandlerBinding::jhybridobject>
153+
surfaceHandlerBinding) {
154+
surfaceHandlerBinding->cthis()->getSurfaceHandler().stop();
155+
}
156+
157+
void FabricMountingManagerTestHelper::commitViews(
158+
jint surfaceId,
159+
jni::alias_ref<jni::JArrayInt> tags) {
160+
auto children =
161+
std::make_shared<std::vector<std::shared_ptr<const ShadowNode>>>();
162+
for (auto tag : toTags(tags)) {
163+
auto props = std::make_shared<ViewProps>();
164+
props->collapsable = false;
165+
children->push_back(viewComponentDescriptor_->createShadowNode(
166+
{.props = props},
167+
viewComponentDescriptor_->createFamily(
168+
{.tag = tag, .surfaceId = surfaceId, .instanceHandle = nullptr})));
169+
}
170+
javaUIManager_->getBinding()
171+
->getScheduler()
172+
->getUIManager()
173+
->getShadowTreeRegistry()
174+
.visit(surfaceId, [&](const ShadowTree& shadowTree) {
175+
shadowTree.commit(
176+
[&](const RootShadowNode& oldRootShadowNode) {
177+
return std::make_shared<RootShadowNode>(
178+
oldRootShadowNode,
179+
ShadowNodeFragment{
180+
.props = ShadowNodeFragment::propsPlaceholder(),
181+
.children = children});
182+
},
183+
{});
184+
});
185+
}
186+
187+
void FabricMountingManagerTestHelper::recordPulledDeletes(jint surfaceId) {
188+
deleteRecorder_ = std::make_shared<DeleteRecorder>();
189+
javaUIManager_->getBinding()
190+
->getScheduler()
191+
->getUIManager()
192+
->getShadowTreeRegistry()
193+
.visit(surfaceId, [&](const ShadowTree& shadowTree) {
194+
shadowTree.getMountingCoordinator()->setMountingOverrideDelegate(
195+
deleteRecorder_);
196+
});
197+
}
198+
199+
jni::local_ref<jni::JArrayInt>
200+
FabricMountingManagerTestHelper::pulledDeleteTags() {
201+
const auto& tags = deleteRecorder_->deletedTags;
202+
auto array = jni::JArrayInt::newArray(tags.size());
203+
array->setRegion(0, static_cast<jsize>(tags.size()), tags.data());
204+
return array;
205+
}
206+
84207
void FabricMountingManagerTestHelper::registerNatives() {
85208
registerHybrid({
86209
makeNativeMethod(
@@ -99,6 +222,26 @@ void FabricMountingManagerTestHelper::registerNatives() {
99222
makeNativeMethod(
100223
"synchronouslyUpdateAnimatedProps",
101224
FabricMountingManagerTestHelper::synchronouslyUpdateAnimatedProps),
225+
makeNativeMethod(
226+
"createRuntimeExecutor",
227+
FabricMountingManagerTestHelper::createRuntimeExecutor),
228+
makeNativeMethod(
229+
"createRuntimeScheduler",
230+
FabricMountingManagerTestHelper::createRuntimeScheduler),
231+
makeNativeMethod(
232+
"registerViewComponent",
233+
FabricMountingManagerTestHelper::registerViewComponent),
234+
makeNativeMethod(
235+
"commitViews", FabricMountingManagerTestHelper::commitViews),
236+
makeNativeMethod(
237+
"stopSurfaceHandler",
238+
FabricMountingManagerTestHelper::stopSurfaceHandler),
239+
makeNativeMethod(
240+
"recordPulledDeletes",
241+
FabricMountingManagerTestHelper::recordPulledDeletes),
242+
makeNativeMethod(
243+
"pulledDeleteTags",
244+
FabricMountingManagerTestHelper::pulledDeleteTags),
102245
});
103246
}
104247

‎packages/react-native/ReactAndroid/src/main/jni/react/fabric/test_helper/FabricMountingManagerTestHelper.h‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@
88
#pragma once
99

1010
#include <fbjni/fbjni.h>
11+
#include <react/jni/JRuntimeExecutor.h>
12+
#include <react/jni/JRuntimeScheduler.h>
1113
#include <react/jni/NativeMap.h>
1214
#include <react/renderer/components/view/ViewComponentDescriptor.h>
1315

16+
#include <react/fabric/ComponentFactory.h>
1417
#include <react/fabric/FabricMountingManager.h>
18+
#include <react/fabric/SurfaceHandlerBinding.h>
1519

1620
namespace facebook::react {
1721

@@ -31,6 +35,18 @@ class FabricMountingManagerTestHelper : public jni::HybridClass<FabricMountingMa
3135
jni::alias_ref<jni::JArrayInt> tags,
3236
jni::alias_ref<jni::JArrayClass<NativeMap::javaobject>> props);
3337

38+
// A FabricUIManagerBinding registered on the helper's FabricUIManager with
39+
// these never runs JavaScript: its surfaces mount only what the helper
40+
// commits.
41+
jni::local_ref<JRuntimeExecutor::jhybridobject> createRuntimeExecutor();
42+
jni::local_ref<JRuntimeScheduler::jhybridobject> createRuntimeScheduler();
43+
void registerViewComponent(ComponentFactory *componentFactory);
44+
45+
void commitViews(jint surfaceId, jni::alias_ref<jni::JArrayInt> tags);
46+
void stopSurfaceHandler(jni::alias_ref<SurfaceHandlerBinding::jhybridobject> surfaceHandlerBinding);
47+
void recordPulledDeletes(jint surfaceId);
48+
jni::local_ref<jni::JArrayInt> pulledDeleteTags();
49+
3450
static void registerNatives();
3551

3652
private:
@@ -45,6 +61,9 @@ class FabricMountingManagerTestHelper : public jni::HybridClass<FabricMountingMa
4561
jni::global_ref<JFabricUIManager::javaobject> javaUIManager_;
4662
std::shared_ptr<FabricMountingManager> mountingManager_;
4763
std::unique_ptr<ViewComponentDescriptor> viewComponentDescriptor_;
64+
std::shared_ptr<RuntimeScheduler> runtimeScheduler_;
65+
class DeleteRecorder;
66+
std::shared_ptr<DeleteRecorder> deleteRecorder_;
4867
};
4968

5069
} // namespace facebook::react

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

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

88
package com.facebook.react.fabric
99

10+
import android.view.View
1011
import androidx.test.ext.junit.runners.AndroidJUnit4
1112
import androidx.test.platform.app.InstrumentationRegistry
1213
import com.facebook.react.ReactRootView
@@ -16,11 +17,15 @@ import com.facebook.react.bridge.ReactApplicationContext
1617
import com.facebook.react.bridge.ReadableType
1718
import com.facebook.react.bridge.WritableNativeArray
1819
import com.facebook.react.bridge.WritableNativeMap
20+
import com.facebook.react.fabric.events.EventBeatManager
1921
import com.facebook.react.fabric.mounting.MountingManager
2022
import com.facebook.react.fabric.mounting.MountingManager.MountItemExecutor
2123
import com.facebook.react.fabric.mounting.mountitems.IntBufferBatchMountItem
2224
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
25+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsCxxInterop
26+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults
2327
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
28+
import com.facebook.react.uimanager.DisplayMetricsHolder
2429
import com.facebook.react.uimanager.PixelUtil
2530
import com.facebook.react.uimanager.ThemedReactContext
2631
import com.facebook.react.uimanager.ViewManager
@@ -76,6 +81,7 @@ class FabricMountingManagerInstrumentationTest {
7681
@After
7782
fun tearDown() {
7883
ReactNativeFeatureFlags.dangerouslyReset()
84+
ReactNativeFeatureFlagsCxxInterop.dangerouslyReset()
7985
}
8086

8187
private fun startSurface() {
@@ -335,4 +341,121 @@ class FabricMountingManagerInstrumentationTest {
335341
assertThat(fallbackView.alpha).isEqualTo(0.5f)
336342
assertThat(fallbackView.tag).isEqualTo("animated-view")
337343
}
344+
345+
// ---- Surface lifecycle through a real FabricUIManagerBinding ----
346+
347+
/**
348+
* The pull model is tested with Props 2.0 (accumulated, exclusive and reconciled props, and props
349+
* diffed on insert); the push model with all of these flags off.
350+
*/
351+
private fun overrideCxxFlags(pullModel: Boolean) {
352+
ReactNativeFeatureFlagsCxxInterop.dangerouslyForceOverride(
353+
object : ReactNativeFeatureFlagsDefaults() {
354+
override fun enableMountingCoordinatorPullModelAndroid(): Boolean = pullModel
355+
356+
override fun enableAccumulatedUpdatesInRawPropsAndroid(): Boolean = pullModel
357+
358+
override fun enableExclusivePropsUpdateAndroid(): Boolean = pullModel
359+
360+
override fun enablePropsUpdateReconciliationAndroid(): Boolean = pullModel
361+
362+
override fun enablePreallocatedPropsDiffOnInsertAndroid(): Boolean = pullModel
363+
}
364+
)
365+
}
366+
367+
private fun registerBinding(
368+
fabricUIManager: FabricUIManager,
369+
binding: FabricUIManagerBinding = FabricUIManagerBinding(),
370+
): FabricMountingManagerTestHelper {
371+
val helper = FabricMountingManagerTestHelper.create(fabricUIManager)
372+
val componentFactory = ComponentFactory()
373+
helper.registerViewComponent(componentFactory)
374+
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(themedReactContext)
375+
binding.register(
376+
helper.createRuntimeExecutor(),
377+
helper.createRuntimeScheduler(),
378+
fabricUIManager,
379+
EventBeatManager(),
380+
componentFactory,
381+
)
382+
return helper
383+
}
384+
385+
private fun runOnUiThread(block: () -> Unit) {
386+
InstrumentationRegistry.getInstrumentation().runOnMainSync(block)
387+
}
388+
389+
private val rootMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
390+
391+
private fun startSurfaceWithHandler(fabricUIManager: FabricUIManager): SurfaceHandlerBinding {
392+
val surfaceHandler = SurfaceHandlerBinding("TestModule")
393+
surfaceHandler.setLayoutConstraints(
394+
rootMeasureSpec,
395+
rootMeasureSpec,
396+
0,
397+
0,
398+
false,
399+
false,
400+
1f,
401+
1f,
402+
)
403+
fabricUIManager.startSurface(
404+
surfaceHandler,
405+
themedReactContext,
406+
ReactRootView(themedReactContext),
407+
)
408+
return surfaceHandler
409+
}
410+
411+
/**
412+
* A restarted bridgeless surface keeps its id and is registered before it starts, so a pull or a
413+
* mount report left from its previous run can reach it before it runs. Both skip it.
414+
*/
415+
@Test
416+
fun native_registeredSurfaceThatIsNotRunning_pullModel_skipsPullAndMountReport() {
417+
overrideCxxFlags(pullModel = true)
418+
val fabricUIManager = createFabricUIManager()
419+
val binding = FabricUIManagerBinding()
420+
val helper = registerBinding(fabricUIManager, binding)
421+
runOnUiThread {
422+
val surfaceHandler = startSurfaceWithHandler(fabricUIManager)
423+
helper.stopSurfaceHandler(surfaceHandler)
424+
binding.pullAndExecuteTransaction(surfaceHandler.surfaceId)
425+
binding.reportMount(surfaceHandler.surfaceId)
426+
}
427+
}
428+
429+
@Test
430+
fun native_stopSurfaceWithSurfaceHandler_pullModel_pullsDeletes() {
431+
assertStoppingSurfacePullsDeletes(pullModel = true)
432+
}
433+
434+
@Test
435+
fun native_stopSurfaceWithSurfaceHandler_pushModel_pullsDeletes() {
436+
assertStoppingSurfacePullsDeletes(pullModel = false)
437+
}
438+
439+
/**
440+
* The transaction that stops a surface deletes its views. Mounting override delegates, such as
441+
* LayoutAnimations and C++ Animated, must see it pulled.
442+
*/
443+
private fun assertStoppingSurfacePullsDeletes(pullModel: Boolean) {
444+
overrideCxxFlags(pullModel)
445+
val fabricUIManager = createFabricUIManager()
446+
val helper = registerBinding(fabricUIManager)
447+
lateinit var surfaceHandler: SurfaceHandlerBinding
448+
var mounted = false
449+
runOnUiThread {
450+
surfaceHandler = startSurfaceWithHandler(fabricUIManager)
451+
helper.recordPulledDeletes(surfaceHandler.surfaceId)
452+
helper.commitViews(surfaceHandler.surfaceId, intArrayOf(42, 43))
453+
mounted = fabricUIManager.resolveView(42) != null
454+
}
455+
assertThat(mounted).isTrue()
456+
457+
runOnUiThread { fabricUIManager.stopSurface(surfaceHandler) }
458+
459+
assertThat(helper.pulledDeleteTags()).containsExactlyInAnyOrder(42, 43)
460+
}
338461
}

0 commit comments

Comments
 (0)