You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 0ffaff8
Browse filesBrowse the repository at this point in the historyBrowse files
Avoid unnecessary AttributedString copies on iOS (#59018)
Summary:
Measuring `<Text>` on iOS makes a bunch of unnecessary copies of `AttributedString`.
There are a few places where this happens:
- `ParagraphShadowNode` copies a local string into `AttributedStringBox` even though the local is about to be destroyed.
- `TextLayoutManager::measure` uses `auto` instead of a reference, which makes another copy.
- `ensurePlaceholderIfEmpty_DO_NOT_USE` copies the string even when it's not empty.
- The `RCTTextLayoutManager` methods take the string by value, which also copies it.
This PR avoids those extra copies by using `std::move`, passing references where possible, and only creating a placeholder when the string is actually empty. Nothing changes in how text is rendered.
With these changes, a cache hit goes from **5 copies to 2**, and a cache miss goes from **11 to 6**. Draw, hit-testing, and frame updates go from **2 copies to 0**. The remaining copies mostly come from cache keys, which I left alone.
It's a pretty small cleanup, but it removes some unnecessary work.
I also ran a quick benchmark on an M4 Pro using clang `-O2`, taking the median of 7 runs:
| Fragments | Deep copy | Box creation (before → after) | measure() entry (before → after) |
|---|---|---|---|
| 1 | 41 ns | 87 → 52 ns | 77 → 1 ns |
| 3 | 76 ns | 160 → 89 ns | 150 → 1 ns |
| 10 | 211 ns | 436 → 227 ns | 427 → 1 ns |
<details>
<summary>Benchmark code</summary>
```cpp
#include <react/renderer/attributedstring/AttributedString.h>
#include <react/renderer/attributedstring/AttributedStringBox.h>
#include <react/renderer/attributedstring/PlaceholderAttributedString.h>
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <memory>
#include <optional>
#include <vector>
using namespace facebook::react;
static AttributedString makeString(int fragmentCount) {
AttributedString attributedString;
auto owner = std::make_shared<int>(0);
for (int i = 0; i < fragmentCount; i++) {
AttributedString::Fragment fragment;
fragment.string = "This is a typical row label in a list";
fragment.textAttributes.fontSize = 16;
fragment.parentShadowView.tag = i + 1;
// Aliasing shared_ptrs: real refcount traffic without constructing Props/EventEmitter/State.
fragment.parentShadowView.props = std::shared_ptr<const Props>(owner, nullptr);
fragment.parentShadowView.eventEmitter = std::shared_ptr<EventEmitter>(owner, nullptr);
fragment.parentShadowView.state = std::shared_ptr<const State>(owner, nullptr);
attributedString.appendFragment(std::move(fragment));
}
return attributedString;
}
template <typename F>
static double nsPerIteration(F&& body, int iterations) {
std::vector<double> samples;
for (int run = 0; run < 7; run++) {
auto start = std::chrono::steady_clock::now();
for (int i = 0; i < iterations; i++) body();
auto end = std::chrono::steady_clock::now();
samples.push_back(std::chrono::duration<double, std::nano>(end - start).count() / iterations);
}
std::sort(samples.begin(), samples.end());
return samples[3];
}
#define ESCAPE(x) asm volatile("" : : "r"(&(x)) : "memory")
int main() {
const int iterations = 200000;
std::printf("| Fragments | Deep copy | Box from lvalue (before) | Box from rvalue (after) | measure() entry before | measure() entry after |\n");
std::printf("|---|---|---|---|---|---|\n");
for (int n : {1, 3, 10}) {
const auto source = makeString(n);
AttributedStringBox box{source};
double copy = nsPerIteration([&] { AttributedString c = source; ESCAPE(c); }, iterations);
// The three ParagraphShadowNode sites: a dying local handed to the box.
double boxBefore = nsPerIteration([&] { auto local = source; AttributedStringBox b{local}; ESCAPE(b); }, iterations);
double boxAfter = nsPerIteration([&] { auto local = source; AttributedStringBox b{std::move(local)}; ESCAPE(b); }, iterations);
// The iOS TextLayoutManager::measure entry, before and after.
double entryBefore = nsPerIteration([&] {
auto original = box.getValue();
auto attributedString = ensurePlaceholderIfEmpty_DO_NOT_USE(original);
ESCAPE(attributedString);
}, iterations);
double entryAfter = nsPerIteration([&] {
const auto& original = box.getValue();
auto placeholder = original.isEmpty() ? std::optional<AttributedString>{ensurePlaceholderIfEmpty_DO_NOT_USE(original)} : std::nullopt;
const auto& attributedString = placeholder ? *placeholder : original;
ESCAPE(attributedString);
}, iterations);
std::printf("| %d | %.0f ns | %.0f ns | %.0f ns | %.0f ns | %.0f ns |\n", n, copy, boxBefore, boxAfter, entryBefore, entryAfter);
}
}
```
</details>
## Changelog:
[IOS] [CHANGED] - Pass `AttributedString` by const reference in `RCTTextLayoutManager` and add a move constructor for `AttributedStringBox`.
Pull Request resolved: #59018
Test Plan:
- Added `testValueMoveConstructor` to `AttributedStringBoxTest`.
- `yarn fantom Text`: all 223 tests pass.
- RNTester builds and runs on the iPhone 18 Pro simulator, and all 172 `RNTesterUnitTests` pass.
- `yarn format-check-cpp` passes.
- Updated API snapshots.
- Haven't benchmarked this on a physical device yet.
Reviewed By: cipolleschi
Differential Revision: D124965503
Pulled By: javache
fbshipit-source-id: e7e0add9aeb1dc90768ad852b0446f9804bcc83c
Copy file name to clipboardExpand all lines: packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h
+8-8Lines changed: 8 additions & 8 deletions
Original file line number
Diff line number
Diff line change
@@ -29,7 +29,7 @@ using RCTTextLayoutFragmentEnumerationBlock =
Copy file name to clipboardExpand all lines: packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm
Copy file name to clipboardExpand all lines: packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm
0 commit comments