Skip to content

Commit 933e919

Browse files
committed
Avoid copying AttributedString on the iOS text measure path
iOS text measurement deep-copied the AttributedString several times per measure, even on a cache hit. Bind references where the code only reads, move dying locals into the box, add an AttributedString&& box constructor, build the empty-string placeholder only when needed, and take const AttributedString& in the RCTTextLayoutManager methods.
1 parent d2669f4 commit 933e919

11 files changed

Lines changed: 69 additions & 29 deletions

File tree

‎packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
@interface RCTTextLayoutManager (RCTParagraphComponentViewPrivate)
2929

30-
- (CGRect)drawingFrameForAttributedString:(facebook::react::AttributedString)attributedString
30+
- (CGRect)drawingFrameForAttributedString:(const facebook::react::AttributedString &)attributedString
3131
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
3232
frame:(CGRect)frame
3333
containerFrame:(CGRect *)containerFrame;

‎packages/react-native/ReactCommon/react/renderer/attributedstring/AttributedStringBox.cpp‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ AttributedStringBox::AttributedStringBox(const AttributedString& value)
2323
value_(std::make_shared<const AttributedString>(value)),
2424
opaquePointer_({}) {};
2525

26+
AttributedStringBox::AttributedStringBox(AttributedString&& value)
27+
: mode_(Mode::Value),
28+
value_(std::make_shared<const AttributedString>(std::move(value))),
29+
opaquePointer_({}) {};
30+
2631
AttributedStringBox::AttributedStringBox(std::shared_ptr<void> opaquePointer)
2732
: mode_(Mode::OpaquePointer),
2833
value_({}),

‎packages/react-native/ReactCommon/react/renderer/attributedstring/AttributedStringBox.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class AttributedStringBox final {
3737
* Custom explicit constructors.
3838
*/
3939
explicit AttributedStringBox(const AttributedString &value);
40+
explicit AttributedStringBox(AttributedString &&value);
4041
explicit AttributedStringBox(std::shared_ptr<void> opaquePointer);
4142

4243
/*

‎packages/react-native/ReactCommon/react/renderer/attributedstring/tests/AttributedStringBoxTest.cpp‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ TEST(AttributedStringBoxTest, testValueConstructor) {
3030
EXPECT_EQ(attributedStringBox.getValue(), attributedString);
3131
}
3232

33+
TEST(AttributedStringBoxTest, testValueMoveConstructor) {
34+
auto attributedString = AttributedString{};
35+
auto fragment = AttributedString::Fragment{};
36+
fragment.string = "test string";
37+
attributedString.appendFragment(std::move(fragment));
38+
auto expectedAttributedString = attributedString;
39+
auto attributedStringBox = AttributedStringBox{std::move(attributedString)};
40+
41+
EXPECT_EQ(attributedStringBox.getMode(), AttributedStringBox::Mode::Value);
42+
EXPECT_EQ(attributedStringBox.getValue(), expectedAttributedString);
43+
}
44+
3345
TEST(AttributedStringBoxTest, testOpaquePointerConstructor) {
3446
auto string = std::make_shared<std::string>("test string");
3547
auto attributedStringBox = AttributedStringBox{string};

‎packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ const Content& ParagraphShadowNode::getContent(
8888
attributedString.setBaseTextAttributes(textAttributes);
8989

9090
content_ = Content{
91-
attributedString, getConcreteProps().paragraphAttributes, attachments};
91+
std::move(attributedString),
92+
getConcreteProps().paragraphAttributes,
93+
std::move(attachments)};
9294

9395
return content_.value();
9496
}
@@ -264,7 +266,7 @@ Size ParagraphShadowNode::measureContent(
264266

265267
auto size = textLayoutManager_
266268
->measure(
267-
AttributedStringBox{content.attributedString},
269+
AttributedStringBox{std::move(content.attributedString)},
268270
content.paragraphAttributes,
269271
textLayoutContext,
270272
layoutConstraints)
@@ -282,7 +284,7 @@ Float ParagraphShadowNode::baseline(
282284
auto content =
283285
getContentWithMeasuredAttachments(layoutContext, layoutConstraints);
284286

285-
AttributedStringBox attributedStringBox{content.attributedString};
287+
AttributedStringBox attributedStringBox{std::move(content.attributedString)};
286288

287289
if constexpr (TextLayoutManagerExtended::supportsLineMeasurement()) {
288290
auto lines =
@@ -346,7 +348,9 @@ void ParagraphShadowNode::layout(LayoutContext layoutContext) {
346348
.pointScaleFactor = layoutContext.pointScaleFactor,
347349
.surfaceId = getSurfaceId(),
348350
};
349-
AttributedStringBox attributedStringBox{content.attributedString};
351+
// The state update above already captured the attributed string and
352+
// nothing below reads it, so the box can take it instead of copying it.
353+
AttributedStringBox attributedStringBox{std::move(content.attributedString)};
350354

351355
if (getConcreteProps().onTextLayout) {
352356
if constexpr (TextLayoutManagerExtended::supportsLineMeasurement()) {

‎packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ using RCTTextLayoutFragmentEnumerationBlock =
2929
*/
3030
@interface RCTTextLayoutManager : NSObject
3131

32-
- (facebook::react::TextMeasurement)measureAttributedString:(facebook::react::AttributedString)attributedString
32+
- (facebook::react::TextMeasurement)measureAttributedString:(const facebook::react::AttributedString &)attributedString
3333
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
3434
layoutContext:(facebook::react::TextLayoutContext)layoutContext
3535
layoutConstraints:(facebook::react::LayoutConstraints)layoutConstraints;
@@ -39,23 +39,23 @@ using RCTTextLayoutFragmentEnumerationBlock =
3939
layoutContext:(facebook::react::TextLayoutContext)layoutContext
4040
layoutConstraints:(facebook::react::LayoutConstraints)layoutConstraints;
4141

42-
- (void)drawAttributedString:(facebook::react::AttributedString)attributedString
42+
- (void)drawAttributedString:(const facebook::react::AttributedString &)attributedString
4343
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
4444
frame:(CGRect)frame
4545
drawHighlightPath:(void (^_Nullable)(UIBezierPath *highlightPath))block;
4646

47-
- (facebook::react::LinesMeasurements)getLinesForAttributedString:(facebook::react::AttributedString)attributedString
48-
paragraphAttributes:
49-
(facebook::react::ParagraphAttributes)paragraphAttributes
50-
size:(CGSize)size;
47+
- (facebook::react::LinesMeasurements)
48+
getLinesForAttributedString:(const facebook::react::AttributedString &)attributedString
49+
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
50+
size:(CGSize)size;
5151

5252
- (std::shared_ptr<const facebook::react::EventEmitter>)
53-
getEventEmitterWithAttributeString:(facebook::react::AttributedString)attributedString
53+
getEventEmitterWithAttributeString:(const facebook::react::AttributedString &)attributedString
5454
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
5555
frame:(CGRect)frame
5656
atPoint:(CGPoint)point;
5757

58-
- (void)getRectWithAttributedString:(facebook::react::AttributedString)attributedString
58+
- (void)getRectWithAttributedString:(const facebook::react::AttributedString &)attributedString
5959
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
6060
enumerateAttribute:(NSString *)enumerateAttribute
6161
frame:(CGRect)frame

‎packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ - (TextMeasurement)measureNSAttributedString:(NSAttributedString *)attributedStr
5959
return [self _measureTextStorage:textStorage paragraphAttributes:paragraphAttributes layoutContext:layoutContext];
6060
}
6161

62-
- (TextMeasurement)measureAttributedString:(AttributedString)attributedString
62+
- (TextMeasurement)measureAttributedString:(const AttributedString &)attributedString
6363
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
6464
layoutContext:(TextLayoutContext)layoutContext
6565
layoutConstraints:(LayoutConstraints)layoutConstraints
@@ -70,7 +70,7 @@ - (TextMeasurement)measureAttributedString:(AttributedString)attributedString
7070
layoutConstraints:layoutConstraints];
7171
}
7272

73-
- (CGRect)drawingFrameForAttributedString:(AttributedString)attributedString
73+
- (CGRect)drawingFrameForAttributedString:(const AttributedString &)attributedString
7474
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
7575
frame:(CGRect)frame
7676
containerFrame:(CGRect *)containerFrame
@@ -128,7 +128,7 @@ - (CGRect)drawingFrameForAttributedString:(AttributedString)attributedString
128128
return drawingFrame;
129129
}
130130

131-
- (void)drawAttributedString:(AttributedString)attributedString
131+
- (void)drawAttributedString:(const AttributedString &)attributedString
132132
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
133133
frame:(CGRect)frame
134134
drawHighlightPath:(void (^_Nullable)(UIBezierPath *highlightPath))block
@@ -363,7 +363,7 @@ - (void)processTruncatedAttributedText:(NSTextStorage *)textStorage
363363
}
364364
}
365365

366-
- (LinesMeasurements)getLinesForAttributedString:(facebook::react::AttributedString)attributedString
366+
- (LinesMeasurements)getLinesForAttributedString:(const facebook::react::AttributedString &)attributedString
367367
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
368368
size:(CGSize)size
369369
{
@@ -467,7 +467,7 @@ - (NSTextStorage *)_textStorageAndLayoutManagerWithAttributesString:(NSAttribute
467467
return textStorage;
468468
}
469469

470-
- (std::shared_ptr<const EventEmitter>)getEventEmitterWithAttributeString:(AttributedString)attributedString
470+
- (std::shared_ptr<const EventEmitter>)getEventEmitterWithAttributeString:(const AttributedString &)attributedString
471471
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
472472
frame:(CGRect)frame
473473
atPoint:(CGPoint)point
@@ -497,7 +497,7 @@ - (NSTextStorage *)_textStorageAndLayoutManagerWithAttributesString:(NSAttribute
497497
return nil;
498498
}
499499

500-
- (void)getRectWithAttributedString:(AttributedString)attributedString
500+
- (void)getRectWithAttributedString:(const AttributedString &)attributedString
501501
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
502502
enumerateAttribute:(NSString *)enumerateAttribute
503503
frame:(CGRect)frame
@@ -539,7 +539,7 @@ - (void)getRectWithAttributedString:(AttributedString)attributedString
539539

540540
#pragma mark - Private
541541

542-
- (NSAttributedString *)_nsAttributedStringFromAttributedString:(AttributedString)attributedString
542+
- (NSAttributedString *)_nsAttributedStringFromAttributedString:(const AttributedString &)attributedString
543543
{
544544
auto sharedNSAttributedString = _cache.get(attributedString, [&]() {
545545
return wrapManagedObject(RCTNSAttributedStringFromAttributedString(attributedString));

‎packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,23 @@
88
#import "TextLayoutManager.h"
99
#import "RCTTextLayoutManager.h"
1010

11+
#import <optional>
12+
1113
#import <react/renderer/attributedstring/PlaceholderAttributedString.h>
1214
#import <react/renderer/telemetry/TransactionTelemetry.h>
1315
#import <react/utils/ManagedObjectWrapper.h>
1416

1517
namespace facebook::react {
1618

19+
// `ensurePlaceholderIfEmpty_DO_NOT_USE` returns a copy, so only call it for an empty string that needs one.
20+
static std::optional<AttributedString> placeholderIfEmpty(const AttributedString &attributedString)
21+
{
22+
if (attributedString.isEmpty()) {
23+
return ensurePlaceholderIfEmpty_DO_NOT_USE(attributedString);
24+
}
25+
return std::nullopt;
26+
}
27+
1728
TextLayoutManager::TextLayoutManager(const std::shared_ptr<const ContextContainer> & /*contextContainer*/)
1829
{
1930
nativeTextLayoutManager_ = wrapManagedObject([RCTTextLayoutManager new]);
@@ -37,8 +48,10 @@
3748

3849
switch (attributedStringBox.getMode()) {
3950
case AttributedStringBox::Mode::Value: {
40-
auto originalAtributedString = attributedStringBox.getValue();
41-
auto attributedString = ensurePlaceholderIfEmpty_DO_NOT_USE(originalAtributedString);
51+
const auto &originalAttributedString = attributedStringBox.getValue();
52+
auto placeholderAttributedString = placeholderIfEmpty(originalAttributedString);
53+
const auto &attributedString =
54+
placeholderAttributedString ? *placeholderAttributedString : originalAttributedString;
4255

4356
measurement = textMeasureCache_.get(
4457
{.attributedString = attributedString,
@@ -60,7 +73,7 @@
6073
// being used to represent empty string. iOS TextLayoutManager
6174
// should instead measure using `baseTextAttributes` of the
6275
// `AttributedString`.
63-
if (originalAtributedString.isEmpty()) {
76+
if (originalAttributedString.isEmpty()) {
6477
measurement.size.width = 0;
6578
}
6679

@@ -106,7 +119,9 @@
106119
const Size &size) const
107120
{
108121
react_native_assert(attributedStringBox.getMode() == AttributedStringBox::Mode::Value);
109-
auto attributedString = ensurePlaceholderIfEmpty_DO_NOT_USE(attributedStringBox.getValue());
122+
const auto &originalAttributedString = attributedStringBox.getValue();
123+
auto placeholderAttributedString = placeholderIfEmpty(originalAttributedString);
124+
const auto &attributedString = placeholderAttributedString ? *placeholderAttributedString : originalAttributedString;
110125

111126
RCTTextLayoutManager *textLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(nativeTextLayoutManager_);
112127

‎scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,6 +1366,7 @@ class facebook::react::AttributedStringBox {
13661366
public AttributedStringBox();
13671367
public AttributedStringBox(const facebook::react::AttributedString& value);
13681368
public AttributedStringBox(const facebook::react::AttributedStringBox& other) = default;
1369+
public AttributedStringBox(facebook::react::AttributedString&& value);
13691370
public AttributedStringBox(facebook::react::AttributedStringBox&& other) noexcept;
13701371
public AttributedStringBox(std::shared_ptr<void> opaquePointer);
13711372
public const facebook::react::AttributedString& getValue() const;

‎scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1939,12 +1939,12 @@ interface RCTTextInputComponentView : public RCTViewComponentView {
19391939
}
19401940

19411941
interface RCTTextLayoutManager : public NSObject {
1942-
public virtual facebook::react::LinesMeasurements getLinesForAttributedString:paragraphAttributes:size:(facebook::react::AttributedString attributedString, facebook::react::ParagraphAttributes paragraphAttributes, CGSize size);
1943-
public virtual facebook::react::TextMeasurement measureAttributedString:paragraphAttributes:layoutContext:layoutConstraints:(facebook::react::AttributedString attributedString, facebook::react::ParagraphAttributes paragraphAttributes, facebook::react::TextLayoutContext layoutContext, facebook::react::LayoutConstraints layoutConstraints);
1942+
public virtual facebook::react::LinesMeasurements getLinesForAttributedString:paragraphAttributes:size:(const facebook::react::AttributedString& attributedString, facebook::react::ParagraphAttributes paragraphAttributes, CGSize size);
1943+
public virtual facebook::react::TextMeasurement measureAttributedString:paragraphAttributes:layoutContext:layoutConstraints:(const facebook::react::AttributedString& attributedString, facebook::react::ParagraphAttributes paragraphAttributes, facebook::react::TextLayoutContext layoutContext, facebook::react::LayoutConstraints layoutConstraints);
19441944
public virtual facebook::react::TextMeasurement measureNSAttributedString:paragraphAttributes:layoutContext:layoutConstraints:(NSAttributedString* attributedString, facebook::react::ParagraphAttributes paragraphAttributes, facebook::react::TextLayoutContext layoutContext, facebook::react::LayoutConstraints layoutConstraints);
1945-
public virtual std::shared_ptr<const facebook::react::EventEmitter> getEventEmitterWithAttributeString:paragraphAttributes:frame:atPoint:(facebook::react::AttributedString attributedString, facebook::react::ParagraphAttributes paragraphAttributes, CGRect frame, CGPoint point);
1946-
public virtual void drawAttributedString:paragraphAttributes:frame:drawHighlightPath:(facebook::react::AttributedString attributedString, facebook::react::ParagraphAttributes paragraphAttributes, CGRect frame, void(^)(UIBezierPath* highlightPath) block);
1947-
public virtual void getRectWithAttributedString:paragraphAttributes:enumerateAttribute:frame:usingBlock:(facebook::react::AttributedString attributedString, facebook::react::ParagraphAttributes paragraphAttributes, NSString* enumerateAttribute, CGRect frame, RCTTextLayoutFragmentEnumerationBlock block);
1945+
public virtual std::shared_ptr<const facebook::react::EventEmitter> getEventEmitterWithAttributeString:paragraphAttributes:frame:atPoint:(const facebook::react::AttributedString& attributedString, facebook::react::ParagraphAttributes paragraphAttributes, CGRect frame, CGPoint point);
1946+
public virtual void drawAttributedString:paragraphAttributes:frame:drawHighlightPath:(const facebook::react::AttributedString& attributedString, facebook::react::ParagraphAttributes paragraphAttributes, CGRect frame, void(^)(UIBezierPath* highlightPath) block);
1947+
public virtual void getRectWithAttributedString:paragraphAttributes:enumerateAttribute:frame:usingBlock:(const facebook::react::AttributedString& attributedString, facebook::react::ParagraphAttributes paragraphAttributes, NSString* enumerateAttribute, CGRect frame, RCTTextLayoutFragmentEnumerationBlock block);
19481948
}
19491949

19501950
interface RCTTextSelection : public NSObject {
@@ -3963,6 +3963,7 @@ class facebook::react::AttributedStringBox {
39633963
public AttributedStringBox();
39643964
public AttributedStringBox(const facebook::react::AttributedString& value);
39653965
public AttributedStringBox(const facebook::react::AttributedStringBox& other) = default;
3966+
public AttributedStringBox(facebook::react::AttributedString&& value);
39663967
public AttributedStringBox(facebook::react::AttributedStringBox&& other) noexcept;
39673968
public AttributedStringBox(std::shared_ptr<void> opaquePointer);
39683969
public const facebook::react::AttributedString& getValue() const;

0 commit comments

Comments
 (0)