mirror of
https://github.com/block/buzz.git
synced 2026-11-14 13:21:02 +00:00
perf(mobile): parse each event's thread reference once (#9460)
Every time code asked an event for its parent or thread root (`threadReference`, `parentEventId`), `parseThreadReference` walked its tags again and ran a 64-character hex regex over each `e` tag. The channel store asks that many times per event: - `_trimReplyOverlay` runs on every live event merged into the window and re-parsed every overlay event, twice. - `_reconcilePageSummaries` called `cachedThreadReplyIds(root)` once per row of a fetched page, and each call re-parsed every cached reply (twice per reply) to find that root's replies. A 200-row page did 200 full passes. Now: - `parseThreadReference` keeps its result per tag list in an `Expando`. Event tags are never changed after an event is built (nothing in `lib` mutates a `NostrEvent`'s tags), so the cached value always matches. - `_reconcilePageSummaries` indexes cached replies by root once, then reads each row's count from that map. `cachedThreadReplyIds` keeps its signature and result and shares the same code. ### Validation Full mobile test suite and `flutter analyze` pass. Existing thread-summary, reply-overlay and thread-reference tests cover the unchanged results. On the iPhone 12 mini (profile build, Builderlab), same scripted run on `main` and on this branch: open three threads from Activity, back out after each. | Dart CPU in thread-reference code | main | this branch | | --- | --- | --- | | First thread (not cached yet) | 5 ms | 2 ms | | Second thread (loads the channel window) | 27 ms, plus 18 ms trimming the reply overlay | 3 ms, plus 2 ms | | Third thread | 10 ms | 0 ms | The threads opened and showed the same replies in both runs, and the run covered a channel window load, live events arriving while a thread was open, and backing out to Home. ### Regressions None expected. Results are identical; the cache holds one small record per live tag list and is released with the event. Signed-off-by: Tom Brow <tomb@squareup.com> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -624,7 +624,11 @@ class ChannelMessagesNotifier extends Notifier<AsyncValue<List<NostrEvent>>> {
|
||||
_rotateDeletionProofTargets(missingIds);
|
||||
|
||||
/// Snapshots cached overlay replies covered by a query for this outer root.
|
||||
Set<String> cachedThreadReplyIds(String rootId) {
|
||||
Set<String> cachedThreadReplyIds(String rootId) =>
|
||||
_cachedThreadReplyIdsByRoot(only: rootId)[rootId] ?? <String>{};
|
||||
|
||||
/// Snapshots cached overlay replies for every outer root, or only [only].
|
||||
Map<String, Set<String>> _cachedThreadReplyIdsByRoot({String? only}) {
|
||||
final deletedIds = {
|
||||
for (final event in [
|
||||
..._windowStore.liveAux,
|
||||
@@ -636,15 +640,21 @@ class ChannelMessagesNotifier extends Notifier<AsyncValue<List<NostrEvent>>> {
|
||||
for (final tag in event.tags)
|
||||
if (tag.length > 1 && tag[0] == 'e') tag[1],
|
||||
};
|
||||
return {
|
||||
for (final entry in _localReplyRoots.entries)
|
||||
if (entry.value == rootId && !deletedIds.contains(entry.key)) entry.key,
|
||||
for (final event in _cachedThreadEvents)
|
||||
if (event.threadReference.parentId != null &&
|
||||
event.threadReference.rootId == rootId &&
|
||||
!deletedIds.contains(event.id))
|
||||
event.id,
|
||||
};
|
||||
final byRoot = <String, Set<String>>{};
|
||||
void add(String root, String id) {
|
||||
if ((only == null || root == only) && !deletedIds.contains(id)) {
|
||||
(byRoot[root] ??= <String>{}).add(id);
|
||||
}
|
||||
}
|
||||
|
||||
for (final entry in _localReplyRoots.entries) {
|
||||
add(entry.value, entry.key);
|
||||
}
|
||||
for (final event in _cachedThreadEvents) {
|
||||
final thread = event.threadReference;
|
||||
if (thread.parentId != null) add(thread.rootId!, event.id);
|
||||
}
|
||||
return byRoot;
|
||||
}
|
||||
|
||||
/// Applies explicit deletion evidence fetched for cached replies.
|
||||
|
||||
@@ -2,6 +2,8 @@ part of '../channel_messages_provider.dart';
|
||||
|
||||
extension _ThreadSummaryState on ChannelMessagesNotifier {
|
||||
void _reconcilePageSummaries(ChannelWindowPage page, int pageVersion) {
|
||||
// Nothing below changes the cached replies, so index them once per page.
|
||||
final cachedReplies = _cachedThreadReplyIdsByRoot();
|
||||
for (final row in page.rows) {
|
||||
final root = row.event.id;
|
||||
if ((_threadEvidenceVersions[root] ?? 0) > pageVersion) continue;
|
||||
@@ -10,7 +12,7 @@ extension _ThreadSummaryState on ChannelMessagesNotifier {
|
||||
_summaryRefreshes.cancel(root);
|
||||
_setThreadQueryVersion(root, pageVersion);
|
||||
_clearDeletionUncertainty(root, pageVersion);
|
||||
if (cachedThreadReplyIds(root).length >
|
||||
if ((cachedReplies[root]?.length ?? 0) >
|
||||
(row.thread?.descendantCount ?? 0)) {
|
||||
_queueOverflowSummary(root);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,18 @@
|
||||
/// The last valid 64-character ASCII-hex marker wins. IDs are lowercased for
|
||||
/// client lookup. Root-only events remain top-level; a reply without a valid
|
||||
/// root uses its parent as root.
|
||||
///
|
||||
/// Timeline and thread-summary code reads this many times per event, so the
|
||||
/// result is kept per tag list. Event tags are never changed after an event is
|
||||
/// built.
|
||||
({String? parentId, String? rootId}) parseThreadReference(
|
||||
List<List<String>> tags,
|
||||
) => _threadReferences[tags] ??= _parseThreadReference(tags);
|
||||
|
||||
final _threadReferences = Expando<({String? parentId, String? rootId})>();
|
||||
|
||||
({String? parentId, String? rootId}) _parseThreadReference(
|
||||
List<List<String>> tags,
|
||||
) {
|
||||
String? parentId;
|
||||
String? rootId;
|
||||
|
||||
Reference in New Issue
Block a user