diff --git a/apple/MarkdownTextInputDecoratorShadowNode.h b/apple/MarkdownTextInputDecoratorShadowNode.h index c2f19b41..b821ab2d 100644 --- a/apple/MarkdownTextInputDecoratorShadowNode.h +++ b/apple/MarkdownTextInputDecoratorShadowNode.h @@ -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 markdownUtils_; }; } // namespace react diff --git a/apple/MarkdownTextInputDecoratorShadowNode.mm b/apple/MarkdownTextInputDecoratorShadowNode.mm index 43278241..57c88dd0 100644 --- a/apple/MarkdownTextInputDecoratorShadowNode.mm +++ b/apple/MarkdownTextInputDecoratorShadowNode.mm @@ -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(sourceShadowNode); + markdownUtils_ = source.markdownUtils_; + initialize(); makeChildNodeMutable(); @@ -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( + (__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 @@ -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 @@ -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 diff --git a/apple/RCTMarkdownUtils.h b/apple/RCTMarkdownUtils.h index 12fb1ba9..9b290783 100644 --- a/apple/RCTMarkdownUtils.h +++ b/apple/RCTMarkdownUtils.h @@ -11,6 +11,15 @@ NS_ASSUME_NONNULL_BEGIN - (void)applyMarkdownFormatting:(nonnull NSMutableAttributedString *)attributedString withDefaultTextAttributes:(nonnull NSDictionary *)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 *)defaultTextAttributes + markdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle + parserId:(nonnull NSNumber *)parserId; + @end NS_ASSUME_NONNULL_END diff --git a/apple/RCTMarkdownUtils.mm b/apple/RCTMarkdownUtils.mm index 71e14702..b2919d55 100644 --- a/apple/RCTMarkdownUtils.mm +++ b/apple/RCTMarkdownUtils.mm @@ -34,4 +34,22 @@ - (void)applyMarkdownFormatting:(nonnull NSMutableAttributedString *)attributedS withMarkdownStyle:_markdownStyle]; } +- (void)applyMarkdownFormatting:(nonnull NSMutableAttributedString *)attributedString + withDefaultTextAttributes:(nonnull NSDictionary *)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