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
This commit is contained in:
Joshua Gross 2019-01-25 16:55:06 -08:00 committed by Facebook Github Bot
parent af1f2728a0
commit a9049442f7
15 changed files with 234 additions and 30 deletions

View File

@ -10,6 +10,7 @@
#include <functional>
#include <memory>
#include <folly/Hash.h>
#include <folly/Optional.h>
#include <react/attributedstring/TextAttributes.h>
#include <react/core/Sealable.h>
@ -89,12 +90,12 @@ template <>
struct hash<facebook::react::AttributedString::Fragment> {
size_t operator()(
const facebook::react::AttributedString::Fragment &fragment) const {
return std::hash<decltype(fragment.string)>{}(fragment.string) +
std::hash<decltype(fragment.textAttributes)>{}(
fragment.textAttributes) +
std::hash<decltype(fragment.shadowView)>{}(fragment.shadowView) +
std::hash<decltype(fragment.parentShadowView)>{}(
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<facebook::react::AttributedString> {
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<facebook::react::AttributedString::Fragment>{}(fragment);
folly::hash::hash_combine(seed, fragment);
}
return result;
return seed;
}
};
} // namespace std

View File

@ -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"),

View File

@ -10,10 +10,25 @@
#include <react/attributedstring/conversions.h>
#include <react/debug/debugStringConvertibleUtils.h>
#include <react/graphics/conversions.h>
#include <react/utils/FloatComparison.h>
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

View File

@ -9,6 +9,7 @@
#include <limits>
#include <folly/Hash.h>
#include <react/attributedstring/primitives.h>
#include <react/debug/DebugStringConvertible.h>
#include <react/graphics/Geometry.h>
@ -53,6 +54,9 @@ class ParagraphAttributes : public DebugStringConvertible {
Float minimumFontSize{std::numeric_limits<Float>::quiet_NaN()};
Float maximumFontSize{std::numeric_limits<Float>::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<facebook::react::ParagraphAttributes> {
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<float>{}(attributes.minimumFontSize));
folly::hash::hash_combine(
seed, std::hash<float>{}(attributes.maximumFontSize));
return hash<int>()(seed);
}
};
} // namespace std

View File

@ -10,6 +10,7 @@
#include <react/attributedstring/conversions.h>
#include <react/core/conversions.h>
#include <react/graphics/conversions.h>
#include <react/utils/FloatComparison.h>
#include <cmath>
#include <react/debug/debugStringConvertibleUtils.h>
@ -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 {

View File

@ -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"),

View File

@ -7,7 +7,10 @@
#pragma once
#include <react/components/text/ParagraphShadowNode.h>
#include "ParagraphMeasurementCache.h"
#include "ParagraphShadowNode.h"
#include <folly/container/EvictingCacheMap.h>
#include <react/core/ConcreteComponentDescriptor.h>
#include <react/textlayoutmanager/TextLayoutManager.h>
#include <react/uimanager/ContextContainer.h>
@ -28,6 +31,10 @@ class ParagraphComponentDescriptor final
// Every single `ParagraphShadowNode` will have a reference to
// a shared `TextLayoutManager`.
textLayoutManager_ = std::make_shared<TextLayoutManager>(contextContainer);
// Every single `ParagraphShadowNode` will have a reference to
// a shared `EvictingCacheMap`, a simple LRU cache for Paragraph
// measurements.
measureCache_ = std::make_shared<ParagraphMeasurementCache>();
}
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

View File

@ -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 <folly/container/EvictingCacheMap.h>
#include <react/attributedstring/AttributedString.h>
#include <react/attributedstring/ParagraphAttributes.h>
#include <react/core/LayoutConstraints.h>
namespace facebook {
namespace react {
using ParagraphMeasurementCacheKey =
std::tuple<AttributedString, ParagraphAttributes, LayoutConstraints>;
using ParagraphMeasurementCacheValue = Size;
using ParagraphMeasurementCacheHash = std::hash<ParagraphMeasurementCacheKey>;
class ParagraphMeasurementCache;
using SharedParagraphMeasurementCache =
std::shared_ptr<const ParagraphMeasurementCache>;
class ParagraphMeasurementCache {
public:
ParagraphMeasurementCache() : cache_{256} {}
bool exists(ParagraphMeasurementCacheKey &key) const {
std::lock_guard<std::mutex> lock(mutex_);
return cache_.exists(key);
}
ParagraphMeasurementCacheValue get(
const ParagraphMeasurementCacheKey &key) const {
std::lock_guard<std::mutex> lock(mutex_);
return cache_.get(key);
}
void set(
const ParagraphMeasurementCacheKey &key,
ParagraphMeasurementCacheValue &value) const {
std::lock_guard<std::mutex> lock(mutex_);
cache_.set(key, value);
}
private:
mutable folly::EvictingCacheMap<
ParagraphMeasurementCacheKey,
ParagraphMeasurementCacheValue>
cache_;
mutable std::mutex mutex_;
};
} // namespace react
} // namespace facebook

View File

@ -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) {

View File

@ -8,6 +8,7 @@
#pragma once
#include <folly/Optional.h>
#include <react/components/text/ParagraphMeasurementCache.h>
#include <react/components/text/ParagraphProps.h>
#include <react/components/text/TextShadowNode.h>
#include <react/components/view/ConcreteViewShadowNode.h>
@ -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

View File

@ -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"),

View File

@ -7,6 +7,7 @@
#pragma once
#include <folly/Hash.h>
#include <react/core/LayoutPrimitives.h>
#include <react/graphics/Geometry.h>
@ -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<facebook::react::LayoutConstraints> {
size_t operator()(const facebook::react::LayoutConstraints &v) const {
size_t seed = 0;
folly::hash::hash_combine(
seed, std::hash<facebook::react::Size>()(v.minimumSize));
folly::hash::hash_combine(
seed, std::hash<facebook::react::Size>()(v.maximumSize));
folly::hash::hash_combine(seed, v.layoutDirection);
return hash<int>()(seed);
}
};
} // namespace std

View File

@ -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;

27
ReactCommon/utils/BUCK Normal file
View File

@ -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",
],
)

View File

@ -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