Fabric: Text Measuring: TextMeasureCache the new, improved text measure cache

Summary:
Special thanks for Joshua Gross for flagging this problem!

This diff implements a custom evicting hash map designed specially to hold text measurement information. The key feature of this is custom equality checks and hashing functions.

They are designed around the following principals:
* Decorative text attributes (such as color, shadows, etc) has no effect on layout, therefore they should not be taking into account;
* `minimum height`, `minimum width`, and `maximum height` don't affect the measurement;
* the value of `layout metrics` are important only for `attachment` fragments.

After I redo all tests, I will enable this for Android-specific TextLayoutManager in separate diff.

Changelog: [Internal] Fabric-specific internal change.

Reviewed By: sammy-SC

Differential Revision: D18848583

fbshipit-source-id: 46c2fc445fbd1823afc5e7498e37de75381258b1
This commit is contained in:
Valentin Shergin 2019-12-06 10:50:54 -08:00 коммит произвёл Facebook Github Bot
Родитель 1a12919dea
Коммит e08412d9a1
4 изменённых файлов: 201 добавлений и 9 удалений

Просмотреть файл

@ -19,15 +19,21 @@ APPLE_COMPILER_FLAGS = get_apple_compiler_flags()
rn_xplat_cxx_library(
name = "textlayoutmanager",
srcs = glob(
[],
[
"*.cpp",
],
),
headers = subdir_glob(
[],
[
("", "*.h"),
],
prefix = "",
),
header_namespace = "",
exported_headers = subdir_glob(
[],
[
("", "*.h"),
],
prefix = "react/textlayoutmanager",
),
compiler_flags = [

Просмотреть файл

@ -0,0 +1,12 @@
/*
* 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.
*/
#include "TextMeasureCache.h"
namespace facebook {
namespace react {} // namespace react
} // namespace facebook

Просмотреть файл

@ -0,0 +1,178 @@
/*
* 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 <react/attributedstring/AttributedString.h>
#include <react/attributedstring/ParagraphAttributes.h>
#include <react/core/LayoutConstraints.h>
#include <react/utils/FloatComparison.h>
#include <react/utils/SimpleThreadSafeCache.h>
namespace facebook {
namespace react {
// The Key type that is used for Text Measure Cache.
// The equivalence and hashing operations of this are defined to respect the
// nature of text measuring.
class TextMeasureCacheKey final {
public:
AttributedString attributedString{};
ParagraphAttributes paragraphAttributes{};
LayoutConstraints layoutConstraints{};
};
/*
* Maximum size of the Cache.
* The number was empirically chosen based on approximation of an average amount
* of meaningful measures per surface.
*/
constexpr auto kSimpleThreadSafeCacheSizeCap = size_t{256};
/*
* Thread-safe, evicting hash table designed to store text measurement
* information.
*/
using TextMeasureCache = SimpleThreadSafeCache<
TextMeasureCacheKey,
Size,
kSimpleThreadSafeCacheSizeCap>;
inline bool areTextAttributesEquivalentLayoutWise(
TextAttributes const &lhs,
TextAttributes const &rhs) {
// Here we check all attributes that affect layout metrics and don't check any
// attributes that affect only a decorative aspect of displayed text (like
// colors).
return std::tie(
lhs.fontFamily,
lhs.fontWeight,
lhs.fontStyle,
lhs.fontVariant,
lhs.allowFontScaling,
lhs.alignment) ==
std::tie(
rhs.fontFamily,
rhs.fontWeight,
rhs.fontStyle,
rhs.fontVariant,
rhs.allowFontScaling,
rhs.alignment) &&
floatEquality(lhs.fontSize, rhs.fontSize) &&
floatEquality(lhs.fontSizeMultiplier, rhs.fontSizeMultiplier) &&
floatEquality(lhs.letterSpacing, rhs.letterSpacing) &&
floatEquality(lhs.lineHeight, rhs.lineHeight);
}
inline size_t textAttributesHashLayoutWise(
TextAttributes const &textAttributes) {
// Taking into account the same props as
// `areTextAttributesEquivalentLayoutWise` mentions.
return folly::hash::hash_combine(
0,
textAttributes.fontFamily,
textAttributes.fontSize,
textAttributes.fontSizeMultiplier,
textAttributes.fontWeight,
textAttributes.fontStyle,
textAttributes.fontVariant,
textAttributes.allowFontScaling,
textAttributes.letterSpacing,
textAttributes.lineHeight,
textAttributes.alignment);
}
inline bool areAttributedStringFragmentsEquivalentLayoutWise(
AttributedString::Fragment const &lhs,
AttributedString::Fragment const &rhs) {
return lhs.string == rhs.string &&
areTextAttributesEquivalentLayoutWise(
lhs.textAttributes, rhs.textAttributes) &&
// LayoutMetrics of an attachment fragment affects the size of a measured
// attributed string.
(!lhs.isAttachment() ||
(lhs.parentShadowView.layoutMetrics ==
rhs.parentShadowView.layoutMetrics));
}
inline size_t textAttributesHashLayoutWise(
AttributedString::Fragment const &fragment) {
// Here we are not taking `isAttachment` and `layoutMetrics` into account
// because they are logically interdependent and this can break an invariant
// between hash and equivalence functions (and cause cache misses).
return folly::hash::hash_combine(
0,
fragment.string,
textAttributesHashLayoutWise(fragment.textAttributes));
}
inline bool areAttributedStringsEquivalentLayoutWise(
AttributedString const &lhs,
AttributedString const &rhs) {
auto &lhsFragment = lhs.getFragments();
auto &rhsFragment = rhs.getFragments();
if (lhsFragment.size() != rhsFragment.size()) {
return false;
}
auto size = lhsFragment.size();
for (auto i = size_t{0}; i < size; i++) {
if (!areAttributedStringFragmentsEquivalentLayoutWise(
lhsFragment.at(i), rhsFragment.at(i))) {
return false;
}
}
return true;
}
inline size_t textAttributedStringHashLayoutWise(
AttributedString const &attributedString) {
auto seed = size_t{0};
for (auto const &fragment : attributedString.getFragments()) {
seed =
folly::hash::hash_combine(seed, textAttributesHashLayoutWise(fragment));
}
return seed;
}
inline bool operator==(
TextMeasureCacheKey const &lhs,
TextMeasureCacheKey const &rhs) {
return areAttributedStringsEquivalentLayoutWise(
lhs.attributedString, rhs.attributedString) &&
lhs.paragraphAttributes == rhs.paragraphAttributes &&
lhs.layoutConstraints.maximumSize.width ==
rhs.layoutConstraints.maximumSize.width;
}
inline bool operator!=(
TextMeasureCacheKey const &lhs,
TextMeasureCacheKey const &rhs) {
return !(lhs == rhs);
}
} // namespace react
} // namespace facebook
namespace std {
template <>
struct hash<facebook::react::TextMeasureCacheKey> {
size_t operator()(facebook::react::TextMeasureCacheKey const &key) const {
return folly::hash::hash_combine(
0,
textAttributedStringHashLayoutWise(key.attributedString),
key.paragraphAttributes,
key.layoutConstraints.maximumSize.width);
}
};
} // namespace std

Просмотреть файл

@ -12,8 +12,8 @@
#include <react/attributedstring/AttributedStringBox.h>
#include <react/attributedstring/ParagraphAttributes.h>
#include <react/core/LayoutConstraints.h>
#include <react/textlayoutmanager/TextMeasureCache.h>
#include <react/utils/ContextContainer.h>
#include <react/utils/SimpleThreadSafeCache.h>
namespace facebook {
namespace react {
@ -45,12 +45,8 @@ class TextLayoutManager {
void *getNativeTextLayoutManager() const;
private:
using MeasureCacheKey =
std::tuple<AttributedString, ParagraphAttributes, LayoutConstraints>;
using MeasureCache = SimpleThreadSafeCache<MeasureCacheKey, Size, 256>;
void *self_;
MeasureCache measureCache_{};
TextMeasureCache measureCache_{};
};
} // namespace react