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
This commit is contained in:
Nick Gerleman
2025-06-27 14:37:33 -07:00
committed by Facebook GitHub Bot
parent d96bbcdf0c
commit 2d1db71bc0
24 changed files with 263 additions and 62 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<<f5a7da2d8775ac1defa4037efd984504>>
* @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.
*/
@@ -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) {
@@ -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
@@ -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<<a1eb592bf34ec0bd242c44a2f6bb6828>>
*/
/**
@@ -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
@@ -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<<cdd5ac40322e76f95cc8831d30012375>>
*/
/**
@@ -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) {
@@ -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
@@ -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<jdouble()>("preparedTextCacheSize");
return method(javaProvider_);
}
bool traceTurboModulePromiseRejectionsOnAndroid() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("traceTurboModulePromiseRejectionsOnAndroid");
@@ -593,6 +599,11 @@ bool JReactNativeFeatureFlagsCxxInterop::fuseboxNetworkInspectionEnabled(
return ReactNativeFeatureFlags::fuseboxNetworkInspectionEnabled();
}
double JReactNativeFeatureFlagsCxxInterop::preparedTextCacheSize(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::preparedTextCacheSize();
}
bool JReactNativeFeatureFlagsCxxInterop::traceTurboModulePromiseRejectionsOnAndroid(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::traceTurboModulePromiseRejectionsOnAndroid();
@@ -811,6 +822,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
makeNativeMethod(
"fuseboxNetworkInspectionEnabled",
JReactNativeFeatureFlagsCxxInterop::fuseboxNetworkInspectionEnabled),
makeNativeMethod(
"preparedTextCacheSize",
JReactNativeFeatureFlagsCxxInterop::preparedTextCacheSize),
makeNativeMethod(
"traceTurboModulePromiseRejectionsOnAndroid",
JReactNativeFeatureFlagsCxxInterop::traceTurboModulePromiseRejectionsOnAndroid),
@@ -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<JReactNativeFeatureFlagsCxxInterop>);
static double preparedTextCacheSize(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
static bool traceTurboModulePromiseRejectionsOnAndroid(
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<<e95ca3707afa85dbcd17a351872da3b3>>
* @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();
}
@@ -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<<d724d9cfe69141b40f95df5308894f50>>
*/
/**
@@ -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.
*/
@@ -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;
@@ -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<<f71c6e5c0c6ac39842ed570cbe560e49>>
* @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<ReactNativeFeatureFlagsProvider> currentProvider_;
bool wasOverridden_;
std::array<std::atomic<const char*>, 55> accessedFeatureFlags_;
std::array<std::atomic<const char*>, 56> accessedFeatureFlags_;
std::atomic<std::optional<bool>> commonTestFlag_;
std::atomic<std::optional<bool>> animatedShouldSignalBatch_;
@@ -144,6 +145,7 @@ class ReactNativeFeatureFlagsAccessor {
std::atomic<std::optional<bool>> fixMappingOfEventPrioritiesBetweenFabricAndReact_;
std::atomic<std::optional<bool>> fuseboxEnabledRelease_;
std::atomic<std::optional<bool>> fuseboxNetworkInspectionEnabled_;
std::atomic<std::optional<double>> preparedTextCacheSize_;
std::atomic<std::optional<bool>> traceTurboModulePromiseRejectionsOnAndroid_;
std::atomic<std::optional<bool>> updateRuntimeShadowNodeReferencesOnCommit_;
std::atomic<std::optional<bool>> useAlwaysAvailableJSErrorHandling_;
@@ -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;
}
@@ -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()) {
@@ -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<<fd48bf5f21c38d6a16ab54c57e2b5817>>
* @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;
@@ -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<<b8b4c10e83f30eb416dd14304ca973f8>>
* @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();
@@ -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<<db43e2975566cc6f34af1a5337b383ee>>
*/
/**
@@ -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);
@@ -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<facebook::react::LineMeasureCacheKey> {
}
};
template <>
struct hash<facebook::react::PreparedTextCacheKey> {
size_t operator()(const facebook::react::PreparedTextCacheKey& key) const {
return facebook::react::hash_combine(
attributedStringHashDisplayWise(key.attributedString),
key.paragraphAttributes,
key.layoutConstraints);
}
};
} // namespace std
@@ -158,7 +158,9 @@ TextLayoutManager::TextLayoutManager(
const ContextContainer::Shared& contextContainer)
: contextContainer_(contextContainer),
textMeasureCache_(kSimpleThreadSafeCacheSizeCap),
lineMeasureCache_(kSimpleThreadSafeCacheSizeCap) {}
lineMeasureCache_(kSimpleThreadSafeCacheSizeCap),
preparedTextCache_(static_cast<size_t>(
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<jni::global_ref<jobject>>("FabricUIManager");
static auto prepareTextLayout =
jni::findClassStatic("com/facebook/react/fabric/FabricUIManager")
->getMethod<JPreparedLayout::javaobject(
@@ -311,25 +310,32 @@ TextLayoutManager::PreparedLayout TextLayoutManager::prepareLayout(
jfloat,
jfloat)>("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<jni::global_ref<jobject>>("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(
@@ -95,6 +95,11 @@ class TextLayoutManager {
std::shared_ptr<const ContextContainer> contextContainer_;
TextMeasureCache textMeasureCache_;
LineMeasureCache lineMeasureCache_;
SimpleThreadSafeCache<
PreparedTextCacheKey,
PreparedLayout,
-1 /* Set dynamically*/>
preparedTextCache_;
};
} // namespace facebook::react
@@ -20,6 +20,9 @@ concept CacheGeneratorFunction = std::invocable<GeneratorT> &&
/*
* Simple thread-safe LRU cache.
*
* TODO T228961279: The maxSize template parameter should be removed, since it
* may be overriden by the constructor.
*/
template <typename KeyT, typename ValueT, int maxSize>
class SimpleThreadSafeCache {
@@ -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: {
@@ -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<boolean>,
fuseboxEnabledRelease: Getter<boolean>,
fuseboxNetworkInspectionEnabled: Getter<boolean>,
preparedTextCacheSize: Getter<number>,
traceTurboModulePromiseRejectionsOnAndroid: Getter<boolean>,
updateRuntimeShadowNodeReferencesOnCommit: Getter<boolean>,
useAlwaysAvailableJSErrorHandling: Getter<boolean>,
@@ -362,6 +363,10 @@ export const fuseboxEnabledRelease: Getter<boolean> = 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<boolean> = createNativeFlagGetter('fuseboxNetworkInspectionEnabled', false);
/**
* Number cached PreparedLayouts in TextLayoutManager cache
*/
export const preparedTextCacheSize: Getter<number> = 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.
*/
@@ -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<<d2194efcd4c936cd2820fc028ce1733c>>
* @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;