Improve caching of Text per shadow Node

Summary:
This diff optimizes text measurement by adding a layer of caching per shadow node.
When ReactFeatureFlags.enableTextMeasureCachePerShadowNode is enabled, we will cache and reused measurements per shadow node.
This optimization showed a 2x improvement on text rendearing when a screen contains a big amount of text components.

Changelog: [Internal] Internal

Reviewed By: sammy-SC

Differential Revision: D44221170

fbshipit-source-id: c3e7ba1ad216929826a99585874981717c90e13b
This commit is contained in:
David Vacca
2023-03-20 12:13:02 -07:00
committed by Lorenzo Sciandra
parent 05ba16aa3d
commit f5fbda5e46
7 changed files with 59 additions and 6 deletions
@@ -75,6 +75,9 @@ 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
*
@@ -429,6 +429,9 @@ void Binding::installFabricUIManager(
"CalculateTransformedFramesEnabled",
getFeatureFlagValue("calculateTransformedFramesEnabled"));
CoreFeatures::cacheLastTextMeasurement =
getFeatureFlagValue("enableTextMeasureCachePerShadowNode");
// Props setter pattern feature
CoreFeatures::enablePropIteratorSetter =
getFeatureFlagValue("enableCppPropsIteratorSetter");
@@ -15,6 +15,19 @@ 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,
@@ -28,11 +41,23 @@ TextMeasurement ParagraphLayoutManager::measure(
}
}
return textLayoutManager_->measure(
AttributedStringBox(attributedString),
paragraphAttributes,
layoutConstraints,
hostTextStorage_);
if (cacheLastTextMeasurement) {
cachedTextMeasurement_ = textLayoutManager_->measure(
AttributedStringBox(attributedString),
paragraphAttributes,
layoutConstraints,
hostTextStorage_);
availableWidth_ = layoutConstraints.maximumSize.width;
return cachedTextMeasurement_;
} else {
return textLayoutManager_->measure(
AttributedStringBox(attributedString),
paragraphAttributes,
layoutConstraints,
hostTextStorage_);
}
}
LinesMeasurements ParagraphLayoutManager::measureLines(
@@ -54,6 +54,18 @@ class ParagraphLayoutManager {
std::shared_ptr<TextLayoutManager const> mutable textLayoutManager_{};
std::shared_ptr<void> mutable hostTextStorage_{};
/* The width Yoga set as maximum width.
* Yoga sometimes calls measure twice with two
* different maximum width. One if 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_{};
TextMeasurement mutable cachedTextMeasurement_{};
size_t mutable hash_{};
};
} // namespace facebook::react
@@ -15,6 +15,7 @@ bool CoreFeatures::enableMapBuffer = false;
bool CoreFeatures::blockPaintForUseLayoutEffect = false;
bool CoreFeatures::useNativeState = false;
bool CoreFeatures::cacheNSTextStorage = false;
bool CoreFeatures::cacheLastTextMeasurement = false;
} // namespace react
} // namespace facebook
@@ -39,6 +39,11 @@ class CoreFeatures {
// 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.
static bool cacheLastTextMeasurement;
};
} // namespace react
@@ -12,6 +12,7 @@
#include <react/common/mapbuffer/JReadableMapBuffer.h>
#include <react/jni/ReadableNativeMap.h>
#include <react/renderer/attributedstring/conversions.h>
#include <react/renderer/core/CoreFeatures.h>
#include <react/renderer/core/conversions.h>
#include <react/renderer/mapbuffer/MapBuffer.h>
#include <react/renderer/mapbuffer/MapBufferBuilder.h>
@@ -146,7 +147,10 @@ Size measureAndroidComponentMapBuffer(
TextLayoutManager::TextLayoutManager(
const ContextContainer::Shared &contextContainer)
: contextContainer_(contextContainer),
measureCache_(kSimpleThreadSafeCacheSizeCap) {}
measureCache_(
CoreFeatures::cacheLastTextMeasurement
? 8096
: kSimpleThreadSafeCacheSizeCap) {}
void *TextLayoutManager::getNativeTextLayoutManager() const {
return self_;