Skip to content

Commit cfdcdee

Browse files
committed
refactor(ios): change segment signature type from NSString to uint64_t for improved performance
1 parent 325aaba commit cfdcdee

6 files changed

Lines changed: 83 additions & 39 deletions

File tree

ios/EnrichedMarkdown.mm

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ @implementation EnrichedMarkdown {
6565
NSString *_cachedMarkdown;
6666
NSString *_renderedMarkdown;
6767
NSMutableArray<RCTUIView *> *_segmentViews;
68-
NSMutableArray<NSString *> *_segmentSignatures;
68+
NSMutableArray<NSNumber *> *_segmentSignatures;
6969
ENRMSegmentViewRegistry *_segmentViewRegistry;
7070
BOOL _forceRecreateSegments;
7171
BOOL _forceHeightUpdateOnNextRender;
@@ -423,7 +423,7 @@ - (void)renderMarkdownSynchronously:(NSString *)markdownString
423423
for (ENRMRenderedSegment *segment in renderedSegments) {
424424
RCTUIView *view = [_segmentViewRegistry createViewForSegment:segment animateIfStreaming:NO];
425425
[_segmentViews addObject:view];
426-
[_segmentSignatures addObject:segment.signature ?: @""];
426+
[_segmentSignatures addObject:@(segment.signature)];
427427
[self addSubview:view];
428428
}
429429
}

ios/utils/RenderedMarkdownSegment.h

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,21 @@ typedef NS_ENUM(NSInteger, ENRMSegmentKind) { ENRMSegmentKindText, ENRMSegmentKi
2626

2727
@interface ENRMRenderedSegment : NSObject
2828
@property (nonatomic, assign) ENRMSegmentKind kind;
29-
@property (nonatomic, copy) NSString *signature;
29+
@property (nonatomic, assign) uint64_t signature;
3030
@property (nonatomic, strong, nullable) ENRMRenderResult *textResult;
3131
@property (nonatomic, strong, nullable) ENRMTableSegment *tableSegment;
3232
@property (nonatomic, strong, nullable) ENRMMathSegment *mathSegment;
33-
+ (instancetype)textSegmentWithResult:(ENRMRenderResult *)result signature:(NSString *)signature;
34-
+ (instancetype)tableSegmentWithSegment:(ENRMTableSegment *)segment signature:(NSString *)signature;
35-
+ (instancetype)mathSegmentWithSegment:(ENRMMathSegment *)segment signature:(NSString *)signature;
33+
+ (instancetype)textSegmentWithResult:(ENRMRenderResult *)result signature:(uint64_t)signature;
34+
+ (instancetype)tableSegmentWithSegment:(ENRMTableSegment *)segment signature:(uint64_t)signature;
35+
+ (instancetype)mathSegmentWithSegment:(ENRMMathSegment *)segment signature:(uint64_t)signature;
3636
@end
3737

3838
#ifdef __cplusplus
3939
extern "C" {
4040
#endif
4141

42-
NSString *ENRMSignatureForNode(MarkdownASTNode *_Nullable node);
43-
NSString *ENRMSignatureForNodes(NSArray<MarkdownASTNode *> *nodes);
42+
uint64_t ENRMSignatureForNode(MarkdownASTNode *_Nullable node);
43+
uint64_t ENRMSignatureForNodes(NSArray<MarkdownASTNode *> *nodes);
4444

4545
#ifdef __cplusplus
4646
}

ios/utils/RenderedMarkdownSegment.m

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,32 +32,29 @@ + (instancetype)segmentWithLatex:(NSString *)latex
3232
@end
3333

3434
@implementation ENRMRenderedSegment
35-
+ (instancetype)textSegmentWithResult:(ENRMRenderResult *)result signature:(NSString *)signature
35+
+ (instancetype)textSegmentWithResult:(ENRMRenderResult *)result signature:(uint64_t)signature
3636
{
3737
NSParameterAssert(result != nil);
38-
NSParameterAssert(signature != nil);
3938
ENRMRenderedSegment *segment = [[ENRMRenderedSegment alloc] init];
4039
segment.kind = ENRMSegmentKindText;
4140
segment.textResult = result;
4241
segment.signature = signature;
4342
return segment;
4443
}
4544

46-
+ (instancetype)tableSegmentWithSegment:(ENRMTableSegment *)tableSegment signature:(NSString *)signature
45+
+ (instancetype)tableSegmentWithSegment:(ENRMTableSegment *)tableSegment signature:(uint64_t)signature
4746
{
4847
NSParameterAssert(tableSegment != nil);
49-
NSParameterAssert(signature != nil);
5048
ENRMRenderedSegment *segment = [[ENRMRenderedSegment alloc] init];
5149
segment.kind = ENRMSegmentKindTable;
5250
segment.tableSegment = tableSegment;
5351
segment.signature = signature;
5452
return segment;
5553
}
5654

57-
+ (instancetype)mathSegmentWithSegment:(ENRMMathSegment *)mathSegment signature:(NSString *)signature
55+
+ (instancetype)mathSegmentWithSegment:(ENRMMathSegment *)mathSegment signature:(uint64_t)signature
5856
{
5957
NSParameterAssert(mathSegment != nil);
60-
NSParameterAssert(signature != nil);
6158
ENRMRenderedSegment *segment = [[ENRMRenderedSegment alloc] init];
6259
segment.kind = ENRMSegmentKindMath;
6360
segment.mathSegment = mathSegment;
@@ -66,31 +63,68 @@ + (instancetype)mathSegmentWithSegment:(ENRMMathSegment *)mathSegment signature:
6663
}
6764
@end
6865

69-
NSString *ENRMSignatureForNode(MarkdownASTNode *node)
66+
// FNV-1a 64-bit hash for segment signatures. Collisions are theoretically
67+
// possible but negligible for the small number of segments per document
68+
// (~single digits). Worst case is a single skipped view update, corrected
69+
// on the next streaming tick. This replaces the previous approach of building
70+
// and comparing multi-KB NSString signatures from the full AST subtree.
71+
static const uint64_t kFNVOffsetBasis = 14695981039346656037ULL;
72+
static const uint64_t kFNVPrime = 1099511628211ULL;
73+
74+
static inline uint64_t fnvMixByte(uint64_t hash, uint8_t byte)
75+
{
76+
hash ^= byte;
77+
hash *= kFNVPrime;
78+
return hash;
79+
}
80+
81+
static inline uint64_t fnvMixUInt64(uint64_t hash, uint64_t value)
82+
{
83+
for (int i = 0; i < 8; i++) {
84+
hash = fnvMixByte(hash, (uint8_t)(value & 0xFF));
85+
value >>= 8;
86+
}
87+
return hash;
88+
}
89+
90+
static inline uint64_t fnvMixString(uint64_t hash, NSString *string)
91+
{
92+
if (!string)
93+
return hash;
94+
const char *utf8 = [string UTF8String];
95+
while (*utf8) {
96+
hash = fnvMixByte(hash, (uint8_t)*utf8++);
97+
}
98+
return hash;
99+
}
100+
101+
uint64_t ENRMSignatureForNode(MarkdownASTNode *node)
70102
{
71103
if (!node)
72-
return @"";
104+
return kFNVOffsetBasis;
105+
106+
uint64_t hash = kFNVOffsetBasis;
107+
hash = fnvMixUInt64(hash, (uint64_t)node.type);
108+
hash = fnvMixString(hash, node.content);
73109

74-
NSMutableString *signature = [NSMutableString stringWithFormat:@"%ld|%@|", (long)node.type, node.content ?: @""];
75110
NSArray *keys = [[node.attributes allKeys] sortedArrayUsingSelector:@selector(compare:)];
76111
for (NSString *key in keys) {
77-
[signature appendFormat:@"%@=%@;", key, node.attributes[key]];
112+
hash = fnvMixString(hash, key);
113+
hash = fnvMixString(hash, node.attributes[key]);
78114
}
79-
[signature appendString:@"["];
115+
80116
for (MarkdownASTNode *child in node.children) {
81-
[signature appendString:ENRMSignatureForNode(child)];
82-
[signature appendString:@","];
117+
hash = fnvMixUInt64(hash, ENRMSignatureForNode(child));
83118
}
84-
[signature appendString:@"]"];
85-
return signature;
119+
120+
return hash;
86121
}
87122

88-
NSString *ENRMSignatureForNodes(NSArray<MarkdownASTNode *> *nodes)
123+
uint64_t ENRMSignatureForNodes(NSArray<MarkdownASTNode *> *nodes)
89124
{
90-
NSMutableString *signature = [NSMutableString string];
125+
uint64_t hash = kFNVOffsetBasis;
91126
for (MarkdownASTNode *node in nodes) {
92-
[signature appendString:ENRMSignatureForNode(node)];
93-
[signature appendString:@"|"];
127+
hash = fnvMixUInt64(hash, ENRMSignatureForNode(node));
94128
}
95-
return signature;
129+
return hash;
96130
}

ios/utils/SegmentReconciler.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ NS_ASSUME_NONNULL_BEGIN
88

99
@interface ENRMSegmentReconciliationResult : NSObject
1010
@property (nonatomic, strong) NSMutableArray<RCTUIView *> *views;
11-
@property (nonatomic, strong) NSMutableArray<NSString *> *signatures;
11+
@property (nonatomic, strong) NSMutableArray<NSNumber *> *signatures;
1212
@end
1313

1414
@interface ENRMSegmentReconciler : NSObject
1515
// Reuses views by index when the segment kind still matches. Matching
1616
// signatures skip updates; changed signatures update the existing view.
1717
+ (ENRMSegmentReconciliationResult *)
1818
reconcileCurrentViews:(NSArray<RCTUIView *> *)currentViews
19-
currentSignatures:(NSArray<NSString *> *)currentSignatures
19+
currentSignatures:(NSArray<NSNumber *> *)currentSignatures
2020
renderedSegments:(NSArray<ENRMRenderedSegment *> *)renderedSegments
2121
reset:(BOOL)reset
2222
createView:(RCTUIView * (^)(ENRMRenderedSegment *segment))createView

ios/utils/SegmentReconciler.m

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ @implementation ENRMSegmentReconciliationResult
77
@implementation ENRMSegmentReconciler
88
+ (ENRMSegmentReconciliationResult *)
99
reconcileCurrentViews:(NSArray<RCTUIView *> *)currentViews
10-
currentSignatures:(NSArray<NSString *> *)currentSignatures
10+
currentSignatures:(NSArray<NSNumber *> *)currentSignatures
1111
renderedSegments:(NSArray<ENRMRenderedSegment *> *)renderedSegments
1212
reset:(BOOL)reset
1313
createView:(RCTUIView * (^)(ENRMRenderedSegment *segment))createView
@@ -17,7 +17,7 @@ @implementation ENRMSegmentReconciler
1717
matchesKind:(BOOL (^)(RCTUIView *view, ENRMRenderedSegment *segment))matchesKind
1818
{
1919
NSArray<RCTUIView *> *sourceViews = currentViews;
20-
NSArray<NSString *> *sourceSignatures = currentSignatures;
20+
NSArray<NSNumber *> *sourceSignatures = currentSignatures;
2121

2222
if (reset) {
2323
[currentViews enumerateObjectsUsingBlock:^(RCTUIView *view, NSUInteger index, BOOL *stop) { removeView(view); }];
@@ -26,17 +26,17 @@ @implementation ENRMSegmentReconciler
2626
}
2727

2828
NSMutableArray<RCTUIView *> *nextViews = [NSMutableArray arrayWithCapacity:renderedSegments.count];
29-
NSMutableArray<NSString *> *nextSignatures = [NSMutableArray arrayWithCapacity:renderedSegments.count];
29+
NSMutableArray<NSNumber *> *nextSignatures = [NSMutableArray arrayWithCapacity:renderedSegments.count];
3030
NSMutableSet<RCTUIView *> *reusedViews = [NSMutableSet setWithCapacity:sourceViews.count];
3131

3232
[renderedSegments enumerateObjectsUsingBlock:^(ENRMRenderedSegment *segment, NSUInteger index, BOOL *stop) {
3333
RCTUIView *existingView = index < sourceViews.count ? sourceViews[index] : nil;
34-
NSString *existingSignature = index < sourceSignatures.count ? sourceSignatures[index] : nil;
34+
NSNumber *existingSignature = index < sourceSignatures.count ? sourceSignatures[index] : nil;
3535
RCTUIView *view = nil;
36-
NSString *nextSignature = segment.signature ?: @"";
36+
NSNumber *nextSignature = @(segment.signature);
3737

3838
if (existingView && matchesKind(existingView, segment)) {
39-
if (![existingSignature isEqualToString:nextSignature]) {
39+
if (![existingSignature isEqual:nextSignature]) {
4040
updateView(existingView, segment);
4141
}
4242
view = existingView;

ios/utils/SegmentRenderer.m

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,22 +54,32 @@
5454
NSArray *segments = ENRMSplitASTIntoSegments(ast);
5555
NSMutableArray<ENRMRenderedSegment *> *renderedSegments = [NSMutableArray array];
5656

57+
static const uint64_t kTextKindSalt = 0x7465787400000000ULL; // "text"
58+
static const uint64_t kTableKindSalt = 0x7461626C00000000ULL; // "tabl"
59+
static const uint64_t kMathKindSalt = 0x6D61746800000000ULL; // "math"
60+
5761
for (id segment in segments) {
5862
if ([segment isKindOfClass:[ENRMTextSegment class]]) {
5963
ENRMTextSegment *textSegment = (ENRMTextSegment *)segment;
6064
ENRMRenderResult *rendered = ENRMRenderASTNodes(textSegment.nodes, config, allowTrailingMargin, allowFontScaling,
6165
maxFontSizeMultiplier, currentWritingDirection());
62-
NSString *signature = [@"text:" stringByAppendingString:ENRMSignatureForNodes(textSegment.nodes)];
66+
uint64_t signature = ENRMSignatureForNodes(textSegment.nodes) ^ kTextKindSalt;
6367
[renderedSegments addObject:[ENRMRenderedSegment textSegmentWithResult:rendered signature:signature]];
6468
} else if ([segment isKindOfClass:[ENRMTableSegment class]]) {
6569
ENRMTableSegment *tableSegment = (ENRMTableSegment *)segment;
66-
NSString *signature = [@"table:" stringByAppendingString:ENRMSignatureForNode(tableSegment.tableNode)];
70+
uint64_t signature = ENRMSignatureForNode(tableSegment.tableNode) ^ kTableKindSalt;
6771
[renderedSegments addObject:[ENRMRenderedSegment tableSegmentWithSegment:tableSegment signature:signature]];
6872
}
6973
#if ENRICHED_MARKDOWN_MATH
7074
else if ([segment isKindOfClass:[ENRMMathSegment class]]) {
7175
ENRMMathSegment *mathSegment = (ENRMMathSegment *)segment;
72-
NSString *signature = [@"math:" stringByAppendingString:mathSegment.latex ?: @""];
76+
uint64_t signature = ENRMSignatureForNode(nil) ^ kMathKindSalt;
77+
NSString *latex = mathSegment.latex ?: @"";
78+
const char *utf8 = [latex UTF8String];
79+
while (*utf8) {
80+
signature ^= (uint8_t)*utf8++;
81+
signature *= 1099511628211ULL;
82+
}
7383
[renderedSegments addObject:[ENRMRenderedSegment mathSegmentWithSegment:mathSegment signature:signature]];
7484
}
7585
#endif

0 commit comments

Comments
 (0)