Skip to content

Commit a96ace4

Browse files
authored
fix(mobile perf): avoid eager emoji palette scans for prose (#8416)
Ordinary mobile messages eagerly built a set of every community emoji shortcode and segmented their entire body just to decide whether to use enlarged emoji rendering. Stop at the first non-emoji grapheme and build the shortcode set only when a complete shortcode is encountered. Emoji-only sizing and shortcode matching stay unchanged. ### Validation - Full `just ci` passed. - 125 focused detector and message-rendering tests pass; targeted analysis is clean. The palette-access regression fails on the original implementation (10,320 unnecessary reads) and passes with the fix. It also checks that consecutive shortcodes share one palette scan. - **Full Buzz app, real community data, debug/JIT iOS simulator, not a rendering fixture:** two paired actual-input captures measured 125–127 µs per original detector call versus under 1 µs with the fix. All 5,200 executions per version agreed across 260 observed prose calls and a 2,580-entry palette. Instrumentation repeated each detector call, so these captures are excluded from frame comparisons. - Separate clean-code scrolling runs do **not** establish a reliable smoothness improvement: quiet original UI-callback p95 was 10.1–12.6 ms versus 10.1–12.9 ms fixed. A busy fixed run still reached 90.8 ms. Normal app services stayed enabled; only scrolling was exercised. Live background refresh, debug mode, host scheduling, and profiler overhead limit interpretation. No physical-device/release-AOT or end-to-end interaction-latency claim. Signed-off-by: Tom Brow <[email protected]>
1 parent 6286a54 commit a96ace4

2 files changed

Lines changed: 72 additions & 20 deletions

File tree

‎mobile/lib/shared/emoji/emoji_only.dart‎

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -44,43 +44,42 @@ bool isEmojiOnlyMessage(
4444
final trimmed = content.trim();
4545
if (trimmed.isEmpty) return false;
4646

47-
final shortcodes = {
48-
for (final emoji in customEmoji) emoji.shortcode.toLowerCase(),
49-
};
47+
Set<String>? shortcodes;
5048
var sawEmoji = false;
5149

5250
// Grapheme clusters, so a ZWJ family or a flag pair is one unit. Desktop
5351
// hand-rolls this scan; Dart's `characters` already segments correctly.
5452
final iterator = trimmed.characters.iterator;
55-
final clusters = <String>[];
5653
while (iterator.moveNext()) {
57-
clusters.add(iterator.current);
58-
}
59-
60-
var index = 0;
61-
while (index < clusters.length) {
62-
final cluster = clusters[index];
54+
final cluster = iterator.current;
6355

64-
if (cluster.trim().isEmpty) {
65-
index += 1;
66-
continue;
67-
}
56+
if (cluster.trim().isEmpty) continue;
6857

6958
if (cluster == ':') {
70-
final end = clusters.indexOf(':', index + 1);
71-
if (end <= index + 1) return false;
72-
final shortcode = clusters.sublist(index + 1, end).join().toLowerCase();
73-
if (!shortcodes.contains(shortcode)) return false;
59+
final shortcode = StringBuffer();
60+
var closed = false;
61+
while (iterator.moveNext()) {
62+
if (iterator.current == ':') {
63+
closed = true;
64+
break;
65+
}
66+
shortcode.write(iterator.current);
67+
}
68+
if (!closed || shortcode.isEmpty) return false;
69+
shortcodes ??= {
70+
for (final emoji in customEmoji) emoji.shortcode.toLowerCase(),
71+
};
72+
if (!shortcodes.contains(shortcode.toString().toLowerCase())) {
73+
return false;
74+
}
7475
sawEmoji = true;
75-
index = end + 1;
7676
continue;
7777
}
7878

7979
if (!nativeEmoji.contains(cluster) && !_pictographic.hasMatch(cluster)) {
8080
return false;
8181
}
8282
sawEmoji = true;
83-
index += 1;
8483
}
8584

8685
return sawEmoji;

‎mobile/test/shared/emoji/emoji_only_test.dart‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:collection';
2+
13
import 'package:buzz/shared/custom_emoji/custom_emoji.dart';
24
import 'package:buzz/shared/emoji/emoji_only.dart';
35
import 'package:flutter_test/flutter_test.dart';
@@ -55,6 +57,34 @@ void main() {
5557
expect(emojiOnly('hi :buzz:', customEmoji: _customEmoji), isFalse);
5658
});
5759

60+
test('scans consecutive shortcodes without skipping following text', () {
61+
expect(emojiOnly(':BUZZ::buzz:', customEmoji: _customEmoji), isTrue);
62+
expect(emojiOnly('😀:buzz:😀', customEmoji: _customEmoji), isTrue);
63+
expect(
64+
emojiOnly(':buzz:ordinary text', customEmoji: _customEmoji),
65+
isFalse,
66+
);
67+
expect(
68+
emojiOnly(':buzz::unfinished', customEmoji: _customEmoji),
69+
isFalse,
70+
);
71+
expect(emojiOnly(':buzz:::', customEmoji: _customEmoji), isFalse);
72+
});
73+
74+
test('does not inspect the community palette without a shortcode', () {
75+
final palette = _CountingEmojiList();
76+
expect(
77+
emojiOnly('Ordinary message text. ' * 500, customEmoji: palette),
78+
isFalse,
79+
);
80+
expect(emojiOnly('😀 👋', customEmoji: palette), isTrue);
81+
expect(emojiOnly('text :buzz:', customEmoji: palette), isFalse);
82+
expect(emojiOnly(':unfinished', customEmoji: palette), isFalse);
83+
expect(palette.reads, 0);
84+
expect(emojiOnly(':buzz::BUZZ:', customEmoji: palette), isTrue);
85+
expect(palette.reads, palette.length);
86+
});
87+
5888
test('an unknown shortcode is just text', () {
5989
// Desktop behaves the same: without the palette it renders literally, so
6090
// it must not be scaled up as if it were a glyph.
@@ -79,3 +109,26 @@ void main() {
79109
});
80110
});
81111
}
112+
113+
class _CountingEmojiList extends ListBase<CustomEmoji> {
114+
int reads = 0;
115+
116+
@override
117+
int get length => 2580;
118+
119+
@override
120+
set length(int value) => throw UnsupportedError('read-only fixture');
121+
122+
@override
123+
CustomEmoji operator [](int index) {
124+
reads += 1;
125+
return CustomEmoji(
126+
shortcode: index == 0 ? 'buzz' : 'unused_$index',
127+
url: 'https://example.test/emoji.png',
128+
);
129+
}
130+
131+
@override
132+
void operator []=(int index, CustomEmoji value) =>
133+
throw UnsupportedError('read-only fixture');
134+
}

0 commit comments

Comments
 (0)