diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/DeferredViewCleanupQueue.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/DeferredViewCleanupQueue.kt new file mode 100644 index 000000000000..abb77f003958 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/DeferredViewCleanupQueue.kt @@ -0,0 +1,193 @@ +/* + * 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.Handler +import android.os.Looper +import android.view.View +import android.view.ViewTreeObserver +import com.facebook.react.bridge.UiThreadUtil +import com.facebook.systrace.Systrace +import java.util.ArrayDeque + +/** Experimental UI-thread cleanup, gated on a draw rather than ReactChoreographer.IDLE_EVENT. */ +internal class DeferredViewCleanupQueue { + private val handler = Handler(Looper.getMainLooper()) + + private class Task( + val afterDraw: Long, + val estimatedOperations: Int, + val step: () -> Boolean, + ) { + val traceId = nextTaskId++ + } + + internal companion object { + // Queue operations are confined to the UI thread, including task ID allocation. + private var nextTaskId = 0 + const val MAX_PENDING_OPERATIONS = 2048 + const val MAX_PENDING_TASKS = 8 + } + + private val tasks = ArrayDeque() + private var pendingOperations = 0 + private var drawGeneration = 0L + private var root: View? = null + private var observer: ViewTreeObserver? = null + private var posted = false + + internal val size: Int + get() = tasks.size + + internal fun hasCapacityFor(estimatedOperations: Int): Boolean = + tasks.size < MAX_PENDING_TASKS && + estimatedOperations > 0 && + estimatedOperations <= MAX_PENDING_OPERATIONS && + pendingOperations <= MAX_PENDING_OPERATIONS - estimatedOperations + + private val afterDraw: Runnable = Runnable { + removeDrawListener() + handler.removeCallbacks(checkVisibility) + posted = false + drainSlice() + } + + private val drawListener = + ViewTreeObserver.OnDrawListener { + drawGeneration++ + if (!posted) { + posted = true + // onDraw runs inside traversal. Posting moves cleanup outside that traversal; this does + // not imply that the GPU has finished presenting the frame. + handler.post(afterDraw) + } + } + + private val checkVisibility = + object : Runnable { + override fun run() { + val view = root ?: return + if (!view.isAttachedToWindow || !view.isShown || view.windowVisibility != View.VISIBLE) { + handler.removeCallbacks(afterDraw) + removeDrawListener() + posted = false + drainSlice(requireDraw = false) + } else { + // A detach/reattach can replace (and kill) the observer while work is pending. + if (observer?.isAlive != true && !posted) { + removeDrawListener() + observer = view.viewTreeObserver.also { it.addOnDrawListener(drawListener) } + } + // A visible surface must cross a draw boundary. Do not replace that guarantee with a + // timeout on a slow frame. Hidden/detached surfaces have no frame to protect. + view.postInvalidateOnAnimation() + handler.postDelayed(this, 100) + } + } + } + + /** Each invocation performs one bounded unit and returns true once its task is complete. */ + internal fun enqueue(view: View, estimatedOperations: Int = 1, step: () -> Boolean) { + UiThreadUtil.assertOnUiThread() + check(hasCapacityFor(estimatedOperations)) { + "Deferred cleanup operation budget exceeded: $estimatedOperations" + } + val task = Task(drawGeneration + 1, estimatedOperations, step) + tasks.addLast(task) + pendingOperations += estimatedOperations + tracePendingWork() + Systrace.beginAsyncSection( + Systrace.TRACE_TAG_REACT, + "FabricDeferredCleanup::task", + task.traceId, + ) + if (root == null) { + root = view + awaitDraw() + } + } + + internal fun flush() { + UiThreadUtil.assertOnUiThread() + Systrace.beginSection(Systrace.TRACE_TAG_REACT, "FabricDeferredCleanup::flush") + try { + handler.removeCallbacks(afterDraw) + handler.removeCallbacks(checkVisibility) + removeDrawListener() + posted = false + while (tasks.isNotEmpty()) { + val task = tasks.first + if (task.step() && tasks.peekFirst() === task) complete(task) + } + root = null + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT) + } + } + + private fun awaitDraw() { + val view = checkNotNull(root) + observer = view.viewTreeObserver.also { it.addOnDrawListener(drawListener) } + view.postInvalidateOnAnimation() + handler.postDelayed(checkVisibility, 100) + } + + private fun complete(task: Task) { + tasks.removeFirst() + pendingOperations -= task.estimatedOperations + tracePendingWork() + Systrace.endAsyncSection(Systrace.TRACE_TAG_REACT, "FabricDeferredCleanup::task", task.traceId) + } + + private fun tracePendingWork() { + Systrace.traceCounter( + Systrace.TRACE_TAG_REACT, + "FabricDeferredCleanup::pendingTasks", + tasks.size, + ) + // Reservations stay live until the task releases its captured view states. They are not a + // count of unfinished steps; production unmount plans reserve two operations per state. + Systrace.traceCounter( + Systrace.TRACE_TAG_REACT, + "FabricDeferredCleanup::reservedOperations", + pendingOperations, + ) + } + + private fun removeDrawListener() { + observer?.let { if (it.isAlive) it.removeOnDrawListener(drawListener) } + // Android migrates listeners from a dead observer to the new window's observer. + root?.viewTreeObserver?.let { + if (it !== observer && it.isAlive) it.removeOnDrawListener(drawListener) + } + observer = null + } + + private fun drainSlice(requireDraw: Boolean = true) { + Systrace.beginSection(Systrace.TRACE_TAG_REACT, "FabricDeferredCleanup::slice") + var operations = 0 + try { + val deadline = System.nanoTime() + 1_000_000L + do { + val task = tasks.peekFirst() ?: break + if (requireDraw && task.afterDraw > drawGeneration) break + // Detachment listeners can synchronously stop the surface and flush this queue. + if (task.step() && tasks.peekFirst() === task) complete(task) + operations++ + } while (operations < 256 && System.nanoTime() < deadline) + } finally { + Systrace.traceCounter( + Systrace.TRACE_TAG_REACT, + "FabricDeferredCleanup::operations", + operations, + ) + Systrace.endSection(Systrace.TRACE_TAG_REACT) + } + if (tasks.isEmpty()) root = null else awaitDraw() + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt index 0c8c7bf05502..938777b5c557 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt @@ -51,6 +51,8 @@ import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ViewManager import com.facebook.react.uimanager.ViewManagerRegistry import com.facebook.react.uimanager.events.EventCategoryDef +import com.facebook.react.views.view.ReactViewGroup +import com.facebook.react.views.view.ReactViewManager import com.facebook.systrace.Systrace import java.util.ArrayDeque import java.util.LinkedList @@ -90,6 +92,7 @@ internal constructor( private val registryLock = ReentrantReadWriteLock() private val onViewAttachMountItems: Queue = ArrayDeque() + private var deferredViewCleanup: DeferredViewCleanupQueue? = null // These are all non-null, until StopSurface is called private var jsResponderHandler: JSResponderHandler? = jsResponderHandler @@ -256,6 +259,9 @@ internal constructor( if (ReactNativeFeatureFlags.enableViewRecycling()) { viewManagerRegistry?.onSurfaceStopped(surfaceId) } + // Retired views no longer live in tagToViewState. Evict pools first so flushing them does + // not prepare views for recycling on a surface that has already stopped. + deferredViewCleanup?.flush() // Using this as a placeholder value in the map. We're using SparseArrayCompat // since it can efficiently represent the list of pending tags @@ -395,6 +401,207 @@ internal constructor( } } + @UiThread + internal fun tryDeferPureViewUnmount(removes: List, deletes: List): Boolean { + UiThreadUtil.assertOnUiThread() + val plan: DeferredUnmount? + Systrace.beginSection(Systrace.TRACE_TAG_REACT, "FabricDeferredUnmount::validate") + try { + plan = validateDeferredUnmount(removes, deletes) + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT) + } + if (plan == null) return false + + Systrace.beginSection(Systrace.TRACE_TAG_REACT, "FabricDeferredUnmount::detachRoots") + try { + for ((tag, parent, index) in plan.roots) removeViewAt(tag, parent, index) + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT) + } + if (isStopped) return true + Systrace.beginSection(Systrace.TRACE_TAG_REACT, "FabricDeferredUnmount::retireReferences") + try { + registryLock.write { + for (state in plan.states) { + tagToViewState.remove(state.reactTag) + tagToSynchronousMountProps.remove(state.reactTag) + // Never retain JSI pointers until a later draw or VM shutdown. + state.stateWrapper?.destroyState() + state.stateWrapper = null + state.eventEmitter?.destroy() + state.eventEmitter = null + } + } + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT) + } + var removeIndex = 0 + var deleteIndex = 0 + val queue = deferredViewCleanup ?: DeferredViewCleanupQueue().also { deferredViewCleanup = it } + queue.enqueue(plan.root, plan.states.size * 2) { + if (removeIndex < plan.childParents.size) { + val parent = plan.childParents[removeIndex] + val index = plan.childIndices[removeIndex++] + // Object references belong to retired states, never look up a potentially reused tag. + getViewGroupManager(parent.state).removeViewAt(parent.view, index) + } else { + onViewStateDeleted(plan.states[deleteIndex++]) + } + removeIndex == plan.childParents.size && deleteIndex == plan.states.size + } + Systrace.beginSection(Systrace.TRACE_TAG_REACT, "FabricDeferredUnmount::accepted") + Systrace.traceCounter( + Systrace.TRACE_TAG_REACT, + "FabricDeferredUnmount::states", + plan.states.size, + ) + Systrace.endSection(Systrace.TRACE_TAG_REACT) + return true + } + + private class DeferredUnmount( + val root: View, + val states: List, + val roots: List, + val childParents: List, + val childIndices: IntArray, + ) + + private class DeferredUnmountParent(val state: ViewState, val view: ViewGroup) { + var lastIndex: Int = Int.MAX_VALUE + } + + private fun rejectDeferredUnmount(reason: String): DeferredUnmount? { + Systrace.beginSection(Systrace.TRACE_TAG_REACT, reason) + Systrace.endSection(Systrace.TRACE_TAG_REACT) + return null + } + + private fun validateDeferredUnmount( + removes: List, + deletes: List, + ): DeferredUnmount? { + if (isStopped) return rejectDeferredUnmount("FabricDeferredUnmount::rejectSurface") + if (deletes.size !in 2..4096) { + return rejectDeferredUnmount("FabricDeferredUnmount::rejectSize") + } + if ( + deletes.size > DeferredViewCleanupQueue.MAX_PENDING_OPERATIONS / 2 || + deferredViewCleanup?.hasCapacityFor(deletes.size * 2) == false + ) { + return rejectDeferredUnmount("FabricDeferredUnmount::rejectCapacity") + } + val root = + getNullableViewState(surfaceId)?.view + ?: return rejectDeferredUnmount("FabricDeferredUnmount::rejectSurface") + if (!root.isAttachedToWindow) + return rejectDeferredUnmount("FabricDeferredUnmount::rejectSurface") + if (removes.size != deletes.size) + return rejectDeferredUnmount("FabricDeferredUnmount::rejectMembership") + // One index handles membership, duplicate DELETEs, and state lookup. + val stateIndices = HashMap(deletes.size) + for (index in deletes.indices) { + if (stateIndices.put(deletes[index], index) != null) + return rejectDeferredUnmount("FabricDeferredUnmount::rejectMembership") + } + val removed = BooleanArray(deletes.size) + val removeStateIndices = IntArray(removes.size) + for (index in removes.indices) { + val remove = removes[index] + if (remove.size != 3) return rejectDeferredUnmount("FabricDeferredUnmount::rejectMembership") + val stateIndex = stateIndices.getOrDefault(remove[0], -1) + if (stateIndex < 0 || removed[stateIndex]) + return rejectDeferredUnmount("FabricDeferredUnmount::rejectMembership") + removed[stateIndex] = true + removeStateIndices[index] = stateIndex + } + + val states = ArrayList(deletes.size) + for (tag in deletes) { + val state = + getNullableViewState(tag) + ?: return rejectDeferredUnmount("FabricDeferredUnmount::rejectMembership") + val view = + state.view as? ReactViewGroup + ?: return rejectDeferredUnmount("FabricDeferredUnmount::rejectView") + // An exact class check is deliberate: arbitrary manager callbacks may observe attachment. + if ( + state.isRoot || + state.viewManager?.javaClass != ReactViewManager::class.java || + view.javaClass != ReactViewGroup::class.java || + view.removeClippedSubviews || + view.animation != null || + view.hasTransientState() || + view.layoutTransition != null || + tag in viewsWithActiveTouches || + tag in erroneouslyReaddedReactTags + ) + return rejectDeferredUnmount("FabricDeferredUnmount::rejectView") + states.add(state) + } + for (state in states) { + val view = state.view as ReactViewGroup + for (index in 0 until view.childCount) { + val child = view.getChildAt(index) + val childIndex = stateIndices.getOrDefault(child.id, -1) + if (childIndex < 0 || states[childIndex].view !== child) + return rejectDeferredUnmount("FabricDeferredUnmount::rejectChildren") + } + } + + val parents = MutableIntObjectMap() + val roots = ArrayList() + val childParents = ArrayList(removes.size) + val childIndices = IntArray(removes.size) + for (removeIndex in removes.indices) { + val remove = removes[removeIndex] + val parentTag = remove[1] + val index = remove[2] + val stateIndex = removeStateIndices[removeIndex] + var parent = parents[parentTag] + if (parent == null) { + val parentIndex = stateIndices.getOrDefault(parentTag, -1) + val parentState = + if (parentIndex >= 0) states[parentIndex] + else + getNullableViewState(parentTag) + ?: return rejectDeferredUnmount("FabricDeferredUnmount::rejectParent") + val parentView = + parentState.view as? ViewGroup + ?: return rejectDeferredUnmount("FabricDeferredUnmount::rejectParent") + if ( + (!parentState.isRoot && + (parentState.viewManager?.javaClass != ReactViewManager::class.java || + (parentView as? ReactViewGroup)?.removeClippedSubviews != false)) || + parentView.layoutTransition != null || + parentView.animation != null || + parentView.hasTransientState() + ) + return rejectDeferredUnmount("FabricDeferredUnmount::rejectParent") + parent = DeferredUnmountParent(parentState, parentView) + parents[parentTag] = parent + } + if ( + index < 0 || + index >= parent.lastIndex || + parent.view.getChildAt(index) !== states[stateIndex].view + ) + return rejectDeferredUnmount("FabricDeferredUnmount::rejectIndices") + parent.lastIndex = index + if (!stateIndices.containsKey(parentTag)) { + roots.add(remove) + } else { + childIndices[childParents.size] = index + childParents.add(parent) + } + } + if (roots.isEmpty() || childParents.isEmpty()) + return rejectDeferredUnmount("FabricDeferredUnmount::rejectNoSubtree") + // Validation is read-only. All unsupported batches take the original path unchanged. + return DeferredUnmount(root, states, roots, childParents, childIndices) + } + @UiThread public fun removeViewAt(tag: Int, parentTag: Int, index: Int) { UiThreadUtil.assertOnUiThread() diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/IntBufferBatchMountItem.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/IntBufferBatchMountItem.kt index c4026f380688..aec1f950d478 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/IntBufferBatchMountItem.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/IntBufferBatchMountItem.kt @@ -84,6 +84,14 @@ internal class IntBufferBatchMountItem( } beginMarkers("mountViews") + if ( + ReactNativeFeatureFlags.enableDeferredViewCleanupAndroid() && + !ReactNativeFeatureFlags.disableMountItemReorderingAndroid() && + tryDeferPureViewUnmount(surfaceMountingManager) + ) { + endMarkers() + return + } var i = 0 var j = 0 while (i < intBufferLen) { @@ -350,6 +358,64 @@ internal class IntBufferBatchMountItem( } } + // Prototype signal at the Android boundary. Accept only a whole REMOVE-then-DELETE batch; + // mixed transactions, reinsertion, and animation instructions keep their existing semantics. + private fun tryDeferPureViewUnmount( + surface: com.facebook.react.fabric.mounting.SurfaceMountingManager, + ): Boolean { + Systrace.beginSection(Systrace.TRACE_TAG_REACT, "FabricDeferredUnmount::parse") + val parsed: Pair, List>? + try { + parsed = parsePureViewUnmount() + } finally { + Systrace.endSection(Systrace.TRACE_TAG_REACT) + } + return parsed != null && surface.tryDeferPureViewUnmount(parsed.first, parsed.second) + } + + private fun rejectPureViewUnmount(reason: String): Pair, List>? { + Systrace.beginSection(Systrace.TRACE_TAG_REACT, reason) + Systrace.endSection(Systrace.TRACE_TAG_REACT) + return null + } + + private fun parsePureViewUnmount(): Pair, List>? { + if (objBufferLen != 0) return rejectPureViewUnmount("FabricDeferredUnmount::rejectObjectBuffer") + val removes = ArrayList() + val deletes = ArrayList() + var deleting = false + var index = 0 + while (index < intBufferLen) { + val rawType = intBuffer[index++] + val type = rawType and INSTRUCTION_FLAG_MULTIPLE.inv() + val count = + if (rawType and INSTRUCTION_FLAG_MULTIPLE != 0) { + if (index >= intBufferLen) + return rejectPureViewUnmount("FabricDeferredUnmount::rejectMalformed") + intBuffer[index++] + } else 1 + if (count <= 0 || count > 4096) + return rejectPureViewUnmount("FabricDeferredUnmount::rejectCount") + when (type) { + INSTRUCTION_REMOVE -> { + if (deleting || count > (intBufferLen - index) / 3 || removes.size + count > 4096) + return rejectPureViewUnmount("FabricDeferredUnmount::rejectRemovePhase") + repeat(count) { + removes.add(intArrayOf(intBuffer[index++], intBuffer[index++], intBuffer[index++])) + } + } + INSTRUCTION_DELETE -> { + deleting = true + if (count > intBufferLen - index || deletes.size + count > 4096) + return rejectPureViewUnmount("FabricDeferredUnmount::rejectCount") + repeat(count) { deletes.add(intBuffer[index++]) } + } + else -> return rejectPureViewUnmount("FabricDeferredUnmount::rejectMixedInstruction") + } + } + return removes to deletes + } + companion object { const val INSTRUCTION_FLAG_MULTIPLE: Int = 1 diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 7fcdaaeec16d..3d507a63e0a1 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<14dfef8b603befedcad6aa4d9cab2a6f>> */ /** @@ -156,6 +156,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun enableCustomFocusSearchOnClippedElementsAndroid(): Boolean = accessor.enableCustomFocusSearchOnClippedElementsAndroid() + /** + * Experiment with retiring pure RCTView subtrees immediately and cleaning native descendants in bounded slices after drawing on Android + */ + @JvmStatic + public fun enableDeferredViewCleanupAndroid(): Boolean = accessor.enableDeferredViewCleanupAndroid() + /** * Enables destructor calls for ShadowTreeRevision in the background to reduce UI thread work. */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index 6e0da4d9d0e4..4c929c07216d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<039bc767886a7cef3e32a4cfad6a9c4d>> */ /** @@ -41,6 +41,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var enableBridgelessArchitectureCache: Boolean? = null private var enableCppPropsIteratorSetterCache: Boolean? = null private var enableCustomFocusSearchOnClippedElementsAndroidCache: Boolean? = null + private var enableDeferredViewCleanupAndroidCache: Boolean? = null private var enableDestroyShadowTreeRevisionAsyncCache: Boolean? = null private var enableDoubleMeasurementFixAndroidCache: Boolean? = null private var enableEagerRootViewAttachmentCache: Boolean? = null @@ -307,6 +308,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun enableDeferredViewCleanupAndroid(): Boolean { + var cached = enableDeferredViewCleanupAndroidCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.enableDeferredViewCleanupAndroid() + enableDeferredViewCleanupAndroidCache = cached + } + return cached + } + override fun enableDestroyShadowTreeRevisionAsync(): Boolean { var cached = enableDestroyShadowTreeRevisionAsyncCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index 2268d03f3674..ed0ce405913d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<6d86d4adbc261c9271b5bed72bd12dcf>> + * @generated SignedSource<<01b1f494efa669cfdf5af67e49074fcf>> */ /** @@ -70,6 +70,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun enableCustomFocusSearchOnClippedElementsAndroid(): Boolean + @DoNotStrip @JvmStatic public external fun enableDeferredViewCleanupAndroid(): Boolean + @DoNotStrip @JvmStatic public external fun enableDestroyShadowTreeRevisionAsync(): Boolean @DoNotStrip @JvmStatic public external fun enableDoubleMeasurementFixAndroid(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index defafe46f6b4..98e4b1ab3a07 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -65,6 +65,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun enableCustomFocusSearchOnClippedElementsAndroid(): Boolean = true + override fun enableDeferredViewCleanupAndroid(): Boolean = false + override fun enableDestroyShadowTreeRevisionAsync(): Boolean = false override fun enableDoubleMeasurementFixAndroid(): Boolean = false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index 53af4b4edab0..e962b42de4c2 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<8409217c2846a6a49b770ed56ac81a49>> */ /** @@ -45,6 +45,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var enableBridgelessArchitectureCache: Boolean? = null private var enableCppPropsIteratorSetterCache: Boolean? = null private var enableCustomFocusSearchOnClippedElementsAndroidCache: Boolean? = null + private var enableDeferredViewCleanupAndroidCache: Boolean? = null private var enableDestroyShadowTreeRevisionAsyncCache: Boolean? = null private var enableDoubleMeasurementFixAndroidCache: Boolean? = null private var enableEagerRootViewAttachmentCache: Boolean? = null @@ -332,6 +333,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun enableDeferredViewCleanupAndroid(): Boolean { + var cached = enableDeferredViewCleanupAndroidCache + if (cached == null) { + cached = currentProvider.enableDeferredViewCleanupAndroid() + accessedFeatureFlags.add("enableDeferredViewCleanupAndroid") + enableDeferredViewCleanupAndroidCache = cached + } + return cached + } + override fun enableDestroyShadowTreeRevisionAsync(): Boolean { var cached = enableDestroyShadowTreeRevisionAsyncCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index 8fa2f4269464..af250e14e8db 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -65,6 +65,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun enableCustomFocusSearchOnClippedElementsAndroid(): Boolean + @DoNotStrip public fun enableDeferredViewCleanupAndroid(): Boolean + @DoNotStrip public fun enableDestroyShadowTreeRevisionAsync(): Boolean @DoNotStrip public fun enableDoubleMeasurementFixAndroid(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index 23fd0978a766..61865a8a4b2e 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<944781e7a547e601f0637869a3a3ec5e>> + * @generated SignedSource<<46cd2c8c31b559149f3126fd9f37551f>> */ /** @@ -165,6 +165,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool enableDeferredViewCleanupAndroid() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("enableDeferredViewCleanupAndroid"); + return method(javaProvider_); + } + bool enableDestroyShadowTreeRevisionAsync() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("enableDestroyShadowTreeRevisionAsync"); @@ -730,6 +736,11 @@ bool JReactNativeFeatureFlagsCxxInterop::enableCustomFocusSearchOnClippedElement return ReactNativeFeatureFlags::enableCustomFocusSearchOnClippedElementsAndroid(); } +bool JReactNativeFeatureFlagsCxxInterop::enableDeferredViewCleanupAndroid( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::enableDeferredViewCleanupAndroid(); +} + bool JReactNativeFeatureFlagsCxxInterop::enableDestroyShadowTreeRevisionAsync( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::enableDestroyShadowTreeRevisionAsync(); @@ -1204,6 +1215,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "enableCustomFocusSearchOnClippedElementsAndroid", JReactNativeFeatureFlagsCxxInterop::enableCustomFocusSearchOnClippedElementsAndroid), + makeNativeMethod( + "enableDeferredViewCleanupAndroid", + JReactNativeFeatureFlagsCxxInterop::enableDeferredViewCleanupAndroid), makeNativeMethod( "enableDestroyShadowTreeRevisionAsync", JReactNativeFeatureFlagsCxxInterop::enableDestroyShadowTreeRevisionAsync), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index ae774f80c82b..6c1f7435c614 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -93,6 +93,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool enableCustomFocusSearchOnClippedElementsAndroid( facebook::jni::alias_ref); + static bool enableDeferredViewCleanupAndroid( + facebook::jni::alias_ref); + static bool enableDestroyShadowTreeRevisionAsync( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/DeferredViewCleanupTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/DeferredViewCleanupTest.kt new file mode 100644 index 000000000000..79057735560e --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/DeferredViewCleanupTest.kt @@ -0,0 +1,741 @@ +/* + * 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 + +import android.app.Activity +import android.os.Looper +import android.view.View +import android.view.ViewGroup +import android.view.animation.AlphaAnimation +import android.widget.FrameLayout +import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.bridge.ReactTestHelper +import com.facebook.react.fabric.events.EventEmitterWrapper +import com.facebook.react.fabric.mounting.DeferredViewCleanupQueue +import com.facebook.react.fabric.mounting.MountingManager +import com.facebook.react.fabric.mounting.MountingManager.MountItemExecutor +import com.facebook.react.fabric.mounting.SurfaceMountingManager +import com.facebook.react.fabric.mounting.mountitems.IntBufferBatchMountItem +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests +import com.facebook.react.uimanager.StateWrapper +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.ViewManagerRegistry +import com.facebook.react.views.view.ReactViewGroup +import com.facebook.react.views.view.ReactViewManager +import com.facebook.testutils.shadows.ShadowNativeLoader +import com.facebook.testutils.shadows.ShadowSoLoader +import java.time.Duration +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mockingDetails +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.inOrder +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.spy +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode + +@RunWith(RobolectricTestRunner::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(shadows = [ShadowSoLoader::class, ShadowNativeLoader::class]) +class DeferredViewCleanupTest { + private lateinit var root: FrameLayout + private lateinit var surface: SurfaceMountingManager + private lateinit var manager: MountingManager + private lateinit var context: ThemedReactContext + private lateinit var viewManager: ReactViewManager + + @Before + fun setUp() { + ReactNativeFeatureFlagsForTests.setUp() + configure(true) + val activity = Robolectric.buildActivity(Activity::class.java).setup().get() + root = FrameLayout(activity) + activity.setContentView(root) + val reactContext = ReactTestHelper.createTestReactApplicationContext(activity.application) + context = ThemedReactContext(reactContext, activity, null, 1) + com.facebook.react.uimanager.DisplayMetricsHolder.initDisplayMetrics(context) + viewManager = ReactViewManager() + manager = MountingManager(ViewManagerRegistry(listOf(viewManager)), MountItemExecutor {}) + surface = manager.startSurface(1, context, root) + } + + private fun configure( + enabled: Boolean, + disableReordering: Boolean = false, + recycling: Boolean = false, + ) { + ReactNativeFeatureFlags.dangerouslyReset() + ReactNativeFeatureFlags.override( + object : ReactNativeFeatureFlagsDefaults() { + override fun enableDeferredViewCleanupAndroid() = enabled + + override fun disableMountItemReorderingAndroid() = disableReordering + + override fun enableViewRecycling() = recycling + + override fun enableViewRecyclingForView() = recycling + } + ) + } + + @After + fun tearDown() { + surface.stopSurface() + ReactNativeFeatureFlags.dangerouslyReset() + } + + private fun mount( + tag: Int, + parent: Int, + index: Int = 0, + state: StateWrapper? = null, + ): ReactViewGroup { + surface.preallocateView("RCTView", tag, JavaOnlyMap.of(), state, true) + surface.addViewAt(parent, tag, index) + return surface.getView(tag) as ReactViewGroup + } + + private fun unmount() { + IntBufferBatchMountItem(1, intArrayOf(17, 2, 4, 2, 0, 2, 1, 0, 5, 2, 4, 2), emptyArray(), 0) + .execute(manager) + } + + private fun drawAndDrain() { + // Explicit traversal simulation; the device probe separately checks actual Android drawing. + root.viewTreeObserver.dispatchOnDraw() + shadowOf(Looper.getMainLooper()).idle() + } + + @Test + fun retiresTagsAndWrappersImmediately_butKeepsDescendantsUntilAfterDraw() { + val panel = mount(2, 1) + val state: StateWrapper = mock() + val child = mount(4, 2, state = state) + val emitter: EventEmitterWrapper = mock() + surface.updateEventEmitter(4, emitter) + unmount() + assertThat(root.childCount).isZero() + assertThat(child.parent).isSameAs(panel) + assertThat(surface.getViewExists(2)).isFalse() + assertThat(surface.getViewExists(4)).isFalse() + verify(state).destroyState() + verify(emitter).destroy() + drawAndDrain() + assertThat(child.parent).isNull() + surface.stopSurface() + verify(state, times(1)).destroyState() + verify(emitter, times(1)).destroy() + } + + @Test + fun recreatedTagsAreNotTouchedByOldCleanup() { + val oldPanel = mount(2, 1) + val oldChild = mount(4, 2) + unmount() + val panel = mount(2, 1) + val child = mount(4, 2) + assertThat(panel).isNotSameAs(oldPanel) + assertThat(child).isNotSameAs(oldChild) + drawAndDrain() + assertThat(child.parent).isSameAs(panel) + assertThat(panel.parent).isSameAs(root) + assertThat(oldChild.parent).isNull() + } + + @Test + fun stoppingSurfaceDrainsRetiredViewsWithoutWaitingForDraw() { + mount(2, 1) + val child = mount(4, 2) + unmount() + surface.stopSurface() + assertThat(child.parent).isNull() + drawAndDrain() + assertThat(child.parent).isNull() + } + + @Test + fun stoppingSurfaceEvictsPoolBeforeDroppingPendingViews() { + startRecyclingSurface() + val panel = mount(2, 1) + val child = mount(4, 2) + unmount() + assertThat(child.parent).isSameAs(panel) + verify(viewManager, never()).onDropViewInstance(any()) + + surface.stopSurface() + + val ordered = inOrder(viewManager) + ordered.verify(viewManager).onSurfaceStopped(1) + ordered.verify(viewManager).onDropViewInstance(child) + ordered.verify(viewManager).onDropViewInstance(panel) + assertThat( + mockingDetails(viewManager).invocations.none { + it.method.name == "prepareToRecycleView" + } + ) + .isTrue() + assertThat(child.parent).isNull() + drawAndDrain() + shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(300)) + verify(viewManager, times(1)).onDropViewInstance(child) + verify(viewManager, times(1)).onDropViewInstance(panel) + + surface = manager.startSurface(1, context, root) + val replacement = mount(2, 1) + assertThat(replacement).isNotSameAs(panel).isNotSameAs(child) + } + + @Test + fun stoppingSurfaceDuringCleanupEvictsPoolBeforeRemainingDrops() { + startRecyclingSurface() + val panel = mount(2, 1) + val child = mount(4, 2) + unmount() + panel.setOnHierarchyChangeListener( + object : ViewGroup.OnHierarchyChangeListener { + override fun onChildViewAdded(parent: View, child: View) = Unit + + override fun onChildViewRemoved(parent: View, child: View) { + surface.stopSurface() + } + } + ) + drawAndDrain() + assertThat(surface.isStopped).isTrue() + assertThat(child.parent).isNull() + val ordered = inOrder(viewManager) + ordered.verify(viewManager).onSurfaceStopped(1) + ordered.verify(viewManager).onDropViewInstance(child) + ordered.verify(viewManager).onDropViewInstance(panel) + assertThat( + mockingDetails(viewManager).invocations.none { + it.method.name == "prepareToRecycleView" + } + ) + .isTrue() + drawAndDrain() + verify(viewManager, times(1)).onDropViewInstance(child) + verify(viewManager, times(1)).onDropViewInstance(panel) + } + + private fun startRecyclingSurface() { + surface.stopSurface() + configure(true, recycling = true) + // mockito-inline preserves the exact core manager class required by validation. + viewManager = spy(ReactViewManager()) + assertThat(viewManager.javaClass).isEqualTo(ReactViewManager::class.java) + manager = MountingManager(ViewManagerRegistry(listOf(viewManager)), MountItemExecutor {}) + surface = manager.startSurface(1, context, root) + } + + @Test + fun activeTouchFallsBackToExistingDelayedEmitterDestruction() { + mount(2, 1) + val child = mount(4, 2) + val emitter: EventEmitterWrapper = mock() + surface.updateEventEmitter(4, emitter) + surface.markActiveTouchForTag(4) + unmount() + assertThat(child.parent).isNull() + assertThat(surface.getViewExists(4)).isTrue() + verify(emitter, times(0)).destroy() + surface.sweepActiveTouchForTag(4) + verify(emitter).destroy() + } + + @Test + fun disabledExperimentKeepsSynchronousRemoval() { + configure(false) + mount(2, 1) + val child = mount(4, 2) + unmount() + assertThat(child.parent).isNull() + } + + @Test + fun disabledReorderingKeepsSynchronousRemoval() { + configure(true, disableReordering = true) + mount(2, 1) + val child = mount(4, 2) + unmount() + assertThat(child.parent).isNull() + } + + @Test + fun cleanupAdmissionUsesAnOperationBudget() { + val queue = DeferredViewCleanupQueue() + assertThat(queue.hasCapacityFor(DeferredViewCleanupQueue.MAX_PENDING_OPERATIONS)).isTrue() + queue.enqueue(root, DeferredViewCleanupQueue.MAX_PENDING_OPERATIONS) { true } + assertThat(queue.hasCapacityFor(1)).isFalse() + queue.flush() + assertThat(queue.hasCapacityFor(1)).isTrue() + } + + @Test + fun clippedViewsFallBack() { + val panel = mount(2, 1) + val child = mount(4, 2) + panel.removeClippedSubviews = true + unmount() + assertThat(panel.allChildrenCount).isZero() + assertThat(child.parent).isNull() + } + + @Test + fun animatedViewsFallBack() { + mount(2, 1) + val child = mount(4, 2) + child.animation = AlphaAnimation(0f, 1f) + unmount() + assertThat(child.parent).isNull() + } + + @Test + fun incompleteSubtreeIsRejectedBeforeAnyMutation() { + val panel = mount(2, 1) + val child = mount(4, 2) + val survivor = mount(6, 2, 1) + assertThat( + surface.tryDeferPureViewUnmount( + listOf(intArrayOf(4, 2, 0), intArrayOf(2, 1, 0)), + listOf(4, 2), + ) + ) + .isFalse() + assertThat(panel.parent).isSameAs(root) + assertThat(child.parent).isSameAs(panel) + assertThat(survivor.parent).isSameAs(panel) + } + + private fun assertPairBatchRejected(removes: List, deletes: List) { + val panel = mount(2, 1) + val state: StateWrapper = mock() + val child = mount(4, 2, state = state) + val emitter: EventEmitterWrapper = mock() + surface.updateEventEmitter(4, emitter) + assertThat(surface.tryDeferPureViewUnmount(removes, deletes)).isFalse() + assertThat(panel.parent).isSameAs(root) + assertThat(child.parent).isSameAs(panel) + assertThat(surface.getView(2)).isSameAs(panel) + assertThat(surface.getView(4)).isSameAs(child) + verify(state, times(0)).destroyState() + verify(emitter, times(0)).destroy() + // A rejected validation must leave the original synchronous path usable. + configure(false) + unmount() + assertThat(child.parent).isNull() + assertThat(root.childCount).isZero() + assertThat(surface.getViewExists(2)).isFalse() + assertThat(surface.getViewExists(4)).isFalse() + verify(state).destroyState() + verify(emitter).destroy() + } + + @Test + fun duplicateDeletesAreRejectedWithoutMutation() { + assertPairBatchRejected(listOf(intArrayOf(4, 2, 0), intArrayOf(2, 1, 0)), listOf(4, 4)) + } + + @Test + fun duplicateRemovesAreRejectedWithoutMutation() { + assertPairBatchRejected(listOf(intArrayOf(4, 2, 0), intArrayOf(4, 2, 0)), listOf(4, 2)) + } + + @Test + fun mismatchedRemoveTagIsRejectedWithoutMutation() { + assertPairBatchRejected(listOf(intArrayOf(4, 2, 0), intArrayOf(6, 1, 0)), listOf(4, 2)) + } + + @Test + fun missingDeleteStateIsRejectedWithoutMutation() { + assertPairBatchRejected(listOf(intArrayOf(4, 2, 0), intArrayOf(6, 1, 0)), listOf(4, 6)) + } + + @Test + fun mismatchedOperationCountsAreRejectedWithoutMutation() { + assertPairBatchRejected(listOf(intArrayOf(4, 2, 0)), listOf(4, 2)) + } + + @Test + fun malformedRemoveIsRejectedWithoutMutation() { + assertPairBatchRejected(listOf(intArrayOf(4, 2), intArrayOf(2, 1, 0)), listOf(4, 2)) + } + + @Test + fun negativeChildIndexIsRejectedWithoutMutation() { + assertPairBatchRejected(listOf(intArrayOf(4, 2, -1), intArrayOf(2, 1, 0)), listOf(4, 2)) + } + + @Test + fun outOfBoundsChildIndexIsRejectedWithoutMutation() { + assertPairBatchRejected(listOf(intArrayOf(4, 2, 1), intArrayOf(2, 1, 0)), listOf(4, 2)) + } + + @Test + fun wrongObjectWithMatchingTagIsRejectedWithoutMutation() { + val panel = mount(2, 1) + val child = mount(4, 2) + val impostor = ReactViewGroup(context).apply { id = 4 } + panel.removeViewAt(0) + panel.addView(impostor) + assertThat( + surface.tryDeferPureViewUnmount( + listOf(intArrayOf(4, 2, 0), intArrayOf(2, 1, 0)), + listOf(4, 2), + ) + ) + .isFalse() + assertThat(panel.parent).isSameAs(root) + assertThat(impostor.parent).isSameAs(panel) + assertThat(surface.getView(4)).isSameAs(child) + panel.removeViewAt(0) + panel.addView(child) + configure(false) + unmount() + assertThat(child.parent).isNull() + } + + @Test + fun ascendingSiblingIndicesAreRejectedWithoutMutation() { + val panel = mount(2, 1) + val first = mount(4, 2) + val second = mount(6, 2, 1) + assertThat( + surface.tryDeferPureViewUnmount( + listOf(intArrayOf(4, 2, 0), intArrayOf(6, 2, 1), intArrayOf(2, 1, 0)), + listOf(4, 6, 2), + ) + ) + .isFalse() + assertThat(panel.parent).isSameAs(root) + assertThat(panel.getChildAt(0)).isSameAs(first) + assertThat(panel.getChildAt(1)).isSameAs(second) + surface.removeViewAt(6, 2, 1) + surface.deleteView(6) + configure(false) + unmount() + assertThat(first.parent).isNull() + assertThat(second.parent).isNull() + } + + @Test + fun deleteOrderCanDifferFromRemoveOrderWithSparseTags() { + val panel = mount(100_000, 1) + val child = mount(256, 100_000) + assertThat( + surface.tryDeferPureViewUnmount( + listOf(intArrayOf(256, 100_000, 0), intArrayOf(100_000, 1, 0)), + listOf(100_000, 256), + ) + ) + .isTrue() + assertThat(child.parent).isSameAs(panel) + assertThat(surface.getViewExists(100_000)).isFalse() + assertThat(surface.getViewExists(256)).isFalse() + val replacement = mount(100_000, 1) + drawAndDrain() + assertThat(child.parent).isNull() + assertThat(replacement.parent).isSameAs(root) + } + + @Test + fun multipleRootsKeepSurvivingSiblingIndices() { + val survivor = mount(6, 1) + val first = mount(2, 1, 1) + val firstChild = mount(4, 2) + val second = mount(8, 1, 2) + val secondChild = mount(10, 8) + val accepted = + surface.tryDeferPureViewUnmount( + listOf( + intArrayOf(4, 2, 0), + intArrayOf(10, 8, 0), + intArrayOf(8, 1, 2), + intArrayOf(2, 1, 1), + ), + listOf(4, 2, 10, 8), + ) + assertThat(accepted).isTrue() + assertThat(root.childCount).isEqualTo(1) + assertThat(root.getChildAt(0)).isSameAs(survivor) + assertThat(firstChild.parent).isSameAs(first) + assertThat(secondChild.parent).isSameAs(second) + drawAndDrain() + assertThat(firstChild.parent).isNull() + assertThat(secondChild.parent).isNull() + } + + @Test + fun cleanupIsPostedAfterDrawCallbackReturns_andSlicesAreBounded() { + val queue = DeferredViewCleanupQueue() + var operations = 0 + queue.enqueue(root) { ++operations == 600 } + assertThat(operations).isZero() + root.viewTreeObserver.dispatchOnDraw() + assertThat(operations).isZero() + shadowOf(Looper.getMainLooper()).idle() + assertThat(operations).isBetween(1, 256) + val previous = operations + root.viewTreeObserver.dispatchOnDraw() + shadowOf(Looper.getMainLooper()).idle() + assertThat(operations - previous).isBetween(1, 256) + queue.flush() + assertThat(operations).isEqualTo(600) + assertThat(queue.size).isZero() + } + + @Test + fun detachedSurfaceCanCleanUpWithoutADraw() { + val queue = DeferredViewCleanupQueue() + var operations = 0 + queue.enqueue(View(context)) { ++operations == 1 } + shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100)) + assertThat(operations).isEqualTo(1) + assertThat(queue.size).isZero() + } + + @Test + fun visibleSurfaceDoesNotCleanUpOnTimeoutBeforeDraw() { + val queue = DeferredViewCleanupQueue() + var operations = 0 + val view: View = mock() + val observer = View(context).viewTreeObserver + whenever(view.isAttachedToWindow).thenReturn(true) + whenever(view.isShown).thenReturn(true) + whenever(view.windowVisibility).thenReturn(View.VISIBLE) + whenever(view.viewTreeObserver).thenReturn(observer) + queue.enqueue(view) { ++operations == 1 } + shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(300)) + assertThat(operations).isZero() + observer.dispatchOnDraw() + shadowOf(Looper.getMainLooper()).idle() + assertThat(operations).isEqualTo(1) + } + + @Test + fun queueSurvivesWindowReattachment() { + val queue = DeferredViewCleanupQueue() + var operations = 0 + val view: View = mock() + val oldObserver: android.view.ViewTreeObserver = mock() + val newObserver: android.view.ViewTreeObserver = mock() + whenever(oldObserver.isAlive).thenReturn(true) + whenever(newObserver.isAlive).thenReturn(true) + whenever(view.isAttachedToWindow).thenReturn(true) + whenever(view.isShown).thenReturn(true) + whenever(view.windowVisibility).thenReturn(View.VISIBLE) + whenever(view.viewTreeObserver).thenReturn(oldObserver) + queue.enqueue(view) { ++operations == 1 } + whenever(oldObserver.isAlive).thenReturn(false) + whenever(view.viewTreeObserver).thenReturn(newObserver) + shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100)) + assertThat(operations).isZero() + val listener = argumentCaptor() + verify(newObserver).removeOnDrawListener(org.mockito.kotlin.any()) + verify(newObserver).addOnDrawListener(listener.capture()) + listener.firstValue.onDraw() + shadowOf(Looper.getMainLooper()).idle() + assertThat(operations).isEqualTo(1) + assertThat(queue.size).isZero() + } + + @Test + fun retiredViewsAreOnlyRecycledAfterCleanup_andPoolIsEvictedOnStop() { + surface.stopSurface() + configure(true, recycling = true) + viewManager = ReactViewManager() + manager = MountingManager(ViewManagerRegistry(listOf(viewManager)), MountItemExecutor {}) + surface = manager.startSurface(1, context, root) + val oldPanel = mount(2, 1) + val oldChild = mount(4, 2) + unmount() + val newPanel = mount(6, 1) + assertThat(newPanel).isNotSameAs(oldPanel).isNotSameAs(oldChild) + drawAndDrain() + val recycled = mount(8, 6) + assertThat(recycled === oldPanel || recycled === oldChild).isTrue() + assertThat(recycled.parent).isSameAs(newPanel) + surface.stopSurface() + surface = manager.startSurface(1, context, root) + val fresh = mount(10, 1, root.childCount) + assertThat(fresh).isNotSameAs(recycled).isNotSameAs(newPanel) + } + + @Test + fun mixedTransactionKeepsOriginalSynchronousPath() { + mount(2, 1) + val child = mount(4, 2) + // A padding instruction makes this a mixed batch. + IntBufferBatchMountItem( + 1, + intArrayOf(17, 2, 4, 2, 0, 2, 1, 0, 5, 2, 4, 2, 512, 1, 0, 0, 0, 0), + emptyArray(), + 0, + ) + .execute(manager) + assertThat(child.parent).isNull() + } + + @Test + fun layoutAndOverflowUpdatesKeepOriginalSynchronousPath() { + val sibling = mount(6, 1) + mount(2, 1, 1) + val child = mount(4, 2) + IntBufferBatchMountItem( + 1, + intArrayOf( + 17, + 2, + 4, + 2, + 0, + 2, + 1, + 1, + 5, + 2, + 4, + 2, + IntBufferBatchMountItem.INSTRUCTION_UPDATE_LAYOUT, + 6, + 1, + 0, + 0, + 40, + 20, + 1, + 1, + IntBufferBatchMountItem.INSTRUCTION_UPDATE_OVERFLOW_INSET, + 6, + 0, + 0, + 0, + 0, + ), + emptyArray(), + 0, + ) + .execute(manager) + assertThat(child.parent).isNull() + assertThat(sibling.parent).isSameAs(root) + assertThat(sibling.width).isEqualTo(40) + assertThat(sibling.height).isEqualTo(20) + assertThat(surface.getViewExists(2)).isFalse() + drawAndDrain() + assertThat(surface.getView(6)).isSameAs(sibling) + } + + @Test + fun subtreeAboveAdmissionLimitKeepsOriginalSynchronousPath() { + val stateCount = DeferredViewCleanupQueue.MAX_PENDING_OPERATIONS / 2 + 1 + val panel = mount(2, 1) + val children = (1 until stateCount).map { index -> mount(2 + index * 2, 2, index - 1) } + val instructions = ArrayList() + instructions.add(IntBufferBatchMountItem.INSTRUCTION_REMOVE or 1) + instructions.add(stateCount) + for (index in children.indices.reversed()) { + instructions.add(children[index].id) + instructions.add(2) + instructions.add(index) + } + instructions.addAll(listOf(2, 1, 0)) + instructions.add(IntBufferBatchMountItem.INSTRUCTION_DELETE or 1) + instructions.add(stateCount) + instructions.addAll(children.map { it.id }) + instructions.add(2) + IntBufferBatchMountItem(1, instructions.toIntArray(), emptyArray(), 0).execute(manager) + assertThat(panel.parent).isNull() + assertThat(panel.childCount).isZero() + assertThat(children).allSatisfy { assertThat(it.parent).isNull() } + assertThat(surface.getViewExists(2)).isFalse() + assertThat(root.childCount).isZero() + } + + @Test + fun taskEnqueuedAfterDrawWaitsForItsOwnDraw() { + val queue = DeferredViewCleanupQueue() + var first = 0 + var second = 0 + queue.enqueue(root) { ++first == 1 } + root.viewTreeObserver.dispatchOnDraw() + queue.enqueue(root) { ++second == 1 } + shadowOf(Looper.getMainLooper()).idle() + assertThat(first).isEqualTo(1) + assertThat(second).isZero() + root.viewTreeObserver.dispatchOnDraw() + shadowOf(Looper.getMainLooper()).idle() + assertThat(second).isEqualTo(1) + } + + @Test + fun reentrantFlushDoesNotRemoveAnAlreadyCompletedTaskTwice() { + val queue = DeferredViewCleanupQueue() + var operations = 0 + queue.enqueue(root) { + operations++ + if (operations == 1) queue.flush() + operations >= 3 + } + root.viewTreeObserver.dispatchOnDraw() + shadowOf(Looper.getMainLooper()).idle() + assertThat(operations).isEqualTo(3) + assertThat(queue.size).isZero() + } + + @Test + fun anExpensiveOperationYieldsBeforeTheOperationLimit() { + val queue = DeferredViewCleanupQueue() + var operations = 0 + queue.enqueue(root) { + operations++ + // A real operation may exceed the budget. It must finish, then yield to another draw. + val end = System.nanoTime() + 2_000_000L + while (System.nanoTime() < end) {} + operations == 2 + } + root.viewTreeObserver.dispatchOnDraw() + shadowOf(Looper.getMainLooper()).idle() + assertThat(operations).isEqualTo(1) + root.viewTreeObserver.dispatchOnDraw() + shadowOf(Looper.getMainLooper()).idle() + assertThat(operations).isEqualTo(2) + assertThat(queue.size).isZero() + } + + @Test + fun fullQueueFallsBackWithoutTouchingPreviouslyRetiredViews() { + val retired = ArrayList() + repeat(8) { + mount(2, 1) + retired.add(mount(4, 2)) + unmount() + assertThat(retired.last().parent).isNotNull() + } + mount(2, 1) + val fallback = mount(4, 2) + unmount() + assertThat(fallback.parent).isNull() + assertThat(retired).allSatisfy { assertThat(it.parent).isNotNull() } + surface.stopSurface() + assertThat(retired).allSatisfy { assertThat(it.parent).isNull() } + } +} diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/mounting/mountitems/IntBufferBatchMountItemTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/mounting/mountitems/IntBufferBatchMountItemTest.kt new file mode 100644 index 000000000000..e3631f0d94e9 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/mounting/mountitems/IntBufferBatchMountItemTest.kt @@ -0,0 +1,374 @@ +/* + * 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.mountitems + +import android.app.Activity +import android.view.View +import android.widget.FrameLayout +import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.bridge.ReactTestHelper +import com.facebook.react.fabric.events.EventEmitterWrapper +import com.facebook.react.fabric.mounting.MountingManager +import com.facebook.react.fabric.mounting.MountingManager.MountItemExecutor +import com.facebook.react.fabric.mounting.SurfaceMountingManager +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.ViewManagerRegistry +import com.facebook.react.views.view.ReactViewGroup +import com.facebook.react.views.view.ReactViewManager +import com.facebook.testutils.shadows.ShadowNativeLoader +import com.facebook.testutils.shadows.ShadowSoLoader +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(shadows = [ShadowSoLoader::class, ShadowNativeLoader::class]) +class IntBufferBatchMountItemTest { + private val surfaceId = 1 + private lateinit var mountingManager: MountingManager + private lateinit var surface: SurfaceMountingManager + private lateinit var root: FrameLayout + private lateinit var viewManager: RecordingViewManager + + @Before + fun setUp() { + ReactNativeFeatureFlagsForTests.setUp() + ReactNativeFeatureFlags.override( + object : ReactNativeFeatureFlagsDefaults() { + override fun enableDeferredViewCleanupAndroid() = true + } + ) + val activity = Robolectric.buildActivity(Activity::class.java).setup().get() + root = FrameLayout(activity) + activity.setContentView(root) + val reactContext = ReactTestHelper.createTestReactApplicationContext(activity.application) + val context = ThemedReactContext(reactContext, activity, null, surfaceId) + viewManager = RecordingViewManager() + mountingManager = + MountingManager(ViewManagerRegistry(listOf(viewManager)), MountItemExecutor {}) + surface = mountingManager.startSurface(surfaceId, context, root) + } + + @After + fun tearDown() { + ReactNativeFeatureFlags.dangerouslyReset() + } + + private fun mount(tag: Int, parentTag: Int, index: Int): View { + surface.preallocateView("RCTView", tag, JavaOnlyMap.of(), null, true) + surface.addViewAt(parentTag, tag, index) + return surface.getView(tag) + } + + private fun execute(vararg instructions: Int) { + IntBufferBatchMountItem(surfaceId, instructions, emptyArray(), 0).execute(mountingManager) + } + + @Test + fun customManager_preservesIndividualRemovalsAndDropOrder() { + val panel = mount(2, surfaceId, 0) + val row = mount(4, 2, 0) + val child = mount(6, 4, 0) + assertThat(child.isAttachedToWindow).isTrue() + + execute( + REMOVE_MULTIPLE, + 3, + 6, + 4, + 0, + 4, + 2, + 0, + 2, + surfaceId, + 0, + DELETE_MULTIPLE, + 3, + 6, + 4, + 2, + ) + + assertThat(viewManager.attachedParentsAtRemoval).containsExactly(true, true) + assertThat(root.childCount).isZero() + assertThat(listOf(panel, row, child).map { it.parent }).containsOnlyNulls() + assertThat(viewManager.droppedTags).containsExactly(6, 4, 2) + for (tag in intArrayOf(2, 4, 6)) { + assertThat(surface.getViewExists(tag)).isFalse() + } + } + + @Test + fun unmountSiblings_preservesRemovalIndicesAndSurvivingChildren() { + val survivor = mount(2, surfaceId, 0) + mount(4, surfaceId, 1) + mount(6, 4, 0) + mount(8, surfaceId, 2) + mount(10, 8, 0) + + execute( + REMOVE_MULTIPLE, + 4, + 6, + 4, + 0, + 10, + 8, + 0, + 8, + surfaceId, + 2, + 4, + surfaceId, + 1, + DELETE_MULTIPLE, + 4, + 6, + 4, + 10, + 8, + ) + + assertThat(root.childCount).isEqualTo(1) + assertThat(root.getChildAt(0)).isSameAs(survivor) + assertThat(viewManager.attachedParentsAtRemoval).containsExactly(true, true) + assertThat(viewManager.droppedTags).containsExactly(6, 4, 10, 8) + } + + @Test + fun removeAndReinsertSubtree_keepsViewsAlive() { + val destination = mount(2, surfaceId, 0) + val panel = mount(4, surfaceId, 1) + val child = mount(6, 4, 0) + + execute( + REMOVE_MULTIPLE, + 2, + 6, + 4, + 0, + 4, + surfaceId, + 1, + INSERT, + 4, + 2, + 0, + INSERT, + 6, + 4, + 0, + ) + + assertThat(panel.parent).isSameAs(destination) + assertThat(child.parent).isSameAs(panel) + assertThat(child.isAttachedToWindow).isTrue() + assertThat(viewManager.droppedTags).isEmpty() + } + + @Test + fun deleteAndRecreateTag_destroysOldEmitterInSameBatch() { + val oldPanel = mount(2, surfaceId, 0) + val oldChild = mount(4, 2, 0) + val emitter: EventEmitterWrapper = mock() + surface.updateEventEmitter(4, emitter) + + IntBufferBatchMountItem( + surfaceId, + intArrayOf( + REMOVE_MULTIPLE, + 2, + 4, + 2, + 0, + 2, + surfaceId, + 0, + DELETE_MULTIPLE, + 2, + 4, + 2, + CREATE, + 2, + 1, + INSERT, + 2, + surfaceId, + 0, + ), + arrayOf("RCTView", JavaOnlyMap.of(), null, null), + 0, + ) + .execute(mountingManager) + + verify(emitter).destroy() + assertThat(viewManager.droppedTags).containsExactly(4, 2) + assertThat(surface.getView(2)).isNotSameAs(oldPanel) + assertThat(surface.getView(2).parent).isSameAs(root) + assertThat(oldChild.parent).isNull() + assertThat(surface.getViewExists(4)).isFalse() + } + + @Test + fun activeTouch_delaysDeletionUntilTouchEnds() { + mount(2, surfaceId, 0) + val child = mount(4, 2, 0) + surface.markActiveTouchForTag(4) + + execute( + REMOVE_MULTIPLE, + 2, + 4, + 2, + 0, + 2, + surfaceId, + 0, + DELETE_MULTIPLE, + 2, + 4, + 2, + ) + + assertThat(child.parent).isNull() + assertThat(surface.getViewExists(4)).isTrue() + assertThat(viewManager.droppedTags).containsExactly(2) + surface.sweepActiveTouchForTag(4) + assertThat(surface.getViewExists(4)).isFalse() + assertThat(viewManager.droppedTags).containsExactly(2, 4) + } + + @Test + fun clippedSubtree_removesLogicalChildrenAndDropsEachViewOnce() { + val panel = mount(2, surfaceId, 0) as ReactViewGroup + mount(4, 2, 0) + mount(6, 2, 1) + panel.removeClippedSubviews = true + + execute( + REMOVE_MULTIPLE, + 3, + 6, + 2, + 1, + 4, + 2, + 0, + 2, + surfaceId, + 0, + DELETE_MULTIPLE, + 3, + 6, + 4, + 2, + ) + + assertThat(panel.allChildrenCount).isZero() + assertThat(root.childCount).isZero() + assertThat(viewManager.droppedTags).containsExactly(6, 4, 2) + } + + @Test + fun separateRemovalGroups_doNotMoveRemovalsAcrossOtherInstructions() { + val panel = mount(2, surfaceId, 0) + val child = mount(4, 2, 0) + + execute( + REMOVE, + 4, + 2, + 0, + INSERT, + 4, + 2, + 0, + REMOVE_MULTIPLE, + 2, + 4, + 2, + 0, + 2, + surfaceId, + 0, + ) + + assertThat(viewManager.attachedParentsAtRemoval).containsExactly(true, true) + assertThat(panel.parent).isNull() + assertThat(child.parent).isNull() + } + + @Test + fun disableMountItemReordering_preservesOriginalRemovalOrder() { + ReactNativeFeatureFlags.dangerouslyReset() + ReactNativeFeatureFlags.override( + object : ReactNativeFeatureFlagsDefaults() { + override fun disableMountItemReorderingAndroid() = true + + override fun enableDeferredViewCleanupAndroid() = true + } + ) + mount(2, surfaceId, 0) + mount(4, 2, 0) + mount(6, 4, 0) + + execute( + REMOVE_MULTIPLE, + 3, + 6, + 4, + 0, + 4, + 2, + 0, + 2, + surfaceId, + 0, + ) + + assertThat(viewManager.attachedParentsAtRemoval).containsExactly(true, true) + assertThat(root.childCount).isZero() + } + + private class RecordingViewManager : ReactViewManager() { + val attachedParentsAtRemoval = mutableListOf() + val droppedTags = mutableListOf() + + override fun removeViewAt(parent: ReactViewGroup, index: Int) { + attachedParentsAtRemoval.add(parent.isAttachedToWindow) + super.removeViewAt(parent, index) + } + + override fun onDropViewInstance(view: ReactViewGroup) { + droppedTags.add(view.id) + super.onDropViewInstance(view) + } + } + + companion object { + private const val REMOVE_MULTIPLE = IntBufferBatchMountItem.INSTRUCTION_REMOVE or 1 + private const val DELETE_MULTIPLE = IntBufferBatchMountItem.INSTRUCTION_DELETE or 1 + private const val INSERT = IntBufferBatchMountItem.INSTRUCTION_INSERT + private const val REMOVE = IntBufferBatchMountItem.INSTRUCTION_REMOVE + private const val CREATE = IntBufferBatchMountItem.INSTRUCTION_CREATE + } +} diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index ac8ddb96a112..503712e781c3 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<56e127471f63e47798ec619e6fa56738>> + * @generated SignedSource<<8313677038bc76a8e656eee1532d60f1>> */ /** @@ -110,6 +110,10 @@ bool ReactNativeFeatureFlags::enableCustomFocusSearchOnClippedElementsAndroid() return getAccessor().enableCustomFocusSearchOnClippedElementsAndroid(); } +bool ReactNativeFeatureFlags::enableDeferredViewCleanupAndroid() { + return getAccessor().enableDeferredViewCleanupAndroid(); +} + bool ReactNativeFeatureFlags::enableDestroyShadowTreeRevisionAsync() { return getAccessor().enableDestroyShadowTreeRevisionAsync(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 0e5b3f2013fb..96dee130c740 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<6887b671526bc8a3b0678e3e45b4e3b4>> + * @generated SignedSource<> */ /** @@ -146,6 +146,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool enableCustomFocusSearchOnClippedElementsAndroid(); + /** + * Experiment with retiring pure RCTView subtrees immediately and cleaning native descendants in bounded slices after drawing on Android + */ + RN_EXPORT static bool enableDeferredViewCleanupAndroid(); + /** * Enables destructor calls for ShadowTreeRevision in the background to reduce UI thread work. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 7640cea6418a..2210b85c6870 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<5d9712bd7e62ba3a29b084ba4c60bec5>> + * @generated SignedSource<<7b50b4eadcdd21f53b9e23b58c3b59bc>> */ /** @@ -407,6 +407,24 @@ bool ReactNativeFeatureFlagsAccessor::enableCustomFocusSearchOnClippedElementsAn return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::enableDeferredViewCleanupAndroid() { + auto flagValue = enableDeferredViewCleanupAndroid_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(21, "enableDeferredViewCleanupAndroid"); + + flagValue = currentProvider_->enableDeferredViewCleanupAndroid(); + enableDeferredViewCleanupAndroid_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::enableDestroyShadowTreeRevisionAsync() { auto flagValue = enableDestroyShadowTreeRevisionAsync_.load(); @@ -416,7 +434,7 @@ bool ReactNativeFeatureFlagsAccessor::enableDestroyShadowTreeRevisionAsync() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(21, "enableDestroyShadowTreeRevisionAsync"); + markFlagAsAccessed(22, "enableDestroyShadowTreeRevisionAsync"); flagValue = currentProvider_->enableDestroyShadowTreeRevisionAsync(); enableDestroyShadowTreeRevisionAsync_ = flagValue; @@ -434,7 +452,7 @@ bool ReactNativeFeatureFlagsAccessor::enableDoubleMeasurementFixAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(22, "enableDoubleMeasurementFixAndroid"); + markFlagAsAccessed(23, "enableDoubleMeasurementFixAndroid"); flagValue = currentProvider_->enableDoubleMeasurementFixAndroid(); enableDoubleMeasurementFixAndroid_ = flagValue; @@ -452,7 +470,7 @@ bool ReactNativeFeatureFlagsAccessor::enableEagerRootViewAttachment() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(23, "enableEagerRootViewAttachment"); + markFlagAsAccessed(24, "enableEagerRootViewAttachment"); flagValue = currentProvider_->enableEagerRootViewAttachment(); enableEagerRootViewAttachment_ = flagValue; @@ -470,7 +488,7 @@ bool ReactNativeFeatureFlagsAccessor::enableExclusivePropsUpdateAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(24, "enableExclusivePropsUpdateAndroid"); + markFlagAsAccessed(25, "enableExclusivePropsUpdateAndroid"); flagValue = currentProvider_->enableExclusivePropsUpdateAndroid(); enableExclusivePropsUpdateAndroid_ = flagValue; @@ -488,7 +506,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFabricCommitBranching() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(25, "enableFabricCommitBranching"); + markFlagAsAccessed(26, "enableFabricCommitBranching"); flagValue = currentProvider_->enableFabricCommitBranching(); enableFabricCommitBranching_ = flagValue; @@ -506,7 +524,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFabricCommitBranchingMergeOnMainThre // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(26, "enableFabricCommitBranchingMergeOnMainThread"); + markFlagAsAccessed(27, "enableFabricCommitBranchingMergeOnMainThread"); flagValue = currentProvider_->enableFabricCommitBranchingMergeOnMainThread(); enableFabricCommitBranchingMergeOnMainThread_ = flagValue; @@ -524,7 +542,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFabricLogs() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(27, "enableFabricLogs"); + markFlagAsAccessed(28, "enableFabricLogs"); flagValue = currentProvider_->enableFabricLogs(); enableFabricLogs_ = flagValue; @@ -542,7 +560,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFlexboxAutoMinSizeInStrictMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(28, "enableFlexboxAutoMinSizeInStrictMode"); + markFlagAsAccessed(29, "enableFlexboxAutoMinSizeInStrictMode"); flagValue = currentProvider_->enableFlexboxAutoMinSizeInStrictMode(); enableFlexboxAutoMinSizeInStrictMode_ = flagValue; @@ -560,7 +578,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFollowUpMountItemDispatchAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(29, "enableFollowUpMountItemDispatchAndroid"); + markFlagAsAccessed(30, "enableFollowUpMountItemDispatchAndroid"); flagValue = currentProvider_->enableFollowUpMountItemDispatchAndroid(); enableFollowUpMountItemDispatchAndroid_ = flagValue; @@ -578,7 +596,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIOSCompressedTextFrameAdjustment() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(30, "enableIOSCompressedTextFrameAdjustment"); + markFlagAsAccessed(31, "enableIOSCompressedTextFrameAdjustment"); flagValue = currentProvider_->enableIOSCompressedTextFrameAdjustment(); enableIOSCompressedTextFrameAdjustment_ = flagValue; @@ -596,7 +614,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIOSTextBaselineOffsetPerLine() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(31, "enableIOSTextBaselineOffsetPerLine"); + markFlagAsAccessed(32, "enableIOSTextBaselineOffsetPerLine"); flagValue = currentProvider_->enableIOSTextBaselineOffsetPerLine(); enableIOSTextBaselineOffsetPerLine_ = flagValue; @@ -614,7 +632,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIOSViewClipToPaddingBox() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(32, "enableIOSViewClipToPaddingBox"); + markFlagAsAccessed(33, "enableIOSViewClipToPaddingBox"); flagValue = currentProvider_->enableIOSViewClipToPaddingBox(); enableIOSViewClipToPaddingBox_ = flagValue; @@ -632,7 +650,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImagePrefetchingAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(33, "enableImagePrefetchingAndroid"); + markFlagAsAccessed(34, "enableImagePrefetchingAndroid"); flagValue = currentProvider_->enableImagePrefetchingAndroid(); enableImagePrefetchingAndroid_ = flagValue; @@ -650,7 +668,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImageTransparentTintColor() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(34, "enableImageTransparentTintColor"); + markFlagAsAccessed(35, "enableImageTransparentTintColor"); flagValue = currentProvider_->enableImageTransparentTintColor(); enableImageTransparentTintColor_ = flagValue; @@ -668,7 +686,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImmediateUpdateModeForContentOffsetC // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(35, "enableImmediateUpdateModeForContentOffsetChanges"); + markFlagAsAccessed(36, "enableImmediateUpdateModeForContentOffsetChanges"); flagValue = currentProvider_->enableImmediateUpdateModeForContentOffsetChanges(); enableImmediateUpdateModeForContentOffsetChanges_ = flagValue; @@ -686,7 +704,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImperativeEvents() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(36, "enableImperativeEvents"); + markFlagAsAccessed(37, "enableImperativeEvents"); flagValue = currentProvider_->enableImperativeEvents(); enableImperativeEvents_ = flagValue; @@ -704,7 +722,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImperativeFocus() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(37, "enableImperativeFocus"); + markFlagAsAccessed(38, "enableImperativeFocus"); flagValue = currentProvider_->enableImperativeFocus(); enableImperativeFocus_ = flagValue; @@ -722,7 +740,7 @@ bool ReactNativeFeatureFlagsAccessor::enableInteropViewManagerClassLookUpOptimiz // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(38, "enableInteropViewManagerClassLookUpOptimizationIOS"); + markFlagAsAccessed(39, "enableInteropViewManagerClassLookUpOptimizationIOS"); flagValue = currentProvider_->enableInteropViewManagerClassLookUpOptimizationIOS(); enableInteropViewManagerClassLookUpOptimizationIOS_ = flagValue; @@ -740,7 +758,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIntersectionObserverByDefault() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(39, "enableIntersectionObserverByDefault"); + markFlagAsAccessed(40, "enableIntersectionObserverByDefault"); flagValue = currentProvider_->enableIntersectionObserverByDefault(); enableIntersectionObserverByDefault_ = flagValue; @@ -758,7 +776,7 @@ bool ReactNativeFeatureFlagsAccessor::enableKeyEvents() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(40, "enableKeyEvents"); + markFlagAsAccessed(41, "enableKeyEvents"); flagValue = currentProvider_->enableKeyEvents(); enableKeyEvents_ = flagValue; @@ -776,7 +794,7 @@ bool ReactNativeFeatureFlagsAccessor::enableLayoutAnimationsOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(41, "enableLayoutAnimationsOnAndroid"); + markFlagAsAccessed(42, "enableLayoutAnimationsOnAndroid"); flagValue = currentProvider_->enableLayoutAnimationsOnAndroid(); enableLayoutAnimationsOnAndroid_ = flagValue; @@ -794,7 +812,7 @@ bool ReactNativeFeatureFlagsAccessor::enableLayoutAnimationsOnIOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(42, "enableLayoutAnimationsOnIOS"); + markFlagAsAccessed(43, "enableLayoutAnimationsOnIOS"); flagValue = currentProvider_->enableLayoutAnimationsOnIOS(); enableLayoutAnimationsOnIOS_ = flagValue; @@ -812,7 +830,7 @@ bool ReactNativeFeatureFlagsAccessor::enableModuleArgumentNSNullConversionIOS() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(43, "enableModuleArgumentNSNullConversionIOS"); + markFlagAsAccessed(44, "enableModuleArgumentNSNullConversionIOS"); flagValue = currentProvider_->enableModuleArgumentNSNullConversionIOS(); enableModuleArgumentNSNullConversionIOS_ = flagValue; @@ -830,7 +848,7 @@ bool ReactNativeFeatureFlagsAccessor::enableMountingCoordinatorPullModelAndroid( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(44, "enableMountingCoordinatorPullModelAndroid"); + markFlagAsAccessed(45, "enableMountingCoordinatorPullModelAndroid"); flagValue = currentProvider_->enableMountingCoordinatorPullModelAndroid(); enableMountingCoordinatorPullModelAndroid_ = flagValue; @@ -848,7 +866,7 @@ bool ReactNativeFeatureFlagsAccessor::enableMutationObserverByDefault() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(45, "enableMutationObserverByDefault"); + markFlagAsAccessed(46, "enableMutationObserverByDefault"); flagValue = currentProvider_->enableMutationObserverByDefault(); enableMutationObserverByDefault_ = flagValue; @@ -866,7 +884,7 @@ bool ReactNativeFeatureFlagsAccessor::enableNativeCSSParsing() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(46, "enableNativeCSSParsing"); + markFlagAsAccessed(47, "enableNativeCSSParsing"); flagValue = currentProvider_->enableNativeCSSParsing(); enableNativeCSSParsing_ = flagValue; @@ -884,7 +902,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePreallocatedPropsDiffOnInsertAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(47, "enablePreallocatedPropsDiffOnInsertAndroid"); + markFlagAsAccessed(48, "enablePreallocatedPropsDiffOnInsertAndroid"); flagValue = currentProvider_->enablePreallocatedPropsDiffOnInsertAndroid(); enablePreallocatedPropsDiffOnInsertAndroid_ = flagValue; @@ -902,7 +920,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePreparedTextLayout() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(48, "enablePreparedTextLayout"); + markFlagAsAccessed(49, "enablePreparedTextLayout"); flagValue = currentProvider_->enablePreparedTextLayout(); enablePreparedTextLayout_ = flagValue; @@ -920,7 +938,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePropsUpdateReconciliationAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(49, "enablePropsUpdateReconciliationAndroid"); + markFlagAsAccessed(50, "enablePropsUpdateReconciliationAndroid"); flagValue = currentProvider_->enablePropsUpdateReconciliationAndroid(); enablePropsUpdateReconciliationAndroid_ = flagValue; @@ -938,7 +956,7 @@ bool ReactNativeFeatureFlagsAccessor::enableResizeObserverByDefault() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(50, "enableResizeObserverByDefault"); + markFlagAsAccessed(51, "enableResizeObserverByDefault"); flagValue = currentProvider_->enableResizeObserverByDefault(); enableResizeObserverByDefault_ = flagValue; @@ -956,7 +974,7 @@ bool ReactNativeFeatureFlagsAccessor::enableSwiftUIBasedFilters() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(51, "enableSwiftUIBasedFilters"); + markFlagAsAccessed(52, "enableSwiftUIBasedFilters"); flagValue = currentProvider_->enableSwiftUIBasedFilters(); enableSwiftUIBasedFilters_ = flagValue; @@ -974,7 +992,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewCulling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(52, "enableViewCulling"); + markFlagAsAccessed(53, "enableViewCulling"); flagValue = currentProvider_->enableViewCulling(); enableViewCulling_ = flagValue; @@ -992,7 +1010,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecycling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(53, "enableViewRecycling"); + markFlagAsAccessed(54, "enableViewRecycling"); flagValue = currentProvider_->enableViewRecycling(); enableViewRecycling_ = flagValue; @@ -1010,7 +1028,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForImage() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(54, "enableViewRecyclingForImage"); + markFlagAsAccessed(55, "enableViewRecyclingForImage"); flagValue = currentProvider_->enableViewRecyclingForImage(); enableViewRecyclingForImage_ = flagValue; @@ -1028,7 +1046,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForScrollView() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(55, "enableViewRecyclingForScrollView"); + markFlagAsAccessed(56, "enableViewRecyclingForScrollView"); flagValue = currentProvider_->enableViewRecyclingForScrollView(); enableViewRecyclingForScrollView_ = flagValue; @@ -1046,7 +1064,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForText() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(56, "enableViewRecyclingForText"); + markFlagAsAccessed(57, "enableViewRecyclingForText"); flagValue = currentProvider_->enableViewRecyclingForText(); enableViewRecyclingForText_ = flagValue; @@ -1064,7 +1082,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForView() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(57, "enableViewRecyclingForView"); + markFlagAsAccessed(58, "enableViewRecyclingForView"); flagValue = currentProvider_->enableViewRecyclingForView(); enableViewRecyclingForView_ = flagValue; @@ -1082,7 +1100,7 @@ bool ReactNativeFeatureFlagsAccessor::enableVirtualViewContainerStateExperimenta // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(58, "enableVirtualViewContainerStateExperimental"); + markFlagAsAccessed(59, "enableVirtualViewContainerStateExperimental"); flagValue = currentProvider_->enableVirtualViewContainerStateExperimental(); enableVirtualViewContainerStateExperimental_ = flagValue; @@ -1100,7 +1118,7 @@ bool ReactNativeFeatureFlagsAccessor::fixBorderlessRippleAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(59, "fixBorderlessRippleAndroid"); + markFlagAsAccessed(60, "fixBorderlessRippleAndroid"); flagValue = currentProvider_->fixBorderlessRippleAndroid(); fixBorderlessRippleAndroid_ = flagValue; @@ -1118,7 +1136,7 @@ bool ReactNativeFeatureFlagsAccessor::fixMappingOfEventPrioritiesBetweenFabricAn // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(60, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); + markFlagAsAccessed(61, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); flagValue = currentProvider_->fixMappingOfEventPrioritiesBetweenFabricAndReact(); fixMappingOfEventPrioritiesBetweenFabricAndReact_ = flagValue; @@ -1136,7 +1154,7 @@ bool ReactNativeFeatureFlagsAccessor::fixTextLayoutOnPixelDensityChange() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(61, "fixTextLayoutOnPixelDensityChange"); + markFlagAsAccessed(62, "fixTextLayoutOnPixelDensityChange"); flagValue = currentProvider_->fixTextLayoutOnPixelDensityChange(); fixTextLayoutOnPixelDensityChange_ = flagValue; @@ -1154,7 +1172,7 @@ bool ReactNativeFeatureFlagsAccessor::fixYogaFlexBasisFitContentInMainAxis() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(62, "fixYogaFlexBasisFitContentInMainAxis"); + markFlagAsAccessed(63, "fixYogaFlexBasisFitContentInMainAxis"); flagValue = currentProvider_->fixYogaFlexBasisFitContentInMainAxis(); fixYogaFlexBasisFitContentInMainAxis_ = flagValue; @@ -1172,7 +1190,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxAssertSingleHostState() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(63, "fuseboxAssertSingleHostState"); + markFlagAsAccessed(64, "fuseboxAssertSingleHostState"); flagValue = currentProvider_->fuseboxAssertSingleHostState(); fuseboxAssertSingleHostState_ = flagValue; @@ -1190,7 +1208,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxEnabledRelease() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(64, "fuseboxEnabledRelease"); + markFlagAsAccessed(65, "fuseboxEnabledRelease"); flagValue = currentProvider_->fuseboxEnabledRelease(); fuseboxEnabledRelease_ = flagValue; @@ -1208,7 +1226,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxFrameRecordingEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(65, "fuseboxFrameRecordingEnabled"); + markFlagAsAccessed(66, "fuseboxFrameRecordingEnabled"); flagValue = currentProvider_->fuseboxFrameRecordingEnabled(); fuseboxFrameRecordingEnabled_ = flagValue; @@ -1226,7 +1244,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxScreenshotCaptureEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(66, "fuseboxScreenshotCaptureEnabled"); + markFlagAsAccessed(67, "fuseboxScreenshotCaptureEnabled"); flagValue = currentProvider_->fuseboxScreenshotCaptureEnabled(); fuseboxScreenshotCaptureEnabled_ = flagValue; @@ -1244,7 +1262,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxWebSocketEventsEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(67, "fuseboxWebSocketEventsEnabled"); + markFlagAsAccessed(68, "fuseboxWebSocketEventsEnabled"); flagValue = currentProvider_->fuseboxWebSocketEventsEnabled(); fuseboxWebSocketEventsEnabled_ = flagValue; @@ -1262,7 +1280,7 @@ bool ReactNativeFeatureFlagsAccessor::keepTimersRunningWhileHostPausedAndroid() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(68, "keepTimersRunningWhileHostPausedAndroid"); + markFlagAsAccessed(69, "keepTimersRunningWhileHostPausedAndroid"); flagValue = currentProvider_->keepTimersRunningWhileHostPausedAndroid(); keepTimersRunningWhileHostPausedAndroid_ = flagValue; @@ -1280,7 +1298,7 @@ bool ReactNativeFeatureFlagsAccessor::optimizedAnimatedPropUpdates() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(69, "optimizedAnimatedPropUpdates"); + markFlagAsAccessed(70, "optimizedAnimatedPropUpdates"); flagValue = currentProvider_->optimizedAnimatedPropUpdates(); optimizedAnimatedPropUpdates_ = flagValue; @@ -1298,7 +1316,7 @@ bool ReactNativeFeatureFlagsAccessor::overrideBySynchronousMountPropsAtMountingA // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(70, "overrideBySynchronousMountPropsAtMountingAndroid"); + markFlagAsAccessed(71, "overrideBySynchronousMountPropsAtMountingAndroid"); flagValue = currentProvider_->overrideBySynchronousMountPropsAtMountingAndroid(); overrideBySynchronousMountPropsAtMountingAndroid_ = flagValue; @@ -1316,7 +1334,7 @@ bool ReactNativeFeatureFlagsAccessor::perfIssuesEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(71, "perfIssuesEnabled"); + markFlagAsAccessed(72, "perfIssuesEnabled"); flagValue = currentProvider_->perfIssuesEnabled(); perfIssuesEnabled_ = flagValue; @@ -1334,7 +1352,7 @@ bool ReactNativeFeatureFlagsAccessor::perfMonitorV2Enabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(72, "perfMonitorV2Enabled"); + markFlagAsAccessed(73, "perfMonitorV2Enabled"); flagValue = currentProvider_->perfMonitorV2Enabled(); perfMonitorV2Enabled_ = flagValue; @@ -1352,7 +1370,7 @@ double ReactNativeFeatureFlagsAccessor::preparedTextCacheSize() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(73, "preparedTextCacheSize"); + markFlagAsAccessed(74, "preparedTextCacheSize"); flagValue = currentProvider_->preparedTextCacheSize(); preparedTextCacheSize_ = flagValue; @@ -1370,7 +1388,7 @@ bool ReactNativeFeatureFlagsAccessor::preventShadowTreeCommitExhaustion() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(74, "preventShadowTreeCommitExhaustion"); + markFlagAsAccessed(75, "preventShadowTreeCommitExhaustion"); flagValue = currentProvider_->preventShadowTreeCommitExhaustion(); preventShadowTreeCommitExhaustion_ = flagValue; @@ -1388,7 +1406,7 @@ bool ReactNativeFeatureFlagsAccessor::redBoxV2Android() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(75, "redBoxV2Android"); + markFlagAsAccessed(76, "redBoxV2Android"); flagValue = currentProvider_->redBoxV2Android(); redBoxV2Android_ = flagValue; @@ -1406,7 +1424,7 @@ bool ReactNativeFeatureFlagsAccessor::redBoxV2IOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(76, "redBoxV2IOS"); + markFlagAsAccessed(77, "redBoxV2IOS"); flagValue = currentProvider_->redBoxV2IOS(); redBoxV2IOS_ = flagValue; @@ -1424,7 +1442,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldPressibilityUseW3CPointerEventsForHo // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(77, "shouldPressibilityUseW3CPointerEventsForHover"); + markFlagAsAccessed(78, "shouldPressibilityUseW3CPointerEventsForHover"); flagValue = currentProvider_->shouldPressibilityUseW3CPointerEventsForHover(); shouldPressibilityUseW3CPointerEventsForHover_ = flagValue; @@ -1442,7 +1460,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldTriggerResponderTransferOnScrollAndr // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(78, "shouldTriggerResponderTransferOnScrollAndroid"); + markFlagAsAccessed(79, "shouldTriggerResponderTransferOnScrollAndroid"); flagValue = currentProvider_->shouldTriggerResponderTransferOnScrollAndroid(); shouldTriggerResponderTransferOnScrollAndroid_ = flagValue; @@ -1460,7 +1478,7 @@ bool ReactNativeFeatureFlagsAccessor::skipActivityIdentityAssertionOnHostPause() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(79, "skipActivityIdentityAssertionOnHostPause"); + markFlagAsAccessed(80, "skipActivityIdentityAssertionOnHostPause"); flagValue = currentProvider_->skipActivityIdentityAssertionOnHostPause(); skipActivityIdentityAssertionOnHostPause_ = flagValue; @@ -1478,7 +1496,7 @@ bool ReactNativeFeatureFlagsAccessor::skipUnchangedChildrenInDifferentiator() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(80, "skipUnchangedChildrenInDifferentiator"); + markFlagAsAccessed(81, "skipUnchangedChildrenInDifferentiator"); flagValue = currentProvider_->skipUnchangedChildrenInDifferentiator(); skipUnchangedChildrenInDifferentiator_ = flagValue; @@ -1496,7 +1514,7 @@ bool ReactNativeFeatureFlagsAccessor::syncAndroidClipBoundsWithOverflow() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(81, "syncAndroidClipBoundsWithOverflow"); + markFlagAsAccessed(82, "syncAndroidClipBoundsWithOverflow"); flagValue = currentProvider_->syncAndroidClipBoundsWithOverflow(); syncAndroidClipBoundsWithOverflow_ = flagValue; @@ -1514,7 +1532,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(82, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(83, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -1532,7 +1550,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommit( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(83, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(84, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -1550,7 +1568,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommitT // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(84, "updateRuntimeShadowNodeReferencesOnCommitThread"); + markFlagAsAccessed(85, "updateRuntimeShadowNodeReferencesOnCommitThread"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommitThread(); updateRuntimeShadowNodeReferencesOnCommitThread_ = flagValue; @@ -1568,7 +1586,7 @@ bool ReactNativeFeatureFlagsAccessor::useAlwaysAvailableJSErrorHandling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(85, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(86, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -1586,7 +1604,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(86, "useFabricInterop"); + markFlagAsAccessed(87, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -1604,7 +1622,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(87, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(88, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -1622,7 +1640,7 @@ bool ReactNativeFeatureFlagsAccessor::useNestedScrollViewAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(88, "useNestedScrollViewAndroid"); + markFlagAsAccessed(89, "useNestedScrollViewAndroid"); flagValue = currentProvider_->useNestedScrollViewAndroid(); useNestedScrollViewAndroid_ = flagValue; @@ -1640,7 +1658,7 @@ bool ReactNativeFeatureFlagsAccessor::useSharedAnimatedBackend() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(89, "useSharedAnimatedBackend"); + markFlagAsAccessed(90, "useSharedAnimatedBackend"); flagValue = currentProvider_->useSharedAnimatedBackend(); useSharedAnimatedBackend_ = flagValue; @@ -1658,7 +1676,7 @@ bool ReactNativeFeatureFlagsAccessor::useTraitHiddenOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(90, "useTraitHiddenOnAndroid"); + markFlagAsAccessed(91, "useTraitHiddenOnAndroid"); flagValue = currentProvider_->useTraitHiddenOnAndroid(); useTraitHiddenOnAndroid_ = flagValue; @@ -1676,7 +1694,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(91, "useTurboModuleInterop"); + markFlagAsAccessed(92, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1694,7 +1712,7 @@ bool ReactNativeFeatureFlagsAccessor::useWindowDensityForDisplayMetricsAndroid() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(92, "useWindowDensityForDisplayMetricsAndroid"); + markFlagAsAccessed(93, "useWindowDensityForDisplayMetricsAndroid"); flagValue = currentProvider_->useWindowDensityForDisplayMetricsAndroid(); useWindowDensityForDisplayMetricsAndroid_ = flagValue; @@ -1712,7 +1730,7 @@ double ReactNativeFeatureFlagsAccessor::viewCullingOutsetRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(93, "viewCullingOutsetRatio"); + markFlagAsAccessed(94, "viewCullingOutsetRatio"); flagValue = currentProvider_->viewCullingOutsetRatio(); viewCullingOutsetRatio_ = flagValue; @@ -1730,7 +1748,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(94, "viewTransitionEnabled"); + markFlagAsAccessed(95, "viewTransitionEnabled"); flagValue = currentProvider_->viewTransitionEnabled(); viewTransitionEnabled_ = flagValue; @@ -1748,7 +1766,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionUseHardwareBitmapAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(95, "viewTransitionUseHardwareBitmapAndroid"); + markFlagAsAccessed(96, "viewTransitionUseHardwareBitmapAndroid"); flagValue = currentProvider_->viewTransitionUseHardwareBitmapAndroid(); viewTransitionUseHardwareBitmapAndroid_ = flagValue; @@ -1766,7 +1784,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(96, "virtualViewPrerenderRatio"); + markFlagAsAccessed(97, "virtualViewPrerenderRatio"); flagValue = currentProvider_->virtualViewPrerenderRatio(); virtualViewPrerenderRatio_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index 7a9bc59bc57e..288105bd08f2 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<17f41335dce7e5b45e7af2878c47d69b>> + * @generated SignedSource<<31565afae024721e2c6b174154d34b5e>> */ /** @@ -55,6 +55,7 @@ class ReactNativeFeatureFlagsAccessor { bool enableBridgelessArchitecture(); bool enableCppPropsIteratorSetter(); bool enableCustomFocusSearchOnClippedElementsAndroid(); + bool enableDeferredViewCleanupAndroid(); bool enableDestroyShadowTreeRevisionAsync(); bool enableDoubleMeasurementFixAndroid(); bool enableEagerRootViewAttachment(); @@ -142,7 +143,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 97> accessedFeatureFlags_; + std::array, 98> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; @@ -165,6 +166,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> enableBridgelessArchitecture_; std::atomic> enableCppPropsIteratorSetter_; std::atomic> enableCustomFocusSearchOnClippedElementsAndroid_; + std::atomic> enableDeferredViewCleanupAndroid_; std::atomic> enableDestroyShadowTreeRevisionAsync_; std::atomic> enableDoubleMeasurementFixAndroid_; std::atomic> enableEagerRootViewAttachment_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 600aab1882cc..3535729cdb31 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<35e8175277baa31d135d802ef5a5e6f1>> + * @generated SignedSource<<28efd87716f01f50e71d95b299285cc3>> */ /** @@ -113,6 +113,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return true; } + bool enableDeferredViewCleanupAndroid() override { + return false; + } + bool enableDestroyShadowTreeRevisionAsync() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index 88878807cfec..5050634b725a 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -236,6 +236,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::enableCustomFocusSearchOnClippedElementsAndroid(); } + bool enableDeferredViewCleanupAndroid() override { + auto value = values_["enableDeferredViewCleanupAndroid"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableDeferredViewCleanupAndroid(); + } + bool enableDestroyShadowTreeRevisionAsync() override { auto value = values_["enableDestroyShadowTreeRevisionAsync"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index 664922287009..e8e6d94655fc 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<7413e958441d0502dc1266ff87e9da85>> + * @generated SignedSource<<1d60e151861a5273e7ed92ac47b2226e>> */ /** @@ -48,6 +48,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool enableBridgelessArchitecture() = 0; virtual bool enableCppPropsIteratorSetter() = 0; virtual bool enableCustomFocusSearchOnClippedElementsAndroid() = 0; + virtual bool enableDeferredViewCleanupAndroid() = 0; virtual bool enableDestroyShadowTreeRevisionAsync() = 0; virtual bool enableDoubleMeasurementFixAndroid() = 0; virtual bool enableEagerRootViewAttachment() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index ca8a80f1adbf..856ab626c303 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -149,6 +149,11 @@ bool NativeReactNativeFeatureFlags::enableCustomFocusSearchOnClippedElementsAndr return ReactNativeFeatureFlags::enableCustomFocusSearchOnClippedElementsAndroid(); } +bool NativeReactNativeFeatureFlags::enableDeferredViewCleanupAndroid( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::enableDeferredViewCleanupAndroid(); +} + bool NativeReactNativeFeatureFlags::enableDestroyShadowTreeRevisionAsync( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::enableDestroyShadowTreeRevisionAsync(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 7413766e285a..997024ff2eed 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<90f271ea47f91e6a84979209b7b3d816>> + * @generated SignedSource<> */ /** @@ -80,6 +80,8 @@ class NativeReactNativeFeatureFlags bool enableCustomFocusSearchOnClippedElementsAndroid(jsi::Runtime& runtime); + bool enableDeferredViewCleanupAndroid(jsi::Runtime& runtime); + bool enableDestroyShadowTreeRevisionAsync(jsi::Runtime& runtime); bool enableDoubleMeasurementFixAndroid(jsi::Runtime& runtime); diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 612c196ff22d..09ec8ff1188d 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -278,6 +278,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + enableDeferredViewCleanupAndroid: { + defaultValue: false, + metadata: { + dateAdded: '2026-10-27', + description: + 'Experiment with retiring pure RCTView subtrees immediately and cleaning native descendants in bounded slices after drawing on Android', + expectedReleaseValue: false, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, enableDestroyShadowTreeRevisionAsync: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index e5a8120cff62..158b2120b31e 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<674f975884ae746e067e614523f2bfc3>> + * @generated SignedSource<<8cdfb2126d4af4c32e114fc4531e995a>> * @flow strict * @noformat */ @@ -70,6 +70,7 @@ export type ReactNativeFeatureFlags = Readonly<{ enableBridgelessArchitecture: Getter, enableCppPropsIteratorSetter: Getter, enableCustomFocusSearchOnClippedElementsAndroid: Getter, + enableDeferredViewCleanupAndroid: Getter, enableDestroyShadowTreeRevisionAsync: Getter, enableDoubleMeasurementFixAndroid: Getter, enableEagerRootViewAttachment: Getter, @@ -306,6 +307,10 @@ export const enableCppPropsIteratorSetter: Getter = createNativeFlagGet * This enables the fabric implementation of focus search so that we can focus clipped elements */ export const enableCustomFocusSearchOnClippedElementsAndroid: Getter = createNativeFlagGetter('enableCustomFocusSearchOnClippedElementsAndroid', true); +/** + * Experiment with retiring pure RCTView subtrees immediately and cleaning native descendants in bounded slices after drawing on Android + */ +export const enableDeferredViewCleanupAndroid: Getter = createNativeFlagGetter('enableDeferredViewCleanupAndroid', false); /** * Enables destructor calls for ShadowTreeRevision in the background to reduce UI thread work. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 6fc752bba2a9..798171176259 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<2eb3f4a3dca013c51ad2913944855010>> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -46,6 +46,7 @@ export interface Spec extends TurboModule { readonly enableBridgelessArchitecture?: () => boolean; readonly enableCppPropsIteratorSetter?: () => boolean; readonly enableCustomFocusSearchOnClippedElementsAndroid?: () => boolean; + readonly enableDeferredViewCleanupAndroid?: () => boolean; readonly enableDestroyShadowTreeRevisionAsync?: () => boolean; readonly enableDoubleMeasurementFixAndroid?: () => boolean; readonly enableEagerRootViewAttachment?: () => boolean;