Implement solution for ShadowTree commmit exhaustion using recursive locks (behind a flag) (#52795)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/52795

Changelog: [internal]

This is another attempt to fix https://github.com/facebook/react-native/issues/51870, inspired by https://github.com/facebook/react-native/pull/52314 but gated behind a feature flag until we've tested it carefully.

Reviewed By: sammy-SC

Differential Revision: D78817100

fbshipit-source-id: 45e6cae019b212528f2b2e74b9f52fe43d07f537
This commit is contained in:
Rubén Norte
2025-07-23 15:06:12 -07:00
committed by Facebook GitHub Bot
parent c6c7c3720f
commit 2de4984970
24 changed files with 224 additions and 52 deletions
@@ -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<<a5908495c093849007082e6297647f8a>>
* @generated SignedSource<<92170b32c13dc405a81b4a5cf5677afb>>
*/
/**
@@ -312,6 +312,12 @@ public object ReactNativeFeatureFlags {
@JvmStatic
public fun preparedTextCacheSize(): Double = accessor.preparedTextCacheSize()
/**
* Enables a new mechanism in ShadowTree to prevent problems caused by multiple threads trying to commit concurrently. If a thread tries to commit a few times unsuccessfully, it will acquire a lock and try again.
*/
@JvmStatic
public fun preventShadowTreeCommitExhaustion(): Boolean = accessor.preventShadowTreeCommitExhaustion()
/**
* Releases the cached image data when it is consumed by the observers.
*/
@@ -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<<d4f92fba9ba8b4d457587446ce069d32>>
* @generated SignedSource<<34c0a7def872e270cd542785d9bfde40>>
*/
/**
@@ -67,6 +67,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
private var fuseboxNetworkInspectionEnabledCache: Boolean? = null
private var hideOffscreenVirtualViewsOnIOSCache: Boolean? = null
private var preparedTextCacheSizeCache: Double? = null
private var preventShadowTreeCommitExhaustionCache: Boolean? = null
private var releaseImageDataWhenConsumedCache: Boolean? = null
private var shouldPressibilityUseW3CPointerEventsForHoverCache: Boolean? = null
private var skipActivityIdentityAssertionOnHostPauseCache: Boolean? = null
@@ -507,6 +508,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
return cached
}
override fun preventShadowTreeCommitExhaustion(): Boolean {
var cached = preventShadowTreeCommitExhaustionCache
if (cached == null) {
cached = ReactNativeFeatureFlagsCxxInterop.preventShadowTreeCommitExhaustion()
preventShadowTreeCommitExhaustionCache = cached
}
return cached
}
override fun releaseImageDataWhenConsumed(): Boolean {
var cached = releaseImageDataWhenConsumedCache
if (cached == null) {
@@ -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<<568d3a6628dd6cc9b32a77c590210730>>
* @generated SignedSource<<3c9a5f37b4d141ffcf9a2ec152f70563>>
*/
/**
@@ -122,6 +122,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
@DoNotStrip @JvmStatic public external fun preparedTextCacheSize(): Double
@DoNotStrip @JvmStatic public external fun preventShadowTreeCommitExhaustion(): Boolean
@DoNotStrip @JvmStatic public external fun releaseImageDataWhenConsumed(): Boolean
@DoNotStrip @JvmStatic public external fun shouldPressibilityUseW3CPointerEventsForHover(): Boolean
@@ -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<<276706dd4eef0370e61fc36e3420f0aa>>
* @generated SignedSource<<728ae563ec31340ccc8c4c593d566c64>>
*/
/**
@@ -117,6 +117,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
override fun preparedTextCacheSize(): Double = 200.0
override fun preventShadowTreeCommitExhaustion(): Boolean = false
override fun releaseImageDataWhenConsumed(): Boolean = false
override fun shouldPressibilityUseW3CPointerEventsForHover(): Boolean = false
@@ -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<<c387f3ce74df2fc0120f5abd0dcb2f9b>>
* @generated SignedSource<<9a5b0bdc0bd9ab0889e548b25cfe5371>>
*/
/**
@@ -71,6 +71,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
private var fuseboxNetworkInspectionEnabledCache: Boolean? = null
private var hideOffscreenVirtualViewsOnIOSCache: Boolean? = null
private var preparedTextCacheSizeCache: Double? = null
private var preventShadowTreeCommitExhaustionCache: Boolean? = null
private var releaseImageDataWhenConsumedCache: Boolean? = null
private var shouldPressibilityUseW3CPointerEventsForHoverCache: Boolean? = null
private var skipActivityIdentityAssertionOnHostPauseCache: Boolean? = null
@@ -558,6 +559,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
return cached
}
override fun preventShadowTreeCommitExhaustion(): Boolean {
var cached = preventShadowTreeCommitExhaustionCache
if (cached == null) {
cached = currentProvider.preventShadowTreeCommitExhaustion()
accessedFeatureFlags.add("preventShadowTreeCommitExhaustion")
preventShadowTreeCommitExhaustionCache = cached
}
return cached
}
override fun releaseImageDataWhenConsumed(): Boolean {
var cached = releaseImageDataWhenConsumedCache
if (cached == null) {
@@ -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<<636ec2163060c2ef6fdbc5e27ef41716>>
* @generated SignedSource<<0bafb0a2fb79c4220d21f1736894af14>>
*/
/**
@@ -23,6 +23,8 @@ public open class ReactNativeFeatureFlagsOverrides_RNOSS_Experimental_Android :
// We could use JNI to get the defaults from C++,
// but that is more expensive than just duplicating the defaults here.
override fun preventShadowTreeCommitExhaustion(): Boolean = true
override fun useNativeEqualsInNativeReadableArrayAndroid(): Boolean = true
override fun useNativeTransformHelperAndroid(): Boolean = true
@@ -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<<68ec2f18b3554bcd6e3e10c530d8cca0>>
* @generated SignedSource<<ee4525af457a5ad08f506cf8447a4156>>
*/
/**
@@ -117,6 +117,8 @@ public interface ReactNativeFeatureFlagsProvider {
@DoNotStrip public fun preparedTextCacheSize(): Double
@DoNotStrip public fun preventShadowTreeCommitExhaustion(): Boolean
@DoNotStrip public fun releaseImageDataWhenConsumed(): Boolean
@DoNotStrip public fun shouldPressibilityUseW3CPointerEventsForHover(): Boolean
@@ -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<<c00ad0b31911831b23dd4c9ebab1c494>>
* @generated SignedSource<<fa975c22ecec781f4621cad8b90ea290>>
*/
/**
@@ -321,6 +321,12 @@ class ReactNativeFeatureFlagsJavaProvider
return method(javaProvider_);
}
bool preventShadowTreeCommitExhaustion() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("preventShadowTreeCommitExhaustion");
return method(javaProvider_);
}
bool releaseImageDataWhenConsumed() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("releaseImageDataWhenConsumed");
@@ -656,6 +662,11 @@ double JReactNativeFeatureFlagsCxxInterop::preparedTextCacheSize(
return ReactNativeFeatureFlags::preparedTextCacheSize();
}
bool JReactNativeFeatureFlagsCxxInterop::preventShadowTreeCommitExhaustion(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::preventShadowTreeCommitExhaustion();
}
bool JReactNativeFeatureFlagsCxxInterop::releaseImageDataWhenConsumed(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::releaseImageDataWhenConsumed();
@@ -908,6 +919,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
makeNativeMethod(
"preparedTextCacheSize",
JReactNativeFeatureFlagsCxxInterop::preparedTextCacheSize),
makeNativeMethod(
"preventShadowTreeCommitExhaustion",
JReactNativeFeatureFlagsCxxInterop::preventShadowTreeCommitExhaustion),
makeNativeMethod(
"releaseImageDataWhenConsumed",
JReactNativeFeatureFlagsCxxInterop::releaseImageDataWhenConsumed),
@@ -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<<9ad2d870c21a193730e937d566aae5dc>>
* @generated SignedSource<<3e83546899ed1194fe03f143865ba97a>>
*/
/**
@@ -171,6 +171,9 @@ class JReactNativeFeatureFlagsCxxInterop
static double preparedTextCacheSize(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
static bool preventShadowTreeCommitExhaustion(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
static bool releaseImageDataWhenConsumed(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
@@ -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<<4ee4c8163356e7777ab080a9364a5492>>
* @generated SignedSource<<21d92927b8b282b29ed9ff5795c60f8a>>
*/
/**
@@ -214,6 +214,10 @@ double ReactNativeFeatureFlags::preparedTextCacheSize() {
return getAccessor().preparedTextCacheSize();
}
bool ReactNativeFeatureFlags::preventShadowTreeCommitExhaustion() {
return getAccessor().preventShadowTreeCommitExhaustion();
}
bool ReactNativeFeatureFlags::releaseImageDataWhenConsumed() {
return getAccessor().releaseImageDataWhenConsumed();
}
@@ -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<<0bbb98a247e0cb3d1a54228507f98b49>>
* @generated SignedSource<<4e6e5bfa892fcfc1ad67d625a6b58ce6>>
*/
/**
@@ -274,6 +274,11 @@ class ReactNativeFeatureFlags {
*/
RN_EXPORT static double preparedTextCacheSize();
/**
* Enables a new mechanism in ShadowTree to prevent problems caused by multiple threads trying to commit concurrently. If a thread tries to commit a few times unsuccessfully, it will acquire a lock and try again.
*/
RN_EXPORT static bool preventShadowTreeCommitExhaustion();
/**
* Releases the cached image data when it is consumed by the observers.
*/
@@ -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<<a03ba90ac1fc3573fdacd287051bb7cf>>
* @generated SignedSource<<e3663b1990ce3e0f459a9cb202ad24ee>>
*/
/**
@@ -875,6 +875,24 @@ double ReactNativeFeatureFlagsAccessor::preparedTextCacheSize() {
return flagValue.value();
}
bool ReactNativeFeatureFlagsAccessor::preventShadowTreeCommitExhaustion() {
auto flagValue = preventShadowTreeCommitExhaustion_.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(47, "preventShadowTreeCommitExhaustion");
flagValue = currentProvider_->preventShadowTreeCommitExhaustion();
preventShadowTreeCommitExhaustion_ = flagValue;
}
return flagValue.value();
}
bool ReactNativeFeatureFlagsAccessor::releaseImageDataWhenConsumed() {
auto flagValue = releaseImageDataWhenConsumed_.load();
@@ -884,7 +902,7 @@ bool ReactNativeFeatureFlagsAccessor::releaseImageDataWhenConsumed() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(47, "releaseImageDataWhenConsumed");
markFlagAsAccessed(48, "releaseImageDataWhenConsumed");
flagValue = currentProvider_->releaseImageDataWhenConsumed();
releaseImageDataWhenConsumed_ = flagValue;
@@ -902,7 +920,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(48, "shouldPressibilityUseW3CPointerEventsForHover");
markFlagAsAccessed(49, "shouldPressibilityUseW3CPointerEventsForHover");
flagValue = currentProvider_->shouldPressibilityUseW3CPointerEventsForHover();
shouldPressibilityUseW3CPointerEventsForHover_ = flagValue;
@@ -920,7 +938,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(49, "skipActivityIdentityAssertionOnHostPause");
markFlagAsAccessed(50, "skipActivityIdentityAssertionOnHostPause");
flagValue = currentProvider_->skipActivityIdentityAssertionOnHostPause();
skipActivityIdentityAssertionOnHostPause_ = flagValue;
@@ -938,7 +956,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(50, "traceTurboModulePromiseRejectionsOnAndroid");
markFlagAsAccessed(51, "traceTurboModulePromiseRejectionsOnAndroid");
flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid();
traceTurboModulePromiseRejectionsOnAndroid_ = flagValue;
@@ -956,7 +974,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(51, "updateRuntimeShadowNodeReferencesOnCommit");
markFlagAsAccessed(52, "updateRuntimeShadowNodeReferencesOnCommit");
flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit();
updateRuntimeShadowNodeReferencesOnCommit_ = flagValue;
@@ -974,7 +992,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(52, "useAlwaysAvailableJSErrorHandling");
markFlagAsAccessed(53, "useAlwaysAvailableJSErrorHandling");
flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling();
useAlwaysAvailableJSErrorHandling_ = flagValue;
@@ -992,7 +1010,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(53, "useFabricInterop");
markFlagAsAccessed(54, "useFabricInterop");
flagValue = currentProvider_->useFabricInterop();
useFabricInterop_ = flagValue;
@@ -1010,7 +1028,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeEqualsInNativeReadableArrayAndroi
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(54, "useNativeEqualsInNativeReadableArrayAndroid");
markFlagAsAccessed(55, "useNativeEqualsInNativeReadableArrayAndroid");
flagValue = currentProvider_->useNativeEqualsInNativeReadableArrayAndroid();
useNativeEqualsInNativeReadableArrayAndroid_ = flagValue;
@@ -1028,7 +1046,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeTransformHelperAndroid() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(55, "useNativeTransformHelperAndroid");
markFlagAsAccessed(56, "useNativeTransformHelperAndroid");
flagValue = currentProvider_->useNativeTransformHelperAndroid();
useNativeTransformHelperAndroid_ = flagValue;
@@ -1046,7 +1064,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(56, "useNativeViewConfigsInBridgelessMode");
markFlagAsAccessed(57, "useNativeViewConfigsInBridgelessMode");
flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode();
useNativeViewConfigsInBridgelessMode_ = flagValue;
@@ -1064,7 +1082,7 @@ bool ReactNativeFeatureFlagsAccessor::useOptimizedEventBatchingOnAndroid() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(57, "useOptimizedEventBatchingOnAndroid");
markFlagAsAccessed(58, "useOptimizedEventBatchingOnAndroid");
flagValue = currentProvider_->useOptimizedEventBatchingOnAndroid();
useOptimizedEventBatchingOnAndroid_ = flagValue;
@@ -1082,7 +1100,7 @@ bool ReactNativeFeatureFlagsAccessor::useRawPropsJsiValue() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(58, "useRawPropsJsiValue");
markFlagAsAccessed(59, "useRawPropsJsiValue");
flagValue = currentProvider_->useRawPropsJsiValue();
useRawPropsJsiValue_ = flagValue;
@@ -1100,7 +1118,7 @@ bool ReactNativeFeatureFlagsAccessor::useShadowNodeStateOnClone() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(59, "useShadowNodeStateOnClone");
markFlagAsAccessed(60, "useShadowNodeStateOnClone");
flagValue = currentProvider_->useShadowNodeStateOnClone();
useShadowNodeStateOnClone_ = flagValue;
@@ -1118,7 +1136,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(60, "useTurboModuleInterop");
markFlagAsAccessed(61, "useTurboModuleInterop");
flagValue = currentProvider_->useTurboModuleInterop();
useTurboModuleInterop_ = flagValue;
@@ -1136,7 +1154,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModules() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(61, "useTurboModules");
markFlagAsAccessed(62, "useTurboModules");
flagValue = currentProvider_->useTurboModules();
useTurboModules_ = flagValue;
@@ -1154,7 +1172,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(62, "virtualViewPrerenderRatio");
markFlagAsAccessed(63, "virtualViewPrerenderRatio");
flagValue = currentProvider_->virtualViewPrerenderRatio();
virtualViewPrerenderRatio_ = flagValue;
@@ -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<<84fc9cb4ade1c9a321e7d739b403fa72>>
* @generated SignedSource<<ddda049798e510fb51f84f7d396192e2>>
*/
/**
@@ -79,6 +79,7 @@ class ReactNativeFeatureFlagsAccessor {
bool fuseboxNetworkInspectionEnabled();
bool hideOffscreenVirtualViewsOnIOS();
double preparedTextCacheSize();
bool preventShadowTreeCommitExhaustion();
bool releaseImageDataWhenConsumed();
bool shouldPressibilityUseW3CPointerEventsForHover();
bool skipActivityIdentityAssertionOnHostPause();
@@ -106,7 +107,7 @@ class ReactNativeFeatureFlagsAccessor {
std::unique_ptr<ReactNativeFeatureFlagsProvider> currentProvider_;
bool wasOverridden_;
std::array<std::atomic<const char*>, 63> accessedFeatureFlags_;
std::array<std::atomic<const char*>, 64> accessedFeatureFlags_;
std::atomic<std::optional<bool>> commonTestFlag_;
std::atomic<std::optional<bool>> cxxNativeAnimatedEnabled_;
@@ -155,6 +156,7 @@ class ReactNativeFeatureFlagsAccessor {
std::atomic<std::optional<bool>> fuseboxNetworkInspectionEnabled_;
std::atomic<std::optional<bool>> hideOffscreenVirtualViewsOnIOS_;
std::atomic<std::optional<double>> preparedTextCacheSize_;
std::atomic<std::optional<bool>> preventShadowTreeCommitExhaustion_;
std::atomic<std::optional<bool>> releaseImageDataWhenConsumed_;
std::atomic<std::optional<bool>> shouldPressibilityUseW3CPointerEventsForHover_;
std::atomic<std::optional<bool>> skipActivityIdentityAssertionOnHostPause_;
@@ -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<<4933ca80972665b1ac6e358e626d57ac>>
* @generated SignedSource<<4c386a379665e7c983db3da799f7c902>>
*/
/**
@@ -215,6 +215,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider {
return 200.0;
}
bool preventShadowTreeCommitExhaustion() override {
return false;
}
bool releaseImageDataWhenConsumed() override {
return false;
}
@@ -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<<72907a7058f38e80b54947d321b0062d>>
* @generated SignedSource<<c7559d37ffcb752e95b8313dcd1ee681>>
*/
/**
@@ -468,6 +468,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef
return ReactNativeFeatureFlagsDefaults::preparedTextCacheSize();
}
bool preventShadowTreeCommitExhaustion() override {
auto value = values_["preventShadowTreeCommitExhaustion"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::preventShadowTreeCommitExhaustion();
}
bool releaseImageDataWhenConsumed() override {
auto value = values_["releaseImageDataWhenConsumed"];
if (!value.isNull()) {
@@ -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<<c35e74abcb9b95a9c31854a32c282edd>>
* @generated SignedSource<<a045579d42e45fa80831856734a063aa>>
*/
/**
@@ -27,6 +27,10 @@ class ReactNativeFeatureFlagsOverridesOSSExperimental : public ReactNativeFeatur
public:
ReactNativeFeatureFlagsOverridesOSSExperimental() = default;
bool preventShadowTreeCommitExhaustion() override {
return true;
}
bool useNativeEqualsInNativeReadableArrayAndroid() override {
return true;
}
@@ -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<<87afa2a91688f00bc5843cc13a8b72f1>>
* @generated SignedSource<<96ec4b7e248bbde00b52f09779e8a051>>
*/
/**
@@ -72,6 +72,7 @@ class ReactNativeFeatureFlagsProvider {
virtual bool fuseboxNetworkInspectionEnabled() = 0;
virtual bool hideOffscreenVirtualViewsOnIOS() = 0;
virtual double preparedTextCacheSize() = 0;
virtual bool preventShadowTreeCommitExhaustion() = 0;
virtual bool releaseImageDataWhenConsumed() = 0;
virtual bool shouldPressibilityUseW3CPointerEventsForHover() = 0;
virtual bool skipActivityIdentityAssertionOnHostPause() = 0;
@@ -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<<c000bcb7b4e0cac836a708e509f59b86>>
* @generated SignedSource<<a7a1684e83d7a0d96e9ff5623d74916a>>
*/
/**
@@ -279,6 +279,11 @@ double NativeReactNativeFeatureFlags::preparedTextCacheSize(
return ReactNativeFeatureFlags::preparedTextCacheSize();
}
bool NativeReactNativeFeatureFlags::preventShadowTreeCommitExhaustion(
jsi::Runtime& /*runtime*/) {
return ReactNativeFeatureFlags::preventShadowTreeCommitExhaustion();
}
bool NativeReactNativeFeatureFlags::releaseImageDataWhenConsumed(
jsi::Runtime& /*runtime*/) {
return ReactNativeFeatureFlags::releaseImageDataWhenConsumed();
@@ -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<<dc6fc103b2ecf2a64f229f5f6333c313>>
* @generated SignedSource<<008cdb8915d0252d2c714529fb177245>>
*/
/**
@@ -130,6 +130,8 @@ class NativeReactNativeFeatureFlags
double preparedTextCacheSize(jsi::Runtime& runtime);
bool preventShadowTreeCommitExhaustion(jsi::Runtime& runtime);
bool releaseImageDataWhenConsumed(jsi::Runtime& runtime);
bool shouldPressibilityUseW3CPointerEventsForHover(jsi::Runtime& runtime);
@@ -22,6 +22,10 @@
namespace facebook::react {
namespace {
const int MAX_COMMIT_ATTEMPTS_BEFORE_LOCKING = 3;
} // namespace
using CommitStatus = ShadowTree::CommitStatus;
using CommitMode = ShadowTree::CommitMode;
@@ -210,7 +214,8 @@ void ShadowTree::setCommitMode(CommitMode commitMode) const {
auto revision = ShadowTreeRevision{};
{
std::unique_lock lock(commitMutex_);
ShadowTree::UniqueLock lock = uniqueCommitLock();
if (commitMode_ == commitMode) {
return;
}
@@ -227,7 +232,7 @@ void ShadowTree::setCommitMode(CommitMode commitMode) const {
}
CommitMode ShadowTree::getCommitMode() const {
std::shared_lock lock(commitMutex_);
SharedLock lock = sharedCommitLock();
return commitMode_;
}
@@ -241,17 +246,32 @@ CommitStatus ShadowTree::commit(
const CommitOptions& commitOptions) const {
[[maybe_unused]] int attempts = 0;
while (true) {
attempts++;
auto status = tryCommit(transaction, commitOptions);
if (status != CommitStatus::Failed) {
return status;
if (ReactNativeFeatureFlags::preventShadowTreeCommitExhaustion()) {
while (attempts < MAX_COMMIT_ATTEMPTS_BEFORE_LOCKING) {
auto status = tryCommit(transaction, commitOptions);
if (status != CommitStatus::Failed) {
return status;
}
attempts++;
}
// After multiple attempts, we failed to commit the transaction.
// Something internally went terribly wrong.
react_native_assert(attempts < 1024);
{
std::unique_lock lock(commitMutexRecursive_);
return tryCommit(transaction, commitOptions);
}
} else {
while (true) {
attempts++;
auto status = tryCommit(transaction, commitOptions);
if (status != CommitStatus::Failed) {
return status;
}
// After multiple attempts, we failed to commit the transaction.
// Something internally went terribly wrong.
react_native_assert(attempts < 1024);
}
}
}
@@ -269,7 +289,7 @@ CommitStatus ShadowTree::tryCommit(
{
// Reading `currentRevision_` in shared manner.
std::shared_lock lock(commitMutex_);
SharedLock lock = sharedCommitLock();
commitMode = commitMode_;
oldRevision = currentRevision_;
}
@@ -310,7 +330,7 @@ CommitStatus ShadowTree::tryCommit(
{
// Updating `currentRevision_` in unique manner if it hasn't changed.
std::unique_lock lock(commitMutex_);
UniqueLock lock = uniqueCommitLock();
if (currentRevision_.number != oldRevision.number) {
return CommitStatus::Failed;
@@ -349,7 +369,7 @@ CommitStatus ShadowTree::tryCommit(
}
ShadowTreeRevision ShadowTree::getCurrentRevision() const {
std::shared_lock lock(commitMutex_);
SharedLock lock = sharedCommitLock();
return currentRevision_;
}
@@ -397,4 +417,20 @@ void ShadowTree::notifyDelegatesOfUpdates() const {
delegate_.shadowTreeDidFinishTransaction(mountingCoordinator_, true);
}
inline ShadowTree::UniqueLock ShadowTree::uniqueCommitLock() const {
if (ReactNativeFeatureFlags::preventShadowTreeCommitExhaustion()) {
return std::unique_lock{commitMutexRecursive_};
} else {
return std::unique_lock{commitMutex_};
}
}
inline ShadowTree::SharedLock ShadowTree::sharedCommitLock() const {
if (ReactNativeFeatureFlags::preventShadowTreeCommitExhaustion()) {
return std::unique_lock{commitMutexRecursive_};
} else {
return std::shared_lock{commitMutex_};
}
}
} // namespace facebook::react
@@ -8,6 +8,8 @@
#pragma once
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <react/renderer/components/root/RootShadowNode.h>
#include <react/renderer/core/LayoutConstraints.h>
@@ -150,10 +152,21 @@ class ShadowTree final {
const SurfaceId surfaceId_;
const ShadowTreeDelegate& delegate_;
mutable std::shared_mutex commitMutex_;
mutable std::recursive_mutex commitMutexRecursive_;
mutable CommitMode commitMode_{
CommitMode::Normal}; // Protected by `commitMutex_`.
mutable ShadowTreeRevision currentRevision_; // Protected by `commitMutex_`.
std::shared_ptr<const MountingCoordinator> mountingCoordinator_;
using UniqueLock = std::variant<
std::unique_lock<std::shared_mutex>,
std::unique_lock<std::recursive_mutex>>;
using SharedLock = std::variant<
std::shared_lock<std::shared_mutex>,
std::unique_lock<std::recursive_mutex>>;
inline UniqueLock uniqueCommitLock() const;
inline SharedLock sharedCommitLock() const;
};
} // namespace facebook::react
@@ -547,6 +547,17 @@ const definitions: FeatureFlagDefinitions = {
},
ossReleaseStage: 'none',
},
preventShadowTreeCommitExhaustion: {
defaultValue: false,
metadata: {
dateAdded: '2025-07-23',
description:
'Enables a new mechanism in ShadowTree to prevent problems caused by multiple threads trying to commit concurrently. If a thread tries to commit a few times unsuccessfully, it will acquire a lock and try again.',
expectedReleaseValue: true,
purpose: 'experimentation',
},
ossReleaseStage: 'experimental',
},
releaseImageDataWhenConsumed: {
defaultValue: false,
metadata: {
@@ -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<<4b5f7535318d03c648dd81ca9a81277c>>
* @generated SignedSource<<6a5b796b26a689b4b8ebaa101c60b5c8>>
* @flow strict
* @noformat
*/
@@ -93,6 +93,7 @@ export type ReactNativeFeatureFlags = $ReadOnly<{
fuseboxNetworkInspectionEnabled: Getter<boolean>,
hideOffscreenVirtualViewsOnIOS: Getter<boolean>,
preparedTextCacheSize: Getter<number>,
preventShadowTreeCommitExhaustion: Getter<boolean>,
releaseImageDataWhenConsumed: Getter<boolean>,
shouldPressibilityUseW3CPointerEventsForHover: Getter<boolean>,
skipActivityIdentityAssertionOnHostPause: Getter<boolean>,
@@ -358,6 +359,10 @@ export const hideOffscreenVirtualViewsOnIOS: Getter<boolean> = createNativeFlagG
* Number cached PreparedLayouts in TextLayoutManager cache
*/
export const preparedTextCacheSize: Getter<number> = createNativeFlagGetter('preparedTextCacheSize', 200);
/**
* Enables a new mechanism in ShadowTree to prevent problems caused by multiple threads trying to commit concurrently. If a thread tries to commit a few times unsuccessfully, it will acquire a lock and try again.
*/
export const preventShadowTreeCommitExhaustion: Getter<boolean> = createNativeFlagGetter('preventShadowTreeCommitExhaustion', false);
/**
* Releases the cached image data when it is consumed by the observers.
*/
@@ -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<<2b9624673a7fd8b4f820921ba0a088d6>>
* @generated SignedSource<<119985fbbfbc9ace52b26a7fb3b91f03>>
* @flow strict
* @noformat
*/
@@ -72,6 +72,7 @@ export interface Spec extends TurboModule {
+fuseboxNetworkInspectionEnabled?: () => boolean;
+hideOffscreenVirtualViewsOnIOS?: () => boolean;
+preparedTextCacheSize?: () => number;
+preventShadowTreeCommitExhaustion?: () => boolean;
+releaseImageDataWhenConsumed?: () => boolean;
+shouldPressibilityUseW3CPointerEventsForHover?: () => boolean;
+skipActivityIdentityAssertionOnHostPause?: () => boolean;