From 09cb08e5c7abed867b6ef0a5ce55da2e0aa6ae72 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Thu, 30 Apr 2026 09:50:12 +0300 Subject: [PATCH] perf(cache): O(1) author lookup for metadata invalidation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit consumeMetadata() was looping ALL notes in cache (O(N)) to find notes by the author whose metadata changed. With hundreds of cached notes, this caused lag on every metadata event. Added notesByAuthor index (ConcurrentHashMap>) populated at loadEvent time. consumeMetadata now looks up notes by author in O(1) instead of scanning the entire cache. Before: ~500 notes × per metadata event = lag After: ~3 notes per author × per metadata event = instant Co-Authored-By: Claude Opus 4.6 (1M context) --- .../desktop/cache/DesktopLocalCache.kt | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt index 87128a75b..d1b9636a4 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt @@ -85,8 +85,18 @@ class DesktopLocalCache : ICacheProvider { companion object { } + /** Index of notes by author pubkey — for fast metadata invalidation */ + private val notesByAuthor = ConcurrentHashMap>() + val paymentTracker = NwcPaymentTracker() + private fun trackNoteAuthor( + note: Note, + authorPubkey: HexKey, + ) { + notesByAuthor.getOrPut(authorPubkey) { ConcurrentHashMap.newKeySet() }.add(note) + } + // ----- User operations ----- override fun getUserIfExists(pubkey: HexKey): User? = users.get(pubkey) @@ -156,10 +166,9 @@ class DesktopLocalCache : ICacheProvider { if (newUserMetadata != null) { user.updateUserInfo(newUserMetadata, event) _metadataVersion.value++ - // Invalidate metadata flows on notes by this author that have observers - // so QuotedNoteEmbed/FeedNoteCard recompose with updated avatar/name - notes.forEach { _, note -> - if (note.author?.pubkeyHex == event.pubKey && note.flowSet?.metadata?.hasObservers() == true) { + // Invalidate metadata flows on notes by this author (O(K) via index) + notesByAuthor[event.pubKey]?.forEach { note -> + if (note.flowSet?.metadata?.hasObservers() == true) { note.flowSet?.metadata?.invalidateData() } } @@ -245,6 +254,7 @@ class DesktopLocalCache : ICacheProvider { val author = getOrCreateUser(event.pubKey) val repliesTo = event.tagsWithoutCitations().map { getOrCreateNote(it) } note.loadEvent(event, author, repliesTo) + trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } repliesTo.forEach { it.addReply(note) } return true @@ -263,6 +273,7 @@ class DesktopLocalCache : ICacheProvider { val author = getOrCreateUser(event.pubKey) val repliesTo = event.tagsWithoutCitations().map { getOrCreateNote(it) } note.loadEvent(event, author, repliesTo) + trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } repliesTo.forEach { it.addReply(note) } return true @@ -283,6 +294,7 @@ class DesktopLocalCache : ICacheProvider { event.originalPost().mapNotNull { getNoteIfExists(it) } + event.taggedAddresses().mapNotNull { addressableNotes.get(it.toValue()) } note.loadEvent(event, author, reactedTo) + trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } reactedTo.forEach { it.addReaction(note) } return true @@ -300,6 +312,7 @@ class DesktopLocalCache : ICacheProvider { if (note.event != null) return false val author = getOrCreateUser(event.pubKey) note.loadEvent(event, author, emptyList()) + trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } return true } @@ -331,6 +344,7 @@ class DesktopLocalCache : ICacheProvider { event.taggedAddresses().mapNotNull { addressableNotes.get(it.toValue()) } note.loadEvent(event, author, zappedNotes) + trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } // Link zap to target notes @@ -358,6 +372,7 @@ class DesktopLocalCache : ICacheProvider { val boostedNote = boostedId?.let { getOrCreateNote(it) } val repliesTo = listOfNotNull(boostedNote) note.loadEvent(event, author, repliesTo) + trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } boostedNote?.addBoost(note) return true @@ -377,6 +392,7 @@ class DesktopLocalCache : ICacheProvider { val boostedNote = event.boostedEventId()?.let { getOrCreateNote(it) } val repliesTo = listOfNotNull(boostedNote) note.loadEvent(event, author, repliesTo) + trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } boostedNote?.addBoost(note) return true @@ -408,6 +424,7 @@ class DesktopLocalCache : ICacheProvider { if (note.event != null) return false val author = getOrCreateUser(event.pubKey) note.loadEvent(event, author, emptyList()) + trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } return true } @@ -467,6 +484,7 @@ class DesktopLocalCache : ICacheProvider { if (note.event != null) return false note.loadEvent(event, author, emptyList()) + trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } zappedNote?.addZapPayment(note, null) @@ -498,6 +516,7 @@ class DesktopLocalCache : ICacheProvider { if (note.event != null) return false note.loadEvent(event, author, emptyList()) + trackNoteAuthor(note, event.pubKey) relay?.let { note.addRelay(it) } // Link response to zapped note via request @@ -617,6 +636,7 @@ class DesktopLocalCache : ICacheProvider { _followedUsers.value = emptySet() followerCounts.clear() followingCounts.clear() + notesByAuthor.clear() lastContactListCreatedAt = 0L } }