From 2d1db71bc019913b7d7fd82f911b119b7d26f01f Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Fri, 27 Jun 2025 14:37:33 -0700 Subject: [PATCH] Add global cache for Facsimile Layouts (#52308) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/52308 Experimentation shows, that we very heavily rely on the global text cache for performance (not sure how much of this is invalidation, vs repeated text, or revisiting previous surfaces). This adds a global LRU cache, of prepared layouts, given a specific AttributedString and constraints. This is similar to the existing cache, with the caveat, that we need to have separate entries for any display states, instead of just those that effect metrics. I sized it at 200 elements for now, since an Android `Layout` is much heavier than a `Size` (in practice, each seem to weight 1-3KB (though this will be significantly reduced with future change to move from `SpannableStringBuilder` to `SpannableString` and also contributes to the global JNI ref table, but set this up to be customizable via flag, so we can experiment, on perf impact, vs memory. Changelog: [Internal] Reviewed By: rshest Differential Revision: D77341994 fbshipit-source-id: b453250dc475f6a281a3260b876bf80f301dd5dd --- .../featureflags/ReactNativeFeatureFlags.kt | 8 +- .../ReactNativeFeatureFlagsCxxAccessor.kt | 12 ++- .../ReactNativeFeatureFlagsCxxInterop.kt | 4 +- .../ReactNativeFeatureFlagsDefaults.kt | 4 +- .../ReactNativeFeatureFlagsLocalAccessor.kt | 13 ++- .../ReactNativeFeatureFlagsProvider.kt | 4 +- .../JReactNativeFeatureFlagsCxxInterop.cpp | 16 +++- .../JReactNativeFeatureFlagsCxxInterop.h | 5 +- .../featureflags/ReactNativeFeatureFlags.cpp | 6 +- .../featureflags/ReactNativeFeatureFlags.h | 7 +- .../ReactNativeFeatureFlagsAccessor.cpp | 42 ++++++--- .../ReactNativeFeatureFlagsAccessor.h | 6 +- .../ReactNativeFeatureFlagsDefaults.h | 6 +- .../ReactNativeFeatureFlagsDynamicProvider.h | 11 ++- .../ReactNativeFeatureFlagsProvider.h | 3 +- .../NativeReactNativeFeatureFlags.cpp | 7 +- .../NativeReactNativeFeatureFlags.h | 4 +- .../textlayoutmanager/TextMeasureCache.h | 91 +++++++++++++++++-- .../textlayoutmanager/TextLayoutManager.cpp | 48 +++++----- .../textlayoutmanager/TextLayoutManager.h | 5 + .../react/utils/SimpleThreadSafeCache.h | 3 + .../ReactNativeFeatureFlags.config.js | 10 ++ .../featureflags/ReactNativeFeatureFlags.js | 7 +- .../specs/NativeReactNativeFeatureFlags.js | 3 +- 24 files changed, 263 insertions(+), 62 deletions(-) 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 23e9b1b68f8..3b878a5511f 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<<697babc37818f427f02fd3ab4c53febd>> */ /** @@ -294,6 +294,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun fuseboxNetworkInspectionEnabled(): Boolean = accessor.fuseboxNetworkInspectionEnabled() + /** + * Number cached PreparedLayouts in TextLayoutManager cache + */ + @JvmStatic + public fun preparedTextCacheSize(): Double = accessor.preparedTextCacheSize() + /** * Enables storing js caller stack when creating promise in native module. This is useful in case of Promise rejection and tracing the cause. */ 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 f8790cf6fb8..be12735e721 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<<0c6c54413dc51e692605811e89d567dc>> + * @generated SignedSource<<275296a2e051e0ae2a17d80836ae9ee1>> */ /** @@ -64,6 +64,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null private var fuseboxEnabledReleaseCache: Boolean? = null private var fuseboxNetworkInspectionEnabledCache: Boolean? = null + private var preparedTextCacheSizeCache: Double? = null private var traceTurboModulePromiseRejectionsOnAndroidCache: Boolean? = null private var updateRuntimeShadowNodeReferencesOnCommitCache: Boolean? = null private var useAlwaysAvailableJSErrorHandlingCache: Boolean? = null @@ -472,6 +473,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun preparedTextCacheSize(): Double { + var cached = preparedTextCacheSizeCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.preparedTextCacheSize() + preparedTextCacheSizeCache = cached + } + return cached + } + override fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean { var cached = traceTurboModulePromiseRejectionsOnAndroidCache 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 f327c64f018..776e82c485f 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<<4c47c4c6977880efee8b45bbcd4d999d>> + * @generated SignedSource<<25ee348b038042f2f45efdb526949f1b>> */ /** @@ -116,6 +116,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun fuseboxNetworkInspectionEnabled(): Boolean + @DoNotStrip @JvmStatic public external fun preparedTextCacheSize(): Double + @DoNotStrip @JvmStatic public external fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean @DoNotStrip @JvmStatic public external fun updateRuntimeShadowNodeReferencesOnCommit(): 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 160275f2d75..533c82cf5a5 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<<33af38489ce1077f24a639db52213953>> + * @generated SignedSource<> */ /** @@ -111,6 +111,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun fuseboxNetworkInspectionEnabled(): Boolean = false + override fun preparedTextCacheSize(): Double = 200.0 + override fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean = false override fun updateRuntimeShadowNodeReferencesOnCommit(): 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 88020903206..e9e5d239a80 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<<97d032014047ab05c6d9fc39665a4b3e>> + * @generated SignedSource<> */ /** @@ -68,6 +68,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null private var fuseboxEnabledReleaseCache: Boolean? = null private var fuseboxNetworkInspectionEnabledCache: Boolean? = null + private var preparedTextCacheSizeCache: Double? = null private var traceTurboModulePromiseRejectionsOnAndroidCache: Boolean? = null private var updateRuntimeShadowNodeReferencesOnCommitCache: Boolean? = null private var useAlwaysAvailableJSErrorHandlingCache: Boolean? = null @@ -520,6 +521,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun preparedTextCacheSize(): Double { + var cached = preparedTextCacheSizeCache + if (cached == null) { + cached = currentProvider.preparedTextCacheSize() + accessedFeatureFlags.add("preparedTextCacheSize") + preparedTextCacheSizeCache = cached + } + return cached + } + override fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean { var cached = traceTurboModulePromiseRejectionsOnAndroidCache 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 a37ca47eb6e..21e83a1612c 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<<8a607ad40a3e991943c32bc120fcd66f>> + * @generated SignedSource<<10197755f82d5b7251563ce1517c8fac>> */ /** @@ -111,6 +111,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun fuseboxNetworkInspectionEnabled(): Boolean + @DoNotStrip public fun preparedTextCacheSize(): Double + @DoNotStrip public fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean @DoNotStrip public fun updateRuntimeShadowNodeReferencesOnCommit(): 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 2398650a289..638f87f03a2 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<<9a0a41bc27ee271578e74664951c5217>> + * @generated SignedSource<<53e68ed98b80c867e3045902e1618c9b>> */ /** @@ -303,6 +303,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + double preparedTextCacheSize() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("preparedTextCacheSize"); + return method(javaProvider_); + } + bool traceTurboModulePromiseRejectionsOnAndroid() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("traceTurboModulePromiseRejectionsOnAndroid"); @@ -593,6 +599,11 @@ bool JReactNativeFeatureFlagsCxxInterop::fuseboxNetworkInspectionEnabled( return ReactNativeFeatureFlags::fuseboxNetworkInspectionEnabled(); } +double JReactNativeFeatureFlagsCxxInterop::preparedTextCacheSize( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::preparedTextCacheSize(); +} + bool JReactNativeFeatureFlagsCxxInterop::traceTurboModulePromiseRejectionsOnAndroid( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::traceTurboModulePromiseRejectionsOnAndroid(); @@ -811,6 +822,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "fuseboxNetworkInspectionEnabled", JReactNativeFeatureFlagsCxxInterop::fuseboxNetworkInspectionEnabled), + makeNativeMethod( + "preparedTextCacheSize", + JReactNativeFeatureFlagsCxxInterop::preparedTextCacheSize), makeNativeMethod( "traceTurboModulePromiseRejectionsOnAndroid", JReactNativeFeatureFlagsCxxInterop::traceTurboModulePromiseRejectionsOnAndroid), 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 2c5cae300e7..629c42816fe 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<<1d3320eb050bd827c0109319afb92991>> + * @generated SignedSource<<70de16b94b14f0e7de235f72e1566abe>> */ /** @@ -162,6 +162,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool fuseboxNetworkInspectionEnabled( facebook::jni::alias_ref); + static double preparedTextCacheSize( + facebook::jni::alias_ref); + static bool traceTurboModulePromiseRejectionsOnAndroid( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index 9142e4e943b..d2fae4a4d0b 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<> + * @generated SignedSource<<2206721fac3452fd4d9ca3053cc7bc32>> */ /** @@ -202,6 +202,10 @@ bool ReactNativeFeatureFlags::fuseboxNetworkInspectionEnabled() { return getAccessor().fuseboxNetworkInspectionEnabled(); } +double ReactNativeFeatureFlags::preparedTextCacheSize() { + return getAccessor().preparedTextCacheSize(); +} + bool ReactNativeFeatureFlags::traceTurboModulePromiseRejectionsOnAndroid() { return getAccessor().traceTurboModulePromiseRejectionsOnAndroid(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 3cb66d455b9..7358e78e2e3 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<<61381e3eb65214c519a01283366a53c6>> + * @generated SignedSource<> */ /** @@ -259,6 +259,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool fuseboxNetworkInspectionEnabled(); + /** + * Number cached PreparedLayouts in TextLayoutManager cache + */ + RN_EXPORT static double preparedTextCacheSize(); + /** * Enables storing js caller stack when creating promise in native module. This is useful in case of Promise rejection and tracing the cause. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index f1294fbea51..8e7b096dc16 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<<02ed1adf133035a71192f6af88d0214e>> + * @generated SignedSource<<00faab987d6e11584cfd01a88552b434>> */ /** @@ -821,6 +821,24 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxNetworkInspectionEnabled() { return flagValue.value(); } +double ReactNativeFeatureFlagsAccessor::preparedTextCacheSize() { + auto flagValue = preparedTextCacheSize_.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(44, "preparedTextCacheSize"); + + flagValue = currentProvider_->preparedTextCacheSize(); + preparedTextCacheSize_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid() { auto flagValue = traceTurboModulePromiseRejectionsOnAndroid_.load(); @@ -830,7 +848,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(44, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(45, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -848,7 +866,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(45, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(46, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -866,7 +884,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(46, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(47, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -884,7 +902,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(47, "useFabricInterop"); + markFlagAsAccessed(48, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -902,7 +920,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(48, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(49, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -920,7 +938,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(49, "useOptimizedEventBatchingOnAndroid"); + markFlagAsAccessed(50, "useOptimizedEventBatchingOnAndroid"); flagValue = currentProvider_->useOptimizedEventBatchingOnAndroid(); useOptimizedEventBatchingOnAndroid_ = flagValue; @@ -938,7 +956,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(50, "useRawPropsJsiValue"); + markFlagAsAccessed(51, "useRawPropsJsiValue"); flagValue = currentProvider_->useRawPropsJsiValue(); useRawPropsJsiValue_ = flagValue; @@ -956,7 +974,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(51, "useShadowNodeStateOnClone"); + markFlagAsAccessed(52, "useShadowNodeStateOnClone"); flagValue = currentProvider_->useShadowNodeStateOnClone(); useShadowNodeStateOnClone_ = flagValue; @@ -974,7 +992,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(52, "useTurboModuleInterop"); + markFlagAsAccessed(53, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -992,7 +1010,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(53, "useTurboModules"); + markFlagAsAccessed(54, "useTurboModules"); flagValue = currentProvider_->useTurboModules(); useTurboModules_ = flagValue; @@ -1010,7 +1028,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(54, "virtualViewPrerenderRatio"); + markFlagAsAccessed(55, "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 67e93a44e5c..9796b7b0fde 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<> + * @generated SignedSource<<8f9118c510588cd6a5f0402870b53eaf>> */ /** @@ -76,6 +76,7 @@ class ReactNativeFeatureFlagsAccessor { bool fixMappingOfEventPrioritiesBetweenFabricAndReact(); bool fuseboxEnabledRelease(); bool fuseboxNetworkInspectionEnabled(); + double preparedTextCacheSize(); bool traceTurboModulePromiseRejectionsOnAndroid(); bool updateRuntimeShadowNodeReferencesOnCommit(); bool useAlwaysAvailableJSErrorHandling(); @@ -98,7 +99,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 55> accessedFeatureFlags_; + std::array, 56> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> animatedShouldSignalBatch_; @@ -144,6 +145,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> fixMappingOfEventPrioritiesBetweenFabricAndReact_; std::atomic> fuseboxEnabledRelease_; std::atomic> fuseboxNetworkInspectionEnabled_; + std::atomic> preparedTextCacheSize_; std::atomic> traceTurboModulePromiseRejectionsOnAndroid_; std::atomic> updateRuntimeShadowNodeReferencesOnCommit_; std::atomic> useAlwaysAvailableJSErrorHandling_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 8118de5cbab..4f180703c75 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<<24eb62c6e8538db6b5aa4f2394c21132>> + * @generated SignedSource<<04ec71486d4052ac5ec1c6362bfd143c>> */ /** @@ -203,6 +203,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + double preparedTextCacheSize() override { + return 200.0; + } + bool traceTurboModulePromiseRejectionsOnAndroid() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index 855c6b3e535..f1438d1ebcc 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<<66d1d0ee39bc491fc95958b45e4e8797>> + * @generated SignedSource<<78e883d9ea6e9e44a035216b8aa7ae98>> */ /** @@ -441,6 +441,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::fuseboxNetworkInspectionEnabled(); } + double preparedTextCacheSize() override { + auto value = values_["preparedTextCacheSize"]; + if (!value.isNull()) { + return value.getDouble(); + } + + return ReactNativeFeatureFlagsDefaults::preparedTextCacheSize(); + } + bool traceTurboModulePromiseRejectionsOnAndroid() override { auto value = values_["traceTurboModulePromiseRejectionsOnAndroid"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index 613b1e55d49..c17c6d32fb6 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<> + * @generated SignedSource<<9358aab32b6368b205da9726a6e57981>> */ /** @@ -69,6 +69,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool fixMappingOfEventPrioritiesBetweenFabricAndReact() = 0; virtual bool fuseboxEnabledRelease() = 0; virtual bool fuseboxNetworkInspectionEnabled() = 0; + virtual double preparedTextCacheSize() = 0; virtual bool traceTurboModulePromiseRejectionsOnAndroid() = 0; virtual bool updateRuntimeShadowNodeReferencesOnCommit() = 0; virtual bool useAlwaysAvailableJSErrorHandling() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index 8dd0302dd4b..5a0e0fe08de 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<<563e9d9ae11c6ec231664126666e9922>> */ /** @@ -264,6 +264,11 @@ bool NativeReactNativeFeatureFlags::fuseboxNetworkInspectionEnabled( return ReactNativeFeatureFlags::fuseboxNetworkInspectionEnabled(); } +double NativeReactNativeFeatureFlags::preparedTextCacheSize( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::preparedTextCacheSize(); +} + bool NativeReactNativeFeatureFlags::traceTurboModulePromiseRejectionsOnAndroid( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::traceTurboModulePromiseRejectionsOnAndroid(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index c657ba900fa..5cc77680d3b 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<<933ffc3782b46f4c4df1b0a9e30ad1d4>> + * @generated SignedSource<> */ /** @@ -124,6 +124,8 @@ class NativeReactNativeFeatureFlags bool fuseboxNetworkInspectionEnabled(jsi::Runtime& runtime); + double preparedTextCacheSize(jsi::Runtime& runtime); + bool traceTurboModulePromiseRejectionsOnAndroid(jsi::Runtime& runtime); bool updateRuntimeShadowNodeReferencesOnCommit(jsi::Runtime& runtime); diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h index ecd6798f47f..962eca68005 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h @@ -83,6 +83,17 @@ class LineMeasureCacheKey final { Size size{}; }; +/** + * Cache key, mapping an AttributedString under given constraints, to a prepared + * (laid out and drawable) representation of the text. + */ +class PreparedTextCacheKey final { + public: + AttributedString attributedString{}; + ParagraphAttributes paragraphAttributes{}; + LayoutConstraints layoutConstraints{}; +}; + /* * Maximum size of the Cache. * The number was empirically chosen based on approximation of an average amount @@ -167,6 +178,17 @@ inline bool areAttributedStringFragmentsEquivalentLayoutWise( rhs.parentShadowView.layoutMetrics)); } +inline bool areAttributedStringFragmentsEquivalentDisplayWise( + const AttributedString::Fragment& lhs, + const AttributedString::Fragment& rhs) { + return lhs.isContentEqual(rhs) && + // LayoutMetrics of an attachment fragment affects the size of a measured + // attributed string. + (!lhs.isAttachment() || + (lhs.parentShadowView.layoutMetrics == + rhs.parentShadowView.layoutMetrics)); +} + inline size_t attributedStringFragmentHashLayoutWise( const AttributedString::Fragment& fragment) { // Here we are not taking `isAttachment` and `layoutMetrics` into account @@ -176,6 +198,15 @@ inline size_t attributedStringFragmentHashLayoutWise( fragment.string, textAttributesHashLayoutWise(fragment.textAttributes)); } +inline size_t attributedStringFragmentHashDisplayWise( + const AttributedString::Fragment& fragment) { + // Here we are not taking `isAttachment` and `layoutMetrics` into account + // because they are logically interdependent and this can break an invariant + // between hash and equivalence functions (and cause cache misses). + return facebook::react::hash_combine( + fragment.string, fragment.textAttributes); +} + inline bool areAttributedStringsEquivalentLayoutWise( const AttributedString& lhs, const AttributedString& rhs) { @@ -197,6 +228,27 @@ inline bool areAttributedStringsEquivalentLayoutWise( return true; } +inline bool areAttributedStringsEquivalentDisplayWise( + const AttributedString& lhs, + const AttributedString& rhs) { + auto& lhsFragment = lhs.getFragments(); + auto& rhsFragment = rhs.getFragments(); + + if (lhsFragment.size() != rhsFragment.size()) { + return false; + } + + auto size = lhsFragment.size(); + for (size_t i = 0; i < size; i++) { + if (!areAttributedStringFragmentsEquivalentDisplayWise( + lhsFragment.at(i), rhsFragment.at(i))) { + return false; + } + } + + return true; +} + inline size_t attributedStringHashLayoutWise( const AttributedString& attributedString) { auto seed = size_t{0}; @@ -209,6 +261,18 @@ inline size_t attributedStringHashLayoutWise( return seed; } +inline size_t attributedStringHashDisplayWise( + const AttributedString& attributedString) { + size_t seed = 0; + + for (const auto& fragment : attributedString.getFragments()) { + facebook::react::hash_combine( + seed, attributedStringFragmentHashDisplayWise(fragment)); + } + + return seed; +} + inline bool operator==( const TextMeasureCacheKey& lhs, const TextMeasureCacheKey& rhs) { @@ -218,12 +282,6 @@ inline bool operator==( lhs.layoutConstraints == rhs.layoutConstraints; } -inline bool operator!=( - const TextMeasureCacheKey& lhs, - const TextMeasureCacheKey& rhs) { - return !(lhs == rhs); -} - inline bool operator==( const LineMeasureCacheKey& lhs, const LineMeasureCacheKey& rhs) { @@ -233,10 +291,13 @@ inline bool operator==( lhs.size == rhs.size; } -inline bool operator!=( - const LineMeasureCacheKey& lhs, - const LineMeasureCacheKey& rhs) { - return !(lhs == rhs); +inline bool operator==( + const PreparedTextCacheKey& lhs, + const PreparedTextCacheKey& rhs) { + return areAttributedStringsEquivalentDisplayWise( + lhs.attributedString, rhs.attributedString) && + lhs.paragraphAttributes == rhs.paragraphAttributes && + lhs.layoutConstraints == rhs.layoutConstraints; } } // namespace facebook::react @@ -263,4 +324,14 @@ struct hash { } }; +template <> +struct hash { + size_t operator()(const facebook::react::PreparedTextCacheKey& key) const { + return facebook::react::hash_combine( + attributedStringHashDisplayWise(key.attributedString), + key.paragraphAttributes, + key.layoutConstraints); + } +}; + } // namespace std diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp index 3ce824fa8aa..9525394f993 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp @@ -158,7 +158,9 @@ TextLayoutManager::TextLayoutManager( const ContextContainer::Shared& contextContainer) : contextContainer_(contextContainer), textMeasureCache_(kSimpleThreadSafeCacheSizeCap), - lineMeasureCache_(kSimpleThreadSafeCacheSizeCap) {} + lineMeasureCache_(kSimpleThreadSafeCacheSizeCap), + preparedTextCache_(static_cast( + ReactNativeFeatureFlags::preparedTextCacheSize())) {} TextMeasurement TextLayoutManager::measure( const AttributedStringBox& attributedStringBox, @@ -297,9 +299,6 @@ TextLayoutManager::PreparedLayout TextLayoutManager::prepareLayout( const ParagraphAttributes& paragraphAttributes, const TextLayoutContext& layoutContext, const LayoutConstraints& layoutConstraints) const { - const auto& fabricUIManager = - contextContainer_->at>("FabricUIManager"); - static auto prepareTextLayout = jni::findClassStatic("com/facebook/react/fabric/FabricUIManager") ->getMethod("prepareTextLayout"); - auto attributedStringMB = - JReadableMapBuffer::createWithContents(toMapBuffer(attributedString)); - auto paragraphAttributesMB = - JReadableMapBuffer::createWithContents(toMapBuffer(paragraphAttributes)); + return preparedTextCache_.get( + {.attributedString = attributedString, + .paragraphAttributes = paragraphAttributes, + .layoutConstraints = layoutConstraints}, + [&] { + const auto& fabricUIManager = + contextContainer_->at>("FabricUIManager"); - auto minimumSize = layoutConstraints.minimumSize; - auto maximumSize = layoutConstraints.maximumSize; + auto attributedStringMB = JReadableMapBuffer::createWithContents( + toMapBuffer(attributedString)); + auto paragraphAttributesMB = JReadableMapBuffer::createWithContents( + toMapBuffer(paragraphAttributes)); - // T222682416: We don't have any global cache here. We should investigate - // whether that is desirable - return {jni::make_global(prepareTextLayout( - fabricUIManager, - layoutContext.surfaceId, - attributedStringMB.get(), - paragraphAttributesMB.get(), - minimumSize.width, - maximumSize.width, - minimumSize.height, - maximumSize.height))}; + auto minimumSize = layoutConstraints.minimumSize; + auto maximumSize = layoutConstraints.maximumSize; + + return PreparedLayout{jni::make_global(prepareTextLayout( + fabricUIManager, + layoutContext.surfaceId, + attributedStringMB.get(), + paragraphAttributesMB.get(), + minimumSize.width, + maximumSize.width, + minimumSize.height, + maximumSize.height))}; + }); } TextMeasurement TextLayoutManager::measurePreparedLayout( diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h index 249db1eba11..71ab0488614 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h @@ -95,6 +95,11 @@ class TextLayoutManager { std::shared_ptr contextContainer_; TextMeasureCache textMeasureCache_; LineMeasureCache lineMeasureCache_; + SimpleThreadSafeCache< + PreparedTextCacheKey, + PreparedLayout, + -1 /* Set dynamically*/> + preparedTextCache_; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/utils/SimpleThreadSafeCache.h b/packages/react-native/ReactCommon/react/utils/SimpleThreadSafeCache.h index 67468619ad9..3dc71ac2810 100644 --- a/packages/react-native/ReactCommon/react/utils/SimpleThreadSafeCache.h +++ b/packages/react-native/ReactCommon/react/utils/SimpleThreadSafeCache.h @@ -20,6 +20,9 @@ concept CacheGeneratorFunction = std::invocable && /* * Simple thread-safe LRU cache. + * + * TODO T228961279: The maxSize template parameter should be removed, since it + * may be overriden by the constructor. */ template class SimpleThreadSafeCache { diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index ec833899d12..1a549e2e4e6 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -511,6 +511,16 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + preparedTextCacheSize: { + defaultValue: 200, + metadata: { + dateAdded: '2025-06-25', + description: 'Number cached PreparedLayouts in TextLayoutManager cache', + expectedReleaseValue: 200, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, traceTurboModulePromiseRejectionsOnAndroid: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index a2459528371..211a1c4d67d 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<<194e0f47db33d3b9167f58ed186a331c>> + * @generated SignedSource<<731dbe289a0c7cf5bec2c8033150ebb9>> * @flow strict * @noformat */ @@ -94,6 +94,7 @@ export type ReactNativeFeatureFlags = $ReadOnly<{ fixMappingOfEventPrioritiesBetweenFabricAndReact: Getter, fuseboxEnabledRelease: Getter, fuseboxNetworkInspectionEnabled: Getter, + preparedTextCacheSize: Getter, traceTurboModulePromiseRejectionsOnAndroid: Getter, updateRuntimeShadowNodeReferencesOnCommit: Getter, useAlwaysAvailableJSErrorHandling: Getter, @@ -362,6 +363,10 @@ export const fuseboxEnabledRelease: Getter = createNativeFlagGetter('fu * Enable network inspection support in the React Native DevTools CDP backend. Requires `enableBridgelessArchitecture`. This flag is global and should not be changed across React Host lifetimes. */ export const fuseboxNetworkInspectionEnabled: Getter = createNativeFlagGetter('fuseboxNetworkInspectionEnabled', false); +/** + * Number cached PreparedLayouts in TextLayoutManager cache + */ +export const preparedTextCacheSize: Getter = createNativeFlagGetter('preparedTextCacheSize', 200); /** * Enables storing js caller stack when creating promise in native module. This is useful in case of Promise rejection and tracing the cause. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index cb8a470712b..08d3f9a3bb0 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<<543998b20611a8f6adb0840784d1a5a7>> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -69,6 +69,7 @@ export interface Spec extends TurboModule { +fixMappingOfEventPrioritiesBetweenFabricAndReact?: () => boolean; +fuseboxEnabledRelease?: () => boolean; +fuseboxNetworkInspectionEnabled?: () => boolean; + +preparedTextCacheSize?: () => number; +traceTurboModulePromiseRejectionsOnAndroid?: () => boolean; +updateRuntimeShadowNodeReferencesOnCommit?: () => boolean; +useAlwaysAvailableJSErrorHandling?: () => boolean;