diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp index e91e643ccd2..483a1915fd6 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp @@ -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 diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.h b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.h index 87b6a7afa47..33e10e102a8 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.h +++ b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.h @@ -8,6 +8,8 @@ #pragma once #include +#include +#include #include #include @@ -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 mountingCoordinator_; + + using UniqueLock = std::variant< + std::unique_lock, + std::unique_lock>; + using SharedLock = std::variant< + std::shared_lock, + std::unique_lock>; + + inline UniqueLock uniqueCommitLock() const; + inline SharedLock sharedCommitLock() const; }; } // namespace facebook::react diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 74e6001241f..253ca39522b 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -821,6 +821,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', + }, }, }; diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index f5720762666..2859d86c466 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<> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -44,6 +44,7 @@ export type ReactNativeFeatureFlagsJsOnly = $ReadOnly<{ shouldUseRemoveClippedSubviewsAsDefaultOnIOS: Getter, shouldUseSetNativePropsInFabric: Getter, utilizeTokensInIntersectionObserver: Getter, + preventShadowTreeCommitExhaustion: Getter, }>; export type ReactNativeFeatureFlagsJsOnlyOverrides = OverridesFor; @@ -191,6 +192,11 @@ export const shouldUseSetNativePropsInFabric: Getter = createJavaScript */ export const utilizeTokensInIntersectionObserver: Getter = createJavaScriptFlagGetter('utilizeTokensInIntersectionObserver', true); +/** + * 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 = createJavaScriptFlagGetter('preventShadowTreeCommitExhaustion', false); + /** * Common flag for testing. Do NOT modify. */