From 93ce46113cf216f831448b1343caefb59f46e313 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Tue, 9 May 2023 15:25:38 -0700 Subject: [PATCH] Combine iOS and Android optimisations (#37317) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/37317 changelog: [internal] This diff does three things: 1. Combines Android's mobile config `react_fabric:enable_text_measure_cache` with iOS mobile config `react_fabric:enable_nstextstorage_caching`. We will get into why later. 2. Fixes cache `ParagraphLayoutManager::cachedTextMeasurement_` invalidation logic for iOS and Android. 3. Fixes cache invalidation logic for `ParagraphLayoutManager::hostTextStorage_`. Initially, Android's text measure cache (D44221170) and iOS's NSTextStorage (D43692171) were design as two separate optimisations. But they overlap and NSTextStorage actually needs some parts of text measure cache to work correctly. That's why I decided to merge them together. Previously, `ParagraphLayoutManager::cachedTextMeasurement_` was only invalidated if maximum width for the node changed. But node can change in different ways, for example input string changes or attributes change. This diff accounts for that by computing a hash for AttributedString+ParagraphAttributes to check if anything has changed. Previously, `ParagraphLayoutManager::hostTextStorage_` was not correctly invalidated if maximum width changed. To my surprise, this happens less frequently than one expects. bypass-github-export-checks Reviewed By: mdvacca Differential Revision: D45637797 fbshipit-source-id: 1840b51cdb1423de0560a35fd17c24a71ad115e1 --- .../React/Fabric/RCTSurfacePresenter.mm | 4 - .../react/config/ReactFeatureFlags.java | 3 - .../src/main/jni/react/fabric/Binding.cpp | 2 - .../text/ParagraphLayoutManager.cpp | 78 +++++++++++-------- .../components/text/ParagraphLayoutManager.h | 29 ++++++- .../react/renderer/core/CoreFeatures.cpp | 1 - .../react/renderer/core/CoreFeatures.h | 6 +- .../react/renderer/scheduler/Scheduler.cpp | 3 + 8 files changed, 74 insertions(+), 52 deletions(-) diff --git a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm index d86ce029407..77bc364889f 100644 --- a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm +++ b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm @@ -281,10 +281,6 @@ static BackgroundExecutor RCTGetBackgroundExecutor() CoreFeatures::useNativeState = true; } - if (reactNativeConfig && reactNativeConfig->getBool("react_fabric:enable_nstextstorage_caching")) { - CoreFeatures::cacheNSTextStorage = true; - } - if (reactNativeConfig && reactNativeConfig->getBool("react_fabric:cancel_image_downloads_on_recycle")) { CoreFeatures::cancelImageDownloadsOnRecycle = true; } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java index 05eccce40f4..3ee10204917 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java @@ -97,9 +97,6 @@ public class ReactFeatureFlags { /** Feature Flag to enable the pending event queue in fabric before mounting views */ public static boolean enableFabricPendingEventQueue = false; - /** Feature Flag to enable caching mechanism of text measurement at shadow node level */ - public static boolean enableTextMeasureCachePerShadowNode = false; - /** * Feature flag that controls how turbo modules are exposed to JS * diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp index 08d1fa767ee..e8139b5380f 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp @@ -421,8 +421,6 @@ void Binding::installFabricUIManager( "CalculateTransformedFramesEnabled", getFeatureFlagValue("calculateTransformedFramesEnabled")); - CoreFeatures::cacheLastTextMeasurement = - getFeatureFlagValue("enableTextMeasureCachePerShadowNode"); CoreFeatures::enablePropIteratorSetter = getFeatureFlagValue("enableCppPropsIteratorSetter"); CoreFeatures::useNativeState = getFeatureFlagValue("useNativeState"); diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.cpp index 1f4cbc4a497..0b622be5798 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.cpp @@ -7,6 +7,7 @@ #include "ParagraphLayoutManager.h" #include +#include #include namespace facebook::react { @@ -15,40 +16,18 @@ TextMeasurement ParagraphLayoutManager::measure( AttributedString const &attributedString, ParagraphAttributes const ¶graphAttributes, LayoutConstraints layoutConstraints) const { - bool cacheLastTextMeasurement = CoreFeatures::cacheLastTextMeasurement; - if (cacheLastTextMeasurement && - (layoutConstraints.maximumSize.width == availableWidth_ || - layoutConstraints.maximumSize.width == - cachedTextMeasurement_.size.width)) { - /* Yoga has requested measurement for this size before. Let's use cached - * value. `TextLayoutManager` might not have cached this because it could be - * using different width to generate cache key. This happens because Yoga - * switches between available width and exact width but since we already - * know exact width, it is wasteful to calculate it again. - */ - return cachedTextMeasurement_; - } - if (CoreFeatures::cacheNSTextStorage) { - size_t newHash = folly::hash::hash_combine( - 0, - textAttributedStringHashLayoutWise(attributedString), - paragraphAttributes); + if (CoreFeatures::cacheLastTextMeasurement) { + bool shouldMeasure = shoudMeasureString( + attributedString, paragraphAttributes, layoutConstraints); - if (!hostTextStorage_ || newHash != hash_) { - hostTextStorage_ = textLayoutManager_->getHostTextStorage( - attributedString, paragraphAttributes, layoutConstraints); - hash_ = newHash; + if (shouldMeasure) { + cachedTextMeasurement_ = textLayoutManager_->measure( + AttributedStringBox(attributedString), + paragraphAttributes, + layoutConstraints, + hostTextStorage_); + lastAvailableWidth_ = layoutConstraints.maximumSize.width; } - } - - if (cacheLastTextMeasurement) { - cachedTextMeasurement_ = textLayoutManager_->measure( - AttributedStringBox(attributedString), - paragraphAttributes, - layoutConstraints, - hostTextStorage_); - - availableWidth_ = layoutConstraints.maximumSize.width; return cachedTextMeasurement_; } else { @@ -56,10 +35,43 @@ TextMeasurement ParagraphLayoutManager::measure( AttributedStringBox(attributedString), paragraphAttributes, layoutConstraints, - hostTextStorage_); + nullptr); } } +bool ParagraphLayoutManager::shoudMeasureString( + AttributedString const &attributedString, + ParagraphAttributes const ¶graphAttributes, + LayoutConstraints layoutConstraints) const { + size_t newHash = folly::hash::hash_combine( + 0, + textAttributedStringHashLayoutWise(attributedString), + paragraphAttributes); + + if (newHash != paragraphInputHash_) { + // AttributedString or ParagraphAttributes have changed. + // Must create new host text storage and trigger measure. + hostTextStorage_ = textLayoutManager_->getHostTextStorage( + attributedString, paragraphAttributes, layoutConstraints); + paragraphInputHash_ = newHash; + return true; // Must measure again. + } + + bool hasMaximumSizeChanged = + layoutConstraints.maximumSize.width != lastAvailableWidth_; + Float threshold = 0.01; + bool doesMaximumSizeMatchLastMeasurement = + std::abs( + layoutConstraints.maximumSize.width - + cachedTextMeasurement_.size.width) < threshold; + if (hasMaximumSizeChanged && !doesMaximumSizeMatchLastMeasurement) { + hostTextStorage_ = textLayoutManager_->getHostTextStorage( + attributedString, paragraphAttributes, layoutConstraints); + return true; + } + return false; +} + LinesMeasurements ParagraphLayoutManager::measureLines( AttributedString const &attributedString, ParagraphAttributes const ¶graphAttributes, diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.h b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.h index d262db62b8c..9583aae501b 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.h +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.h @@ -52,20 +52,41 @@ class ParagraphLayoutManager { private: std::shared_ptr mutable textLayoutManager_{}; + + /* + * Stores opaque pointer to `NSTextStorage` on iOS. nullptr on Android. + * TODO: In the future, we may want to cache Android's text storage. + */ std::shared_ptr mutable hostTextStorage_{}; + /* + * Hash of AttributedString and ParagraphAttributes last used to + * measure. Result of that measure is stored in cachedTextMeasurement_. + * The available width defined for the measurement is stored in + * lastAvailableWidth_. + */ + size_t mutable paragraphInputHash_{}; + /* The width Yoga set as maximum width. - * Yoga sometimes calls measure twice with two - * different maximum width. One if available space. + * Yoga calls measure twice with two + * different maximum width. One of available space. * The other one is exact space needed for the string. * This happens when node is dirtied but its size is not affected. * To deal with this inefficiency, we cache `TextMeasurement` for each * `ParagraphShadowNode`. If Yoga tries to re-measure with available width * or exact width, we provide it with the cached value. */ - Float mutable availableWidth_{}; + Float mutable lastAvailableWidth_{}; TextMeasurement mutable cachedTextMeasurement_{}; - size_t mutable hash_{}; + /* + * Checks whether the inputs into text measurement meaningfully affect + * text measurement result. Returns true if inputs have changed and measure is + * needed. + */ + bool shoudMeasureString( + AttributedString const &attributedString, + ParagraphAttributes const ¶graphAttributes, + LayoutConstraints layoutConstraints) const; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.cpp b/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.cpp index 5f0abf2f4fc..937e40aa0bf 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.cpp @@ -13,7 +13,6 @@ bool CoreFeatures::enablePropIteratorSetter = false; bool CoreFeatures::enableMapBuffer = false; bool CoreFeatures::blockPaintForUseLayoutEffect = false; bool CoreFeatures::useNativeState = false; -bool CoreFeatures::cacheNSTextStorage = false; bool CoreFeatures::cacheLastTextMeasurement = false; bool CoreFeatures::cancelImageDownloadsOnRecycle = false; diff --git a/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.h b/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.h index 36d31cd18ef..5c49928e226 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.h +++ b/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.h @@ -34,14 +34,10 @@ class CoreFeatures { // in simple data passing scenarios with JS static bool useNativeState; - // Creating NSTextStorage is relatively expensive operation and we were - // creating it twice. Once when measuring text and once when rendering it. - // This flag caches it inside ParagraphState. - static bool cacheNSTextStorage; - // Yoga might measure multiple times the same Text with the same constraints // This flag enables a caching mechanism to avoid subsequents measurements // of the same Text with the same constrainst. + // On iOS, we also cache NSTextStorage. static bool cacheLastTextMeasurement; // Fabric was not cancelling image downloads when was removed diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp index 90d558985b9..f800ed581a6 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp @@ -131,6 +131,9 @@ Scheduler::Scheduler( CoreFeatures::blockPaintForUseLayoutEffect = reactNativeConfig_->getBool( "react_fabric:block_paint_for_use_layout_effect"); + CoreFeatures::cacheLastTextMeasurement = + reactNativeConfig_->getBool("react_fabric:enable_text_measure_cache"); + if (animationDelegate != nullptr) { animationDelegate->setComponentDescriptorRegistry( componentDescriptorRegistry_);