mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
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 <tomb@block.xyz>
This commit is contained in:
@@ -44,35 +44,35 @@ bool isEmojiOnlyMessage(
|
||||
final trimmed = content.trim();
|
||||
if (trimmed.isEmpty) return false;
|
||||
|
||||
final shortcodes = {
|
||||
for (final emoji in customEmoji) emoji.shortcode.toLowerCase(),
|
||||
};
|
||||
Set<String>? shortcodes;
|
||||
var sawEmoji = false;
|
||||
|
||||
// Grapheme clusters, so a ZWJ family or a flag pair is one unit. Desktop
|
||||
// hand-rolls this scan; Dart's `characters` already segments correctly.
|
||||
final iterator = trimmed.characters.iterator;
|
||||
final clusters = <String>[];
|
||||
while (iterator.moveNext()) {
|
||||
clusters.add(iterator.current);
|
||||
}
|
||||
final cluster = iterator.current;
|
||||
|
||||
var index = 0;
|
||||
while (index < clusters.length) {
|
||||
final cluster = clusters[index];
|
||||
|
||||
if (cluster.trim().isEmpty) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (cluster.trim().isEmpty) continue;
|
||||
|
||||
if (cluster == ':') {
|
||||
final end = clusters.indexOf(':', index + 1);
|
||||
if (end <= index + 1) return false;
|
||||
final shortcode = clusters.sublist(index + 1, end).join().toLowerCase();
|
||||
if (!shortcodes.contains(shortcode)) return false;
|
||||
final shortcode = StringBuffer();
|
||||
var closed = false;
|
||||
while (iterator.moveNext()) {
|
||||
if (iterator.current == ':') {
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
shortcode.write(iterator.current);
|
||||
}
|
||||
if (!closed || shortcode.isEmpty) return false;
|
||||
shortcodes ??= {
|
||||
for (final emoji in customEmoji) emoji.shortcode.toLowerCase(),
|
||||
};
|
||||
if (!shortcodes.contains(shortcode.toString().toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
sawEmoji = true;
|
||||
index = end + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -80,7 +80,6 @@ bool isEmojiOnlyMessage(
|
||||
return false;
|
||||
}
|
||||
sawEmoji = true;
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return sawEmoji;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:buzz/shared/custom_emoji/custom_emoji.dart';
|
||||
import 'package:buzz/shared/emoji/emoji_only.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -55,6 +57,34 @@ void main() {
|
||||
expect(emojiOnly('hi :buzz:', customEmoji: _customEmoji), isFalse);
|
||||
});
|
||||
|
||||
test('scans consecutive shortcodes without skipping following text', () {
|
||||
expect(emojiOnly(':BUZZ::buzz:', customEmoji: _customEmoji), isTrue);
|
||||
expect(emojiOnly('😀:buzz:😀', customEmoji: _customEmoji), isTrue);
|
||||
expect(
|
||||
emojiOnly(':buzz:ordinary text', customEmoji: _customEmoji),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
emojiOnly(':buzz::unfinished', customEmoji: _customEmoji),
|
||||
isFalse,
|
||||
);
|
||||
expect(emojiOnly(':buzz:::', customEmoji: _customEmoji), isFalse);
|
||||
});
|
||||
|
||||
test('does not inspect the community palette without a shortcode', () {
|
||||
final palette = _CountingEmojiList();
|
||||
expect(
|
||||
emojiOnly('Ordinary message text. ' * 500, customEmoji: palette),
|
||||
isFalse,
|
||||
);
|
||||
expect(emojiOnly('😀 👋', customEmoji: palette), isTrue);
|
||||
expect(emojiOnly('text :buzz:', customEmoji: palette), isFalse);
|
||||
expect(emojiOnly(':unfinished', customEmoji: palette), isFalse);
|
||||
expect(palette.reads, 0);
|
||||
expect(emojiOnly(':buzz::BUZZ:', customEmoji: palette), isTrue);
|
||||
expect(palette.reads, palette.length);
|
||||
});
|
||||
|
||||
test('an unknown shortcode is just text', () {
|
||||
// Desktop behaves the same: without the palette it renders literally, so
|
||||
// it must not be scaled up as if it were a glyph.
|
||||
@@ -79,3 +109,26 @@ void main() {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _CountingEmojiList extends ListBase<CustomEmoji> {
|
||||
int reads = 0;
|
||||
|
||||
@override
|
||||
int get length => 2580;
|
||||
|
||||
@override
|
||||
set length(int value) => throw UnsupportedError('read-only fixture');
|
||||
|
||||
@override
|
||||
CustomEmoji operator [](int index) {
|
||||
reads += 1;
|
||||
return CustomEmoji(
|
||||
shortcode: index == 0 ? 'buzz' : 'unused_$index',
|
||||
url: 'https://example.test/emoji.png',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void operator []=(int index, CustomEmoji value) =>
|
||||
throw UnsupportedError('read-only fixture');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user