Skip to content

Commit ecc8175

Browse files
authored
fix(ios): persist RCTMarkdownUtils across shadow node clones to prevent AppHang (#758)
1 parent c54eb86 commit ecc8175

4 files changed

Lines changed: 60 additions & 6 deletions

File tree

apple/MarkdownTextInputDecoratorShadowNode.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ class JSI_EXPORT MarkdownTextInputDecoratorShadowNode final
4848
YGMeasureMode heightMode);
4949
static YogaLayoutableShadowNode &
5050
shadowNodeFromContext(YGNodeConstRef yogaNode);
51+
52+
// Persisted RCTMarkdownUtils instance shared across shadow node clones so
53+
// that MarkdownParser's one-entry memo cache (keyed on text + parserId)
54+
// survives repeated Yoga measure callbacks instead of being discarded on
55+
// every call to applyMarkdownFormattingToTextInputState.
56+
mutable std::shared_ptr<void> markdownUtils_;
5157
};
5258

5359
} // namespace react

apple/MarkdownTextInputDecoratorShadowNode.mm

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@
3232
ShadowNode const &sourceShadowNode,
3333
ShadowNodeFragment const &fragment)
3434
: ConcreteViewShadowNode(sourceShadowNode, fragment) {
35+
// Carry the persisted RCTMarkdownUtils over from the source node so the
36+
// MarkdownParser memo cache survives the frequent cloning that happens
37+
// during layout and re-render cycles.
38+
const auto &source =
39+
static_cast<const MarkdownTextInputDecoratorShadowNode &>(sourceShadowNode);
40+
markdownUtils_ = source.markdownUtils_;
41+
3542
initialize();
3643
makeChildNodeMutable();
3744

@@ -182,12 +189,20 @@
182189
const auto defaultNSTextAttributes =
183190
RCTNSTextAttributesFromTextAttributes(defaultTextAttributes);
184191

185-
// this can possibly be optimized
192+
// Lazily create and persist the RCTMarkdownUtils instance so the MarkdownParser
193+
// one-entry memo cache (keyed on text + parserId) survives repeated Yoga measure
194+
// callbacks. Previously a fresh utils/parser was allocated on every call,
195+
// discarding the cache and forcing a full JSI re-parse each time.
196+
if (!markdownUtils_) {
197+
RCTMarkdownUtils *freshUtils = [[RCTMarkdownUtils alloc] init];
198+
markdownUtils_ = std::shared_ptr<void>(
199+
(__bridge_retained void *)freshUtils, [](void *p) { CFRelease(p); });
200+
}
201+
RCTMarkdownUtils *utils = (__bridge RCTMarkdownUtils *)markdownUtils_.get();
202+
186203
RCTMarkdownStyle *markdownStyle =
187204
[[RCTMarkdownStyle alloc] initWithStruct:decoratorProps.markdownStyle];
188-
RCTMarkdownUtils *utils = [[RCTMarkdownUtils alloc] init];
189-
[utils setMarkdownStyle:markdownStyle];
190-
[utils setParserId:[NSNumber numberWithInt:decoratorProps.parserId]];
205+
NSNumber *parserId = [NSNumber numberWithInt:decoratorProps.parserId];
191206

192207
// convert the attibuted string stored in state to
193208
// NSAttributedString
@@ -228,7 +243,10 @@
228243

229244
// apply markdown
230245
NSMutableAttributedString *newString = [nsAttributedString mutableCopy];
231-
[utils applyMarkdownFormatting:newString withDefaultTextAttributes:defaultNSTextAttributes];
246+
[utils applyMarkdownFormatting:newString
247+
withDefaultTextAttributes:defaultNSTextAttributes
248+
markdownStyle:markdownStyle
249+
parserId:parserId];
232250

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

241259
// apply markdown
242260
NSMutableAttributedString *newString = [nsAttributedString mutableCopy];
243-
[utils applyMarkdownFormatting:newString withDefaultTextAttributes:defaultNSTextAttributes];
261+
[utils applyMarkdownFormatting:newString
262+
withDefaultTextAttributes:defaultNSTextAttributes
263+
markdownStyle:markdownStyle
264+
parserId:parserId];
244265

245266
// create a clone of the old TextInputState and update the
246267
// attributed string box to point to the string with markdown

apple/RCTMarkdownUtils.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ NS_ASSUME_NONNULL_BEGIN
1111
- (void)applyMarkdownFormatting:(nonnull NSMutableAttributedString *)attributedString
1212
withDefaultTextAttributes:(nonnull NSDictionary<NSAttributedStringKey, id> *)defaultTextAttributes;
1313

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

1625
NS_ASSUME_NONNULL_END

apple/RCTMarkdownUtils.mm

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,22 @@ - (void)applyMarkdownFormatting:(nonnull NSMutableAttributedString *)attributedS
3434
withMarkdownStyle:_markdownStyle];
3535
}
3636

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

0 commit comments

Comments
 (0)