From a9049442f72ba1c9fa562a91ff21b925c028195e Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Fri, 25 Jan 2019 16:55:06 -0800 Subject: [PATCH] Fabric: Use LRU to cache results of ParagraphShadowNode::measure Summary: Use a folly LRU implementation to cache results of ParagraphShadowNode::measure, which Yoga asks for repeatedly. Should have a substantial speed improvement on Android and iOS, or at least that's the dream. Reviewed By: mdvacca Differential Revision: D13795808 fbshipit-source-id: 5716af0fe0517a72716e48113c8125bb788735d7 --- .../attributedstring/AttributedString.h | 20 +++---- ReactCommon/fabric/attributedstring/BUCK | 1 + .../attributedstring/ParagraphAttributes.cpp | 15 +++++ .../attributedstring/ParagraphAttributes.h | 23 ++++++++ .../attributedstring/TextAttributes.cpp | 21 +++---- ReactCommon/fabric/components/text/BUCK | 2 + .../paragraph/ParagraphComponentDescriptor.h | 14 ++++- .../paragraph/ParagraphMeasurementCache.h | 59 +++++++++++++++++++ .../text/paragraph/ParagraphShadowNode.cpp | 29 +++++++-- .../text/paragraph/ParagraphShadowNode.h | 9 +++ ReactCommon/fabric/core/BUCK | 1 + .../fabric/core/layout/LayoutConstraints.h | 23 ++++++++ ReactCommon/fabric/mounting/ShadowView.h | 2 +- ReactCommon/utils/BUCK | 27 +++++++++ ReactCommon/utils/FloatComparison.h | 18 ++++++ 15 files changed, 234 insertions(+), 30 deletions(-) create mode 100644 ReactCommon/fabric/components/text/paragraph/ParagraphMeasurementCache.h create mode 100644 ReactCommon/utils/BUCK create mode 100644 ReactCommon/utils/FloatComparison.h diff --git a/ReactCommon/fabric/attributedstring/AttributedString.h b/ReactCommon/fabric/attributedstring/AttributedString.h index e1aa5d832..5e09cd34e 100644 --- a/ReactCommon/fabric/attributedstring/AttributedString.h +++ b/ReactCommon/fabric/attributedstring/AttributedString.h @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -89,12 +90,12 @@ template <> struct hash { size_t operator()( const facebook::react::AttributedString::Fragment &fragment) const { - return std::hash{}(fragment.string) + - std::hash{}( - fragment.textAttributes) + - std::hash{}(fragment.shadowView) + - std::hash{}( - fragment.parentShadowView); + auto seed = size_t{0}; + folly::hash::hash_combine(seed, fragment.string); + folly::hash::hash_combine(seed, fragment.textAttributes); + folly::hash::hash_combine(seed, fragment.shadowView); + folly::hash::hash_combine(seed, fragment.parentShadowView); + return seed; } }; @@ -102,14 +103,13 @@ template <> struct hash { size_t operator()( const facebook::react::AttributedString &attributedString) const { - auto result = size_t{0}; + auto seed = size_t{0}; for (const auto &fragment : attributedString.getFragments()) { - result += - std::hash{}(fragment); + folly::hash::hash_combine(seed, fragment); } - return result; + return seed; } }; } // namespace std diff --git a/ReactCommon/fabric/attributedstring/BUCK b/ReactCommon/fabric/attributedstring/BUCK index 5fb128651..21620481d 100644 --- a/ReactCommon/fabric/attributedstring/BUCK +++ b/ReactCommon/fabric/attributedstring/BUCK @@ -56,6 +56,7 @@ rn_xplat_cxx_library( "xplat//folly:memory", "xplat//folly:molly", "xplat//third-party/glog:glog", + react_native_xplat_target("utils:utils"), react_native_xplat_target("fabric/debug:debug"), react_native_xplat_target("fabric/core:core"), react_native_xplat_target("fabric/graphics:graphics"), diff --git a/ReactCommon/fabric/attributedstring/ParagraphAttributes.cpp b/ReactCommon/fabric/attributedstring/ParagraphAttributes.cpp index 00f216f9d..e4910077d 100644 --- a/ReactCommon/fabric/attributedstring/ParagraphAttributes.cpp +++ b/ReactCommon/fabric/attributedstring/ParagraphAttributes.cpp @@ -10,10 +10,25 @@ #include #include #include +#include namespace facebook { namespace react { +bool ParagraphAttributes::operator==(const ParagraphAttributes &rhs) const { + return std::tie(maximumNumberOfLines, ellipsizeMode, adjustsFontSizeToFit) == + std::tie( + rhs.maximumNumberOfLines, + rhs.ellipsizeMode, + rhs.adjustsFontSizeToFit) && + floatEquality(minimumFontSize, rhs.minimumFontSize) && + floatEquality(maximumFontSize, rhs.maximumFontSize); +} + +bool ParagraphAttributes::operator!=(const ParagraphAttributes &rhs) const { + return !(*this == rhs); +} + #pragma mark - DebugStringConvertible #if RN_DEBUG_STRING_CONVERTIBLE diff --git a/ReactCommon/fabric/attributedstring/ParagraphAttributes.h b/ReactCommon/fabric/attributedstring/ParagraphAttributes.h index 2c9a0dc71..36e645e5d 100644 --- a/ReactCommon/fabric/attributedstring/ParagraphAttributes.h +++ b/ReactCommon/fabric/attributedstring/ParagraphAttributes.h @@ -9,6 +9,7 @@ #include +#include #include #include #include @@ -53,6 +54,9 @@ class ParagraphAttributes : public DebugStringConvertible { Float minimumFontSize{std::numeric_limits::quiet_NaN()}; Float maximumFontSize{std::numeric_limits::quiet_NaN()}; + bool operator==(const ParagraphAttributes &) const; + bool operator!=(const ParagraphAttributes &) const; + #pragma mark - DebugStringConvertible #if RN_DEBUG_STRING_CONVERTIBLE @@ -62,3 +66,22 @@ class ParagraphAttributes : public DebugStringConvertible { } // namespace react } // namespace facebook + +namespace std { + +template <> +struct hash { + size_t operator()( + const facebook::react::ParagraphAttributes &attributes) const { + size_t seed = 0; + folly::hash::hash_combine(seed, attributes.maximumNumberOfLines); + folly::hash::hash_combine(seed, attributes.ellipsizeMode); + folly::hash::hash_combine(seed, attributes.adjustsFontSizeToFit); + folly::hash::hash_combine( + seed, std::hash{}(attributes.minimumFontSize)); + folly::hash::hash_combine( + seed, std::hash{}(attributes.maximumFontSize)); + return hash()(seed); + } +}; +} // namespace std diff --git a/ReactCommon/fabric/attributedstring/TextAttributes.cpp b/ReactCommon/fabric/attributedstring/TextAttributes.cpp index 480dd012e..5016c9ce0 100644 --- a/ReactCommon/fabric/attributedstring/TextAttributes.cpp +++ b/ReactCommon/fabric/attributedstring/TextAttributes.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -101,16 +102,11 @@ bool TextAttributes::operator==(const TextAttributes &rhs) const { return std::tie( foregroundColor, backgroundColor, - opacity, fontFamily, - fontSize, - fontSizeMultiplier, fontWeight, fontStyle, fontVariant, allowFontScaling, - letterSpacing, - lineHeight, alignment, baseWritingDirection, textDecorationColor, @@ -118,23 +114,17 @@ bool TextAttributes::operator==(const TextAttributes &rhs) const { textDecorationLineStyle, textDecorationLinePattern, textShadowOffset, - textShadowRadius, textShadowColor, isHighlighted, layoutDirection) == std::tie( rhs.foregroundColor, rhs.backgroundColor, - rhs.opacity, rhs.fontFamily, - rhs.fontSize, - rhs.fontSizeMultiplier, rhs.fontWeight, rhs.fontStyle, rhs.fontVariant, rhs.allowFontScaling, - rhs.letterSpacing, - rhs.lineHeight, rhs.alignment, rhs.baseWritingDirection, rhs.textDecorationColor, @@ -142,10 +132,15 @@ bool TextAttributes::operator==(const TextAttributes &rhs) const { rhs.textDecorationLineStyle, rhs.textDecorationLinePattern, rhs.textShadowOffset, - rhs.textShadowRadius, rhs.textShadowColor, rhs.isHighlighted, - rhs.layoutDirection); + rhs.layoutDirection) && + floatEquality(opacity, rhs.opacity) && + floatEquality(fontSize, rhs.fontSize) && + floatEquality(fontSizeMultiplier, rhs.fontSizeMultiplier) && + floatEquality(letterSpacing, rhs.letterSpacing) && + floatEquality(lineHeight, rhs.lineHeight) && + floatEquality(textShadowRadius, rhs.textShadowRadius); } bool TextAttributes::operator!=(const TextAttributes &rhs) const { diff --git a/ReactCommon/fabric/components/text/BUCK b/ReactCommon/fabric/components/text/BUCK index dff38f26b..fd290c174 100644 --- a/ReactCommon/fabric/components/text/BUCK +++ b/ReactCommon/fabric/components/text/BUCK @@ -56,11 +56,13 @@ rn_xplat_cxx_library( visibility = ["PUBLIC"], deps = [ "xplat//fbsystrace:fbsystrace", + "xplat//folly:evicting_cache_map", "xplat//folly:headers_only", "xplat//folly:memory", "xplat//folly:molly", "xplat//third-party/glog:glog", "xplat//yoga:yoga", + react_native_xplat_target("utils:utils"), react_native_xplat_target("fabric/attributedstring:attributedstring"), react_native_xplat_target("fabric/core:core"), react_native_xplat_target("fabric/debug:debug"), diff --git a/ReactCommon/fabric/components/text/paragraph/ParagraphComponentDescriptor.h b/ReactCommon/fabric/components/text/paragraph/ParagraphComponentDescriptor.h index 9944483a7..40f9e111b 100644 --- a/ReactCommon/fabric/components/text/paragraph/ParagraphComponentDescriptor.h +++ b/ReactCommon/fabric/components/text/paragraph/ParagraphComponentDescriptor.h @@ -7,7 +7,10 @@ #pragma once -#include +#include "ParagraphMeasurementCache.h" +#include "ParagraphShadowNode.h" + +#include #include #include #include @@ -28,6 +31,10 @@ class ParagraphComponentDescriptor final // Every single `ParagraphShadowNode` will have a reference to // a shared `TextLayoutManager`. textLayoutManager_ = std::make_shared(contextContainer); + // Every single `ParagraphShadowNode` will have a reference to + // a shared `EvictingCacheMap`, a simple LRU cache for Paragraph + // measurements. + measureCache_ = std::make_shared(); } void adopt(UnsharedShadowNode shadowNode) const override { @@ -41,6 +48,10 @@ class ParagraphComponentDescriptor final // and communicate text rendering metrics to mounting layer. paragraphShadowNode->setTextLayoutManager(textLayoutManager_); + // `ParagraphShadowNode` uses this to cache the results of text rendering + // measurements. + paragraphShadowNode->setMeasureCache(measureCache_); + // All `ParagraphShadowNode`s must have leaf Yoga nodes with properly // setup measure function. paragraphShadowNode->enableMeasurement(); @@ -48,6 +59,7 @@ class ParagraphComponentDescriptor final private: SharedTextLayoutManager textLayoutManager_; + SharedParagraphMeasurementCache measureCache_; }; } // namespace react diff --git a/ReactCommon/fabric/components/text/paragraph/ParagraphMeasurementCache.h b/ReactCommon/fabric/components/text/paragraph/ParagraphMeasurementCache.h new file mode 100644 index 000000000..361c2aac1 --- /dev/null +++ b/ReactCommon/fabric/components/text/paragraph/ParagraphMeasurementCache.h @@ -0,0 +1,59 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include +#include +#include + +namespace facebook { +namespace react { +using ParagraphMeasurementCacheKey = + std::tuple; +using ParagraphMeasurementCacheValue = Size; + +using ParagraphMeasurementCacheHash = std::hash; + +class ParagraphMeasurementCache; +using SharedParagraphMeasurementCache = + std::shared_ptr; + +class ParagraphMeasurementCache { + public: + ParagraphMeasurementCache() : cache_{256} {} + + bool exists(ParagraphMeasurementCacheKey &key) const { + std::lock_guard lock(mutex_); + return cache_.exists(key); + } + + ParagraphMeasurementCacheValue get( + const ParagraphMeasurementCacheKey &key) const { + std::lock_guard lock(mutex_); + return cache_.get(key); + } + + void set( + const ParagraphMeasurementCacheKey &key, + ParagraphMeasurementCacheValue &value) const { + std::lock_guard lock(mutex_); + cache_.set(key, value); + } + + private: + mutable folly::EvictingCacheMap< + ParagraphMeasurementCacheKey, + ParagraphMeasurementCacheValue> + cache_; + mutable std::mutex mutex_; +}; + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.cpp b/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.cpp index b90fd4637..c0382d486 100644 --- a/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.cpp +++ b/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.cpp @@ -6,8 +6,8 @@ */ #include "ParagraphShadowNode.h" - #include "ParagraphLocalData.h" +#include "ParagraphMeasurementCache.h" namespace facebook { namespace react { @@ -32,6 +32,12 @@ void ParagraphShadowNode::setTextLayoutManager( textLayoutManager_ = textLayoutManager; } +void ParagraphShadowNode::setMeasureCache( + SharedParagraphMeasurementCache cache) { + ensureUnsealed(); + measureCache_ = cache; +} + void ParagraphShadowNode::updateLocalDataIfNeeded() { ensureUnsealed(); @@ -52,10 +58,23 @@ void ParagraphShadowNode::updateLocalDataIfNeeded() { #pragma mark - LayoutableShadowNode Size ParagraphShadowNode::measure(LayoutConstraints layoutConstraints) const { - return textLayoutManager_->measure( - getAttributedString(), - getProps()->paragraphAttributes, - layoutConstraints); + AttributedString attributedString = getAttributedString(); + const ParagraphAttributes attributes = getProps()->paragraphAttributes; + + // Cache results of this function so we don't need to call measure() + // repeatedly + ParagraphMeasurementCacheKey hashValue = + std::make_tuple(attributedString, attributes, layoutConstraints); + if (measureCache_->exists(hashValue)) { + return measureCache_->get(hashValue); + } + + Size measuredSize = textLayoutManager_->measure( + attributedString, getProps()->paragraphAttributes, layoutConstraints); + + measureCache_->set(hashValue, measuredSize); + + return measuredSize; } void ParagraphShadowNode::layout(LayoutContext layoutContext) { diff --git a/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.h b/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.h index 3eb68ae14..4917defe8 100644 --- a/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.h +++ b/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include #include @@ -48,6 +49,13 @@ class ParagraphShadowNode : public ConcreteViewShadowNode< */ void setTextLayoutManager(SharedTextLayoutManager textLayoutManager); + /* + * Associates a shared LRU cache with the node. + * `ParagraphShadowNode` uses this to cache the results of + * text rendering measurements. + */ + void setMeasureCache(SharedParagraphMeasurementCache cache); + #pragma mark - LayoutableShadowNode void layout(LayoutContext layoutContext) override; @@ -61,6 +69,7 @@ class ParagraphShadowNode : public ConcreteViewShadowNode< void updateLocalDataIfNeeded(); SharedTextLayoutManager textLayoutManager_; + SharedParagraphMeasurementCache measureCache_; /* * Cached attributed string that represents the content of the subtree started diff --git a/ReactCommon/fabric/core/BUCK b/ReactCommon/fabric/core/BUCK index f03ab4b70..e171ed429 100644 --- a/ReactCommon/fabric/core/BUCK +++ b/ReactCommon/fabric/core/BUCK @@ -51,6 +51,7 @@ rn_xplat_cxx_library( "xplat//folly:memory", "xplat//folly:molly", "xplat//third-party/glog:glog", + react_native_xplat_target("utils:utils"), react_native_xplat_target("fabric/debug:debug"), react_native_xplat_target("fabric/events:events"), react_native_xplat_target("fabric/graphics:graphics"), diff --git a/ReactCommon/fabric/core/layout/LayoutConstraints.h b/ReactCommon/fabric/core/layout/LayoutConstraints.h index 1afd10b59..1d54a0594 100644 --- a/ReactCommon/fabric/core/layout/LayoutConstraints.h +++ b/ReactCommon/fabric/core/layout/LayoutConstraints.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include @@ -22,5 +23,27 @@ struct LayoutConstraints { LayoutDirection layoutDirection{LayoutDirection::Undefined}; }; +inline bool operator==( + const LayoutConstraints &lhs, + const LayoutConstraints &rhs) { + return std::tie(lhs.minimumSize, lhs.maximumSize, lhs.layoutDirection) == + std::tie(rhs.minimumSize, rhs.maximumSize, rhs.layoutDirection); +} + } // namespace react } // namespace facebook + +namespace std { +template <> +struct hash { + size_t operator()(const facebook::react::LayoutConstraints &v) const { + size_t seed = 0; + folly::hash::hash_combine( + seed, std::hash()(v.minimumSize)); + folly::hash::hash_combine( + seed, std::hash()(v.maximumSize)); + folly::hash::hash_combine(seed, v.layoutDirection); + return hash()(seed); + } +}; +} // namespace std diff --git a/ReactCommon/fabric/mounting/ShadowView.h b/ReactCommon/fabric/mounting/ShadowView.h index a8516da87..a01c74a93 100644 --- a/ReactCommon/fabric/mounting/ShadowView.h +++ b/ReactCommon/fabric/mounting/ShadowView.h @@ -34,7 +34,7 @@ struct ShadowView final { ComponentName componentName = ""; ComponentHandle componentHandle = 0; - Tag tag = -1; + Tag tag = -1; // Tag does not change during the lifetime of a shadow view. SharedProps props = {}; SharedEventEmitter eventEmitter = {}; LayoutMetrics layoutMetrics = EmptyLayoutMetrics; diff --git a/ReactCommon/utils/BUCK b/ReactCommon/utils/BUCK new file mode 100644 index 000000000..d46e8d9e7 --- /dev/null +++ b/ReactCommon/utils/BUCK @@ -0,0 +1,27 @@ +load("@fbsource//tools/build_defs:glob_defs.bzl", "subdir_glob") +load("//tools/build_defs/oss:rn_defs.bzl", "get_apple_compiler_flags", "rn_xplat_cxx_library") + +CXX_LIBRARY_COMPILER_FLAGS = [ + "-std=c++14", + "-Wall", +] + +rn_xplat_cxx_library( + name = "utils", + header_namespace = "", + exported_headers = subdir_glob( + [ + ("", "FloatComparison.h"), + ], + prefix = "react/utils", + ), + compiler_flags = CXX_LIBRARY_COMPILER_FLAGS + [ + "-fexceptions", + "-frtti", + ], + fbobjc_compiler_flags = get_apple_compiler_flags(), + force_static = True, + visibility = [ + "PUBLIC", + ], +) diff --git a/ReactCommon/utils/FloatComparison.h b/ReactCommon/utils/FloatComparison.h new file mode 100644 index 000000000..cb4f2d5d9 --- /dev/null +++ b/ReactCommon/utils/FloatComparison.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +namespace facebook { +namespace react { + +inline bool floatEquality (float a, float b, float epsilon = 0.005f) { + return (std::isnan(a) && std::isnan(b)) || (!std::isnan(a) && !std::isnan(b) && fabs(a - b) < epsilon); +} + +} // namespace react +} // namespace facebook