Repository navigation
Avoid copying text fragments and re-attributing runs when building NSAttributedString - #59154
hryhoriiK97 wants to merge 1 commit into
Conversation
cipolleschi
left a comment
There was a problem hiding this comment.
This PR is doing a lot of changes that are unrelated from the PR title/purpose.
Also, most of them are changes that are not really needed: they are small refactoring that only adds complexity and take time to review.
Please do small focused PRs that do a single thing and do not add not-relevant changes to the PR
| [[NSMutableAttributedString attributedStringWithAttachment:attachment] mutableCopy]; | ||
| if (eventEmitterWrapper != nil) { | ||
| [nsAttributedStringFragment addAttribute:RCTAttributedStringEventEmitterKey | ||
| value:eventEmitterWrapper | ||
| range:NSMakeRange(0, nsAttributedStringFragment.length)]; |
There was a problem hiding this comment.
this changes the behavior of React Native and has nothing to do with the PR title.
Before we were not attaching the event emitter if the fragment was an attachment, now we are.
If you want this change, please create a separate PR with the proper title and summary. Let's have small and focused PRs, please.
There was a problem hiding this comment.
Thanks for the review!
I believe this one is not a behavior change: on main, the same addAttributes call already set the emitter on attachment fragments too (RCTNSAttributedStringFragmentWithAttributesFromFragment applied it to both kinds). To make that clearer, I've reworked the diff so the attachment code stays untouched - only text fragments now receive the emitter in their initial attributes. Happy to adjust if you'd prefer a different shape 🙂
1343122 to
3fee4c7
Compare
dc8e2cc to
37b84be
Compare
@cipolleschi sorry about this! I've updated the PR so it does a single thing: it only avoids copying each text fragment and re-attributing the runs when building the NSAttributedString. The unrelated refactors are removed, and the title, description and measurements are updated to match the smaller diff. Thanks for the review! |
| NSDictionary<NSAttributedStringKey, id> *additionalTextAttributes = | ||
| @{RCTAttributedStringEventEmitterKey : eventEmitterWrapper}; |
There was a problem hiding this comment.
Can we not handle this inside RCTNSAttributedStringFragmentFromFragment too?
There was a problem hiding this comment.
Good point, done. The event emitter is now set inside RCTNSAttributedStringFragmentFromFragment for both text and attachments, so the extra addAttributes pass and the wrapper function are gone.
There was a problem hiding this comment.
Why does this need to return a mutableCopy? It's already mutable and there's no other references
There was a problem hiding this comment.
You're right! I now build the attachment string directly with initWithString:attributes:, so there's no copy. The output is identical.
| [nsAttributedString beginEditing]; | ||
|
|
||
| for (auto fragment : attributedString.getFragments()) { | ||
| for (const auto &fragment : attributedString.getFragments()) { |
…AttributedString Iterate AttributedString fragments by reference instead of copying each Fragment, and build each fragment's NSAttributedString with all of its attributes at once, including the event emitter, for text and attachment fragments alike. This removes the second addAttributes:range: pass, the wrapper function that ran it, and the mutableCopy of the attachment string, which is now created directly with initWithString:attributes:. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
37b84be to
b2188ae
Compare
|
@javache has imported this pull request. If you are a Meta employee, you can view this in D126056330. |
Summary:
RCTNSAttributedStringFromAttributedString(ReactCommon/react/renderer/textlayoutmanager/platform/ios/.../RCTAttributedTextUtils.mm) converts a paragraph'sAttributedStringinto theNSAttributedStringTextKit lays out and draws. It runs once per paragraph per measurement when the attributed-string cache misses, so for a screen of distinct labels it runs once per label.Current behavior. Two pieces of redundant work per fragment:
getFragments()by value (for (auto fragment : ...)), copying eachFragment: itsstd::string, itsTextAttributes, and aShadowViewwith threeshared_ptrmembers (props, event emitter, state), so three atomic retain/release pairs per fragment;addAttributes:range:pass over the fragment's string right after it was created with its attributes, which splits and re-merges the attribute runs TextKit just built. For an attachment, the string fromattributedStringWithAttachment:is also copied withmutableCopybefore that pass.With this change. Fragments are iterated by reference, and
RCTNSAttributedStringFragmentFromFragmentcreates each fragment's string with all of its attributes at once, including the event emitter, for text and attachment fragments alike. The secondaddAttributes:range:pass and the wrapper function that ran it are gone. The attachment string is created withinitWithString:attributes:fromNSAttachmentCharacterandNSAttachmentAttributeName, which is whatattributedStringWithAttachment:produces, so it needs no copy. Every run ends up with the same attributes as before.Changelog:
[IOS] [CHANGED] - Build a paragraph's NSAttributedString without copying each text fragment or re-attributing its runs
Test Plan:
node scripts/clang-format.js --checkon the changed file passes.xcodebuildin Release and run on a physical iPhone 15 Pro (iOS 26.6.1).main's conversion next to this one for every paragraph and compared the two results run by run: string, run ranges and every attribute, with attachments compared by bounds and image and event emitters by the emitter they wrap. Scrolling through the RNTester Text examples, which include inline views and pressable nested text, and mounting 1000 labels five times in the Playground compared over 31,000 conversions, with 217 attachment fragments and over 32,000 fragments carrying an event emitter. There were no differences, and the same check reports an emitter removed on purpose.main's conversion and 99.4 ms with this change, −26% of the conversion step. The presses were driven by Argent on the physical iPhone.🤖 Generated with Claude Code