Skip to content

Commit 8c309d1

Browse files
javachefacebook-github-bot
authored andcommitted
Use MutableIntObjectMap for SurfaceMountingManager view registry (#56646)
Summary: Replace `ConcurrentHashMap<Int, ViewState>` with `MutableIntObjectMap<ViewState>` + `ReentrantReadWriteLock` in `SurfaceMountingManager`, behind the `useOptimizedViewRegistryOnAndroid` feature flag. `ConcurrentHashMap<Int, ViewState>` boxes every `Int` key to `java.lang.Integer` (16 bytes) and allocates a `Node` object per entry (~32 bytes). `MutableIntObjectMap` from `androidx.collection` uses a Swiss Table layout with primitive `IntArray` keys — ~9 bytes/entry vs ~56-64 bytes, a ~6x reduction in map overhead. For a complex surface with 2000+ views, this saves ~90KB per surface in map overhead alone, plus reduced GC pressure from eliminating `Integer` boxing. The `ReentrantReadWriteLock` replaces CHM's built-in concurrency. This matches the access pattern: reads from any thread (`getEventEmitter`, `enqueuePendingEvent`), writes almost exclusively from the UI thread. Readers never block each other. `stopSurface` snapshots entries under the write lock and processes `onViewStateDeleted` outside the lock to minimize reader blocking. Changelog: [Internal] Reviewed By: sammy-SC Differential Revision: D102797904
1 parent 5ae948e commit 8c309d1

21 files changed

Lines changed: 249 additions & 55 deletions

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt

Lines changed: 104 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import android.view.ViewGroup
1515
import android.view.ViewParent
1616
import androidx.annotation.AnyThread
1717
import androidx.annotation.UiThread
18+
import androidx.collection.MutableIntObjectMap
1819
import androidx.collection.SparseArrayCompat
1920
import androidx.core.graphics.drawable.toDrawable
2021
import com.facebook.common.logging.FLog
@@ -56,7 +57,10 @@ import java.util.ArrayDeque
5657
import java.util.LinkedList
5758
import java.util.Queue
5859
import java.util.concurrent.ConcurrentHashMap
60+
import java.util.concurrent.locks.ReentrantReadWriteLock
5961
import kotlin.concurrent.Volatile
62+
import kotlin.concurrent.read
63+
import kotlin.concurrent.write
6064

6165
/** Returns true if the collection contains [key]. */
6266
private operator fun <T> SparseArrayCompat<T>.contains(key: Int): Boolean = containsKey(key)
@@ -84,7 +88,20 @@ internal constructor(
8488
public var context: ThemedReactContext? = reactContext
8589
private set
8690

87-
private val tagToViewState: ConcurrentHashMap<Int, ViewState> = ConcurrentHashMap() // any thread
91+
private val tagToViewState: ConcurrentHashMap<Int, ViewState>?
92+
private val optimizedTagToViewState: MutableIntObjectMap<ViewState>?
93+
private val registryLock = ReentrantReadWriteLock()
94+
95+
init {
96+
if (ReactNativeFeatureFlags.useOptimizedViewRegistryOnAndroid()) {
97+
tagToViewState = null
98+
optimizedTagToViewState = MutableIntObjectMap()
99+
} else {
100+
tagToViewState = ConcurrentHashMap()
101+
optimizedTagToViewState = null
102+
}
103+
}
104+
88105
private val onViewAttachMountItems: Queue<MountItem> = ArrayDeque()
89106

90107
// These are all non-null, until StopSurface is called
@@ -126,7 +143,7 @@ internal constructor(
126143
return
127144
}
128145

129-
tagToViewState[surfaceId] = ViewState(surfaceId, rootView, rootViewManager, true)
146+
registryPut(surfaceId, ViewState(surfaceId, rootView, rootViewManager, true))
130147

131148
val runnable: Runnable =
132149
object : GuardedRunnable(checkNotNull(context)) {
@@ -190,7 +207,7 @@ internal constructor(
190207
if (tagSetForStoppedSurface?.containsKey(tag) == true) {
191208
return true
192209
}
193-
return tagToViewState.containsKey(tag)
210+
return registryContains(tag)
194211
}
195212

196213
@UiThread
@@ -236,7 +253,7 @@ internal constructor(
236253
// Reset all StateWrapper objects
237254
// Since this can happen on any thread, is it possible to race between StateWrapper destruction
238255
// and some accesses from View classes in the UI thread?
239-
for (viewState in tagToViewState.values) {
256+
registryForEachValue { viewState ->
240257
viewState.stateWrapper?.destroyState()
241258
viewState.stateWrapper = null
242259

@@ -248,23 +265,37 @@ internal constructor(
248265
if (ReactNativeFeatureFlags.enableViewRecycling()) {
249266
viewManagerRegistry?.onSurfaceStopped(surfaceId)
250267
}
251-
val tagSetForStoppedSurface =
252-
SparseArrayCompat<Any>().also { this.tagSetForStoppedSurface = it }
253-
for ((key, value) in tagToViewState) {
254-
// Using this as a placeholder value in the map. We're using SparseArrayCompat
255-
// since it can efficiently represent the list of pending tags
256-
tagSetForStoppedSurface[key] = this
257-
258-
// We must call `onDropViewInstance` on all remaining Views
259-
onViewStateDeleted(value)
268+
269+
if (optimizedTagToViewState != null) {
270+
val viewStatesToDelete: ArrayList<ViewState>
271+
registryLock.write {
272+
val tagSetForStoppedSurface =
273+
SparseArrayCompat<Any>().also { this.tagSetForStoppedSurface = it }
274+
viewStatesToDelete = ArrayList(optimizedTagToViewState.size)
275+
optimizedTagToViewState.forEach { key, value ->
276+
tagSetForStoppedSurface[key] = this@SurfaceMountingManager
277+
viewStatesToDelete.add(value)
278+
}
279+
optimizedTagToViewState.clear()
280+
}
281+
for (viewState in viewStatesToDelete) {
282+
onViewStateDeleted(viewState)
283+
}
284+
} else {
285+
val tagSetForStoppedSurface =
286+
SparseArrayCompat<Any>().also { this.tagSetForStoppedSurface = it }
287+
for ((key, value) in tagToViewState!!) {
288+
tagSetForStoppedSurface[key] = this
289+
onViewStateDeleted(value)
290+
}
291+
tagToViewState!!.clear()
260292
}
261293

262294
// Evict all views from cache and memory
263295
jsResponderHandler = null
264296
rootViewManager = null
265297
mountItemExecutor = null
266298
context = null
267-
tagToViewState.clear()
268299
onViewAttachMountItems.clear()
269300
tagToSynchronousMountProps.clear()
270301
FLog.e(TAG, "Surface [$surfaceId] was stopped on SurfaceMountingManager.")
@@ -572,7 +603,7 @@ internal constructor(
572603
this.stateWrapper = stateWrapper
573604
this.eventEmitter = eventEmitterWrapper
574605
}
575-
tagToViewState[reactTag] = viewState
606+
registryPut(reactTag, viewState)
576607

577608
if (isLayoutable) {
578609
@Suppress("UNCHECKED_CAST")
@@ -921,13 +952,19 @@ internal constructor(
921952
return
922953
}
923954

924-
var viewState = tagToViewState[reactTag]
925-
if (viewState == null) {
926-
// TODO T62717437 - Use a flag to determine that these event emitters belong to virtual nodes
927-
// only.
928-
viewState = ViewState(reactTag)
929-
tagToViewState[reactTag] = viewState
930-
}
955+
// TODO T62717437 - Use a flag to determine that these event emitters belong to virtual nodes
956+
// only.
957+
val viewState: ViewState =
958+
if (optimizedTagToViewState != null) {
959+
registryLock.write { optimizedTagToViewState.getOrPut(reactTag) { ViewState(reactTag) } }
960+
} else {
961+
var vs = tagToViewState!![reactTag]
962+
if (vs == null) {
963+
vs = ViewState(reactTag)
964+
tagToViewState!![reactTag] = vs
965+
}
966+
vs
967+
}
931968
val previousEventEmitterWrapper = viewState.eventEmitter
932969
viewState.eventEmitter = eventEmitter
933970

@@ -1039,7 +1076,7 @@ internal constructor(
10391076
// To delete we simply remove the tag from the registry.
10401077
// We want to rely on the correct set of MountInstructions being sent to the platform,
10411078
// or StopSurface being called, so we do not handle deleting descendants of the View.
1042-
tagToViewState.remove(reactTag)
1079+
registryRemove(reactTag)
10431080

10441081
onViewStateDeleted(viewState)
10451082
}
@@ -1084,12 +1121,52 @@ internal constructor(
10841121
}
10851122

10861123
private fun getViewState(reactTag: Int): ViewState =
1087-
tagToViewState[reactTag]
1124+
registryGet(reactTag)
10881125
?: throw RetryableMountingLayerException(
10891126
"Unable to find viewState for tag $reactTag. Surface stopped: $isStopped"
10901127
)
10911128

1092-
private fun getNullableViewState(reactTag: Int): ViewState? = tagToViewState[reactTag]
1129+
private fun getNullableViewState(reactTag: Int): ViewState? = registryGet(reactTag)
1130+
1131+
private fun registryGet(tag: Int): ViewState? {
1132+
return if (optimizedTagToViewState != null) {
1133+
registryLock.read { optimizedTagToViewState[tag] }
1134+
} else {
1135+
tagToViewState!![tag]
1136+
}
1137+
}
1138+
1139+
private fun registryPut(tag: Int, state: ViewState) {
1140+
if (optimizedTagToViewState != null) {
1141+
registryLock.write { optimizedTagToViewState[tag] = state }
1142+
} else {
1143+
tagToViewState!![tag] = state
1144+
}
1145+
}
1146+
1147+
private fun registryRemove(tag: Int) {
1148+
if (optimizedTagToViewState != null) {
1149+
registryLock.write { optimizedTagToViewState.remove(tag) }
1150+
} else {
1151+
tagToViewState!!.remove(tag)
1152+
}
1153+
}
1154+
1155+
private fun registryContains(tag: Int): Boolean {
1156+
return if (optimizedTagToViewState != null) {
1157+
registryLock.read { optimizedTagToViewState.containsKey(tag) }
1158+
} else {
1159+
tagToViewState!!.containsKey(tag)
1160+
}
1161+
}
1162+
1163+
private inline fun registryForEachValue(action: (ViewState) -> Unit) {
1164+
if (optimizedTagToViewState != null) {
1165+
registryLock.read { optimizedTagToViewState.forEachValue(action) }
1166+
} else {
1167+
tagToViewState!!.values.forEach(action)
1168+
}
1169+
}
10931170

10941171
/** Applies a bitmap as the background of the view with the given tag, if it exists. */
10951172
@UiThread
@@ -1100,7 +1177,7 @@ internal constructor(
11001177

11011178
public fun printSurfaceState(): Unit {
11021179
FLog.e(TAG, "Views created for surface $surfaceId:")
1103-
for (viewState in tagToViewState.values) {
1180+
registryForEachValue { viewState ->
11041181
val viewManagerName = viewState.viewManager?.name
11051182
val view = viewState.view
11061183
val parent = if (view != null) view.parent as View? else null
@@ -1142,7 +1219,7 @@ internal constructor(
11421219
): Unit {
11431220
// When the surface stopped we will reset the view state map. We are not going to enqueue
11441221
// pending events as they are not expected to be dispatched anyways.
1145-
val viewState = tagToViewState[reactTag]
1222+
val viewState = registryGet(reactTag)
11461223

11471224
if (viewState == null) {
11481225
// Cannot queue event without view state. Do nothing here.

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<cd8218c8b8588f3317bf63ce8d608548>>
7+
* @generated SignedSource<<6ad566ffaa8330c696fa2088ff696a2b>>
88
*/
99

1010
/**
@@ -534,6 +534,12 @@ public object ReactNativeFeatureFlags {
534534
@JvmStatic
535535
public fun useNestedScrollViewAndroid(): Boolean = accessor.useNestedScrollViewAndroid()
536536

537+
/**
538+
* Use MutableIntObjectMap with ReadWriteLock instead of ConcurrentHashMap for the view registry in SurfaceMountingManager to reduce memory overhead and GC pressure.
539+
*/
540+
@JvmStatic
541+
public fun useOptimizedViewRegistryOnAndroid(): Boolean = accessor.useOptimizedViewRegistryOnAndroid()
542+
537543
/**
538544
* Use shared animation backend in C++ Animated
539545
*/

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<4f9f5c1c46217ed6802abd5f786aac19>>
7+
* @generated SignedSource<<ff84a26e3306cc438fead82ad766ce53>>
88
*/
99

1010
/**
@@ -104,6 +104,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
104104
private var useLISAlgorithmInDifferentiatorCache: Boolean? = null
105105
private var useNativeViewConfigsInBridgelessModeCache: Boolean? = null
106106
private var useNestedScrollViewAndroidCache: Boolean? = null
107+
private var useOptimizedViewRegistryOnAndroidCache: Boolean? = null
107108
private var useSharedAnimatedBackendCache: Boolean? = null
108109
private var useTraitHiddenOnAndroidCache: Boolean? = null
109110
private var useTurboModuleInteropCache: Boolean? = null
@@ -869,6 +870,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
869870
return cached
870871
}
871872

873+
override fun useOptimizedViewRegistryOnAndroid(): Boolean {
874+
var cached = useOptimizedViewRegistryOnAndroidCache
875+
if (cached == null) {
876+
cached = ReactNativeFeatureFlagsCxxInterop.useOptimizedViewRegistryOnAndroid()
877+
useOptimizedViewRegistryOnAndroidCache = cached
878+
}
879+
return cached
880+
}
881+
872882
override fun useSharedAnimatedBackend(): Boolean {
873883
var cached = useSharedAnimatedBackendCache
874884
if (cached == null) {

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<8916e9f4a938a69ff175c806db9835d4>>
7+
* @generated SignedSource<<817790c7ceb9112376b4ab4ee338ff43>>
88
*/
99

1010
/**
@@ -196,6 +196,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
196196

197197
@DoNotStrip @JvmStatic public external fun useNestedScrollViewAndroid(): Boolean
198198

199+
@DoNotStrip @JvmStatic public external fun useOptimizedViewRegistryOnAndroid(): Boolean
200+
199201
@DoNotStrip @JvmStatic public external fun useSharedAnimatedBackend(): Boolean
200202

201203
@DoNotStrip @JvmStatic public external fun useTraitHiddenOnAndroid(): Boolean

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<a9a8ce443fa160a7494fc1c9e7baa02f>>
7+
* @generated SignedSource<<1d764e41252e2709e574da5e0ac64bd7>>
88
*/
99

1010
/**
@@ -191,6 +191,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
191191

192192
override fun useNestedScrollViewAndroid(): Boolean = false
193193

194+
override fun useOptimizedViewRegistryOnAndroid(): Boolean = false
195+
194196
override fun useSharedAnimatedBackend(): Boolean = false
195197

196198
override fun useTraitHiddenOnAndroid(): Boolean = false

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<cbe90c2bf8ba9d34804d97c31edfd31a>>
7+
* @generated SignedSource<<299507458fe84339ddf816dd58671fd3>>
88
*/
99

1010
/**
@@ -108,6 +108,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
108108
private var useLISAlgorithmInDifferentiatorCache: Boolean? = null
109109
private var useNativeViewConfigsInBridgelessModeCache: Boolean? = null
110110
private var useNestedScrollViewAndroidCache: Boolean? = null
111+
private var useOptimizedViewRegistryOnAndroidCache: Boolean? = null
111112
private var useSharedAnimatedBackendCache: Boolean? = null
112113
private var useTraitHiddenOnAndroidCache: Boolean? = null
113114
private var useTurboModuleInteropCache: Boolean? = null
@@ -957,6 +958,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
957958
return cached
958959
}
959960

961+
override fun useOptimizedViewRegistryOnAndroid(): Boolean {
962+
var cached = useOptimizedViewRegistryOnAndroidCache
963+
if (cached == null) {
964+
cached = currentProvider.useOptimizedViewRegistryOnAndroid()
965+
accessedFeatureFlags.add("useOptimizedViewRegistryOnAndroid")
966+
useOptimizedViewRegistryOnAndroidCache = cached
967+
}
968+
return cached
969+
}
970+
960971
override fun useSharedAnimatedBackend(): Boolean {
961972
var cached = useSharedAnimatedBackendCache
962973
if (cached == null) {

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<536a5156deea17740dd24782bf79feb4>>
7+
* @generated SignedSource<<8f887fb839df553b23886a61537e6991>>
88
*/
99

1010
/**
@@ -191,6 +191,8 @@ public interface ReactNativeFeatureFlagsProvider {
191191

192192
@DoNotStrip public fun useNestedScrollViewAndroid(): Boolean
193193

194+
@DoNotStrip public fun useOptimizedViewRegistryOnAndroid(): Boolean
195+
194196
@DoNotStrip public fun useSharedAnimatedBackend(): Boolean
195197

196198
@DoNotStrip public fun useTraitHiddenOnAndroid(): Boolean

0 commit comments

Comments
 (0)