Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apple/MarkdownTextInputDecoratorShadowNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ class JSI_EXPORT MarkdownTextInputDecoratorShadowNode final
YGMeasureMode heightMode);
static YogaLayoutableShadowNode &
shadowNodeFromContext(YGNodeConstRef yogaNode);

// Persisted RCTMarkdownUtils instance shared across shadow node clones so
// that MarkdownParser's one-entry memo cache (keyed on text + parserId)
// survives repeated Yoga measure callbacks instead of being discarded on
// every call to applyMarkdownFormattingToTextInputState.
mutable std::shared_ptr<void> markdownUtils_;
};

} // namespace react
Expand Down
33 changes: 27 additions & 6 deletions apple/MarkdownTextInputDecoratorShadowNode.mm
Comment thread
war-in marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@
ShadowNode const &sourceShadowNode,
ShadowNodeFragment const &fragment)
: ConcreteViewShadowNode(sourceShadowNode, fragment) {
// Carry the persisted RCTMarkdownUtils over from the source node so the
// MarkdownParser memo cache survives the frequent cloning that happens
// during layout and re-render cycles.
const auto &source =
static_cast<const MarkdownTextInputDecoratorShadowNode &>(sourceShadowNode);
markdownUtils_ = source.markdownUtils_;

initialize();
makeChildNodeMutable();

Expand Down Expand Up @@ -182,12 +189,20 @@
const auto defaultNSTextAttributes =
RCTNSTextAttributesFromTextAttributes(defaultTextAttributes);

// this can possibly be optimized
// Lazily create and persist the RCTMarkdownUtils instance so the MarkdownParser
// one-entry memo cache (keyed on text + parserId) survives repeated Yoga measure
// callbacks. Previously a fresh utils/parser was allocated on every call,
// discarding the cache and forcing a full JSI re-parse each time.
if (!markdownUtils_) {
RCTMarkdownUtils *freshUtils = [[RCTMarkdownUtils alloc] init];
markdownUtils_ = std::shared_ptr<void>(
(__bridge_retained void *)freshUtils, [](void *p) { CFRelease(p); });
}
RCTMarkdownUtils *utils = (__bridge RCTMarkdownUtils *)markdownUtils_.get();

RCTMarkdownStyle *markdownStyle =
[[RCTMarkdownStyle alloc] initWithStruct:decoratorProps.markdownStyle];
RCTMarkdownUtils *utils = [[RCTMarkdownUtils alloc] init];
[utils setMarkdownStyle:markdownStyle];
[utils setParserId:[NSNumber numberWithInt:decoratorProps.parserId]];
NSNumber *parserId = [NSNumber numberWithInt:decoratorProps.parserId];

// convert the attibuted string stored in state to
// NSAttributedString
Expand Down Expand Up @@ -228,7 +243,10 @@

// apply markdown
NSMutableAttributedString *newString = [nsAttributedString mutableCopy];
[utils applyMarkdownFormatting:newString withDefaultTextAttributes:defaultNSTextAttributes];
[utils applyMarkdownFormatting:newString
withDefaultTextAttributes:defaultNSTextAttributes
markdownStyle:markdownStyle
parserId:parserId];

// create a clone of the old TextInputState and update the
// attributed string box to point to the string with markdown
Expand All @@ -240,7 +258,10 @@

// apply markdown
NSMutableAttributedString *newString = [nsAttributedString mutableCopy];
[utils applyMarkdownFormatting:newString withDefaultTextAttributes:defaultNSTextAttributes];
[utils applyMarkdownFormatting:newString
withDefaultTextAttributes:defaultNSTextAttributes
markdownStyle:markdownStyle
parserId:parserId];

// create a clone of the old TextInputState and update the
// attributed string box to point to the string with markdown
Expand Down
9 changes: 9 additions & 0 deletions apple/RCTMarkdownUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ NS_ASSUME_NONNULL_BEGIN
- (void)applyMarkdownFormatting:(nonnull NSMutableAttributedString *)attributedString
withDefaultTextAttributes:(nonnull NSDictionary<NSAttributedStringKey, id> *)defaultTextAttributes;

// Atomically sets the style/parser and applies formatting under a single lock.
// Use this from the shadow node measure path, where one RCTMarkdownUtils
// instance is shared across shadow node clones and may be accessed from
// concurrent Fabric commits/layout passes.
- (void)applyMarkdownFormatting:(nonnull NSMutableAttributedString *)attributedString
withDefaultTextAttributes:(nonnull NSDictionary<NSAttributedStringKey, id> *)defaultTextAttributes
markdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle
parserId:(nonnull NSNumber *)parserId;

@end

NS_ASSUME_NONNULL_END
18 changes: 18 additions & 0 deletions apple/RCTMarkdownUtils.mm
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,22 @@ - (void)applyMarkdownFormatting:(nonnull NSMutableAttributedString *)attributedS
withMarkdownStyle:_markdownStyle];
}

- (void)applyMarkdownFormatting:(nonnull NSMutableAttributedString *)attributedString
withDefaultTextAttributes:(nonnull NSDictionary<NSAttributedStringKey, id> *)defaultTextAttributes
markdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle
parserId:(nonnull NSNumber *)parserId
{
// Keep the style/parserId assignment and the parse+format together under a
// single lock. The shadow node shares one instance across clones, and Fabric
// runs commits/layout optimistically on multiple threads, so without this the
// setters could interleave with another thread's parse/format and apply the
// wrong parserId/style for a frame. `@synchronized` is recursive, so nesting
// with `MarkdownParser`'s own `@synchronized(self)` in `parse:` is safe.
@synchronized (self) {
_markdownStyle = markdownStyle;
_parserId = parserId;
[self applyMarkdownFormatting:attributedString withDefaultTextAttributes:defaultTextAttributes];
}
}

@end
Loading