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
This commit is contained in:
Samuel Susla
2023-05-09 15:25:38 -07:00
committed by Facebook GitHub Bot
parent a941d067f5
commit 93ce46113c
8 changed files with 74 additions and 52 deletions
@@ -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;
}
@@ -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
*
@@ -421,8 +421,6 @@ void Binding::installFabricUIManager(
"CalculateTransformedFramesEnabled",
getFeatureFlagValue("calculateTransformedFramesEnabled"));
CoreFeatures::cacheLastTextMeasurement =
getFeatureFlagValue("enableTextMeasureCachePerShadowNode");
CoreFeatures::enablePropIteratorSetter =
getFeatureFlagValue("enableCppPropsIteratorSetter");
CoreFeatures::useNativeState = getFeatureFlagValue("useNativeState");
@@ -7,6 +7,7 @@
#include "ParagraphLayoutManager.h"
#include <folly/Hash.h>
#include <glog/logging.h>
#include <react/renderer/core/CoreFeatures.h>
namespace facebook::react {
@@ -15,40 +16,18 @@ TextMeasurement ParagraphLayoutManager::measure(
AttributedString const &attributedString,
ParagraphAttributes const &paragraphAttributes,
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 &paragraphAttributes,
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 &paragraphAttributes,
@@ -52,20 +52,41 @@ class ParagraphLayoutManager {
private:
std::shared_ptr<TextLayoutManager const> 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<void> 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 &paragraphAttributes,
LayoutConstraints layoutConstraints) const;
};
} // namespace facebook::react
@@ -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;
@@ -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 <ImageView /> was removed
@@ -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_);