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();
|
final trimmed = content.trim();
|
||||||
if (trimmed.isEmpty) return false;
|
if (trimmed.isEmpty) return false;
|
||||||
|
|
||||||
final shortcodes = {
|
Set<String>? shortcodes;
|
||||||
for (final emoji in customEmoji) emoji.shortcode.toLowerCase(),
|
|
||||||
};
|
|
||||||
var sawEmoji = false;
|
var sawEmoji = false;
|
||||||
|
|
||||||
// Grapheme clusters, so a ZWJ family or a flag pair is one unit. Desktop
|
// Grapheme clusters, so a ZWJ family or a flag pair is one unit. Desktop
|
||||||
// hand-rolls this scan; Dart's `characters` already segments correctly.
|
// hand-rolls this scan; Dart's `characters` already segments correctly.
|
||||||
final iterator = trimmed.characters.iterator;
|
final iterator = trimmed.characters.iterator;
|
||||||
final clusters = <String>[];
|
|
||||||
while (iterator.moveNext()) {
|
while (iterator.moveNext()) {
|
||||||
clusters.add(iterator.current);
|
final cluster = iterator.current;
|
||||||
}
|
|
||||||
|
|
||||||
var index = 0;
|
if (cluster.trim().isEmpty) continue;
|
||||||
while (index < clusters.length) {
|
|
||||||
final cluster = clusters[index];
|
|
||||||
|
|
||||||
if (cluster.trim().isEmpty) {
|
|
||||||
index += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cluster == ':') {
|
if (cluster == ':') {
|
||||||
final end = clusters.indexOf(':', index + 1);
|
final shortcode = StringBuffer();
|
||||||
if (end <= index + 1) return false;
|
var closed = false;
|
||||||
final shortcode = clusters.sublist(index + 1, end).join().toLowerCase();
|
while (iterator.moveNext()) {
|
||||||
if (!shortcodes.contains(shortcode)) return false;
|
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;
|
sawEmoji = true;
|
||||||
index = end + 1;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +80,6 @@ bool isEmojiOnlyMessage(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
sawEmoji = true;
|
sawEmoji = true;
|
||||||
index += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sawEmoji;
|
return sawEmoji;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:buzz/shared/custom_emoji/custom_emoji.dart';
|
import 'package:buzz/shared/custom_emoji/custom_emoji.dart';
|
||||||
import 'package:buzz/shared/emoji/emoji_only.dart';
|
import 'package:buzz/shared/emoji/emoji_only.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@@ -55,6 +57,34 @@ void main() {
|
|||||||
expect(emojiOnly('hi :buzz:', customEmoji: _customEmoji), isFalse);
|
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', () {
|
test('an unknown shortcode is just text', () {
|
||||||
// Desktop behaves the same: without the palette it renders literally, so
|
// Desktop behaves the same: without the palette it renders literally, so
|
||||||
// it must not be scaled up as if it were a glyph.
|
// 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