perf(feed): viewport-aware metadata loading with batched author filter
Two optimizations for feed metadata loading speed: 1. Batched author filter (FeedMetadataCoordinator.loadMetadataBatched): Single Filter(kind:0, authors:[all visible]) instead of individual per-pubkey subscriptions through rate limiter. Closes after EOSE. Follows ChessRelayFetchHelper one-shot pattern. Max 100 authors/filter. 2. Viewport-aware scroll observation (FeedScreen): snapshotFlow reads LazyListState.visibleItemsInfo (zero recomposition) with 500ms debounce + distinctUntilChanged. Only fetches metadata for visible notes ± 10 item buffer. Initial load batches first 30 notes. Before: 100+ authors × 20/sec rate limit × 7 relays = 5+ seconds After: ~20 visible authors × 1 batched sub × 7 relays = <1 second Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+55
@@ -36,7 +36,10 @@ import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
/**
|
||||
* Coordinates metadata and reactions loading for feed items.
|
||||
@@ -245,6 +248,58 @@ class FeedMetadataCoordinator(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast-path: batched metadata subscription for visible-viewport authors.
|
||||
* Bypasses rate limiter. Single filter with all authors. Closes after EOSE.
|
||||
*/
|
||||
fun loadMetadataBatched(
|
||||
pubkeys: List<HexKey>,
|
||||
timeoutMs: Long = 5_000L,
|
||||
) {
|
||||
val newPubkeys = pubkeys.filter { it !in queuedPubkeys }.distinct()
|
||||
if (newPubkeys.isEmpty()) return
|
||||
queuedPubkeys.addAll(newPubkeys)
|
||||
|
||||
scope.launch {
|
||||
val filter =
|
||||
Filter(
|
||||
kinds = listOf(MetadataEvent.KIND),
|
||||
authors = newPubkeys.take(100),
|
||||
limit = newPubkeys.size,
|
||||
)
|
||||
val filterMap = indexRelays.associateWith { listOf(filter) }
|
||||
val subId = newSubId()
|
||||
val eoseReceived = mutableSetOf<NormalizedRelayUrl>()
|
||||
val allEose = CompletableDeferred<Unit>()
|
||||
|
||||
val listener =
|
||||
object : SubscriptionListener {
|
||||
override fun onEvent(
|
||||
event: Event,
|
||||
isLive: Boolean,
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
onEvent?.invoke(event, relay)
|
||||
}
|
||||
|
||||
override fun onEose(
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
eoseReceived.add(relay)
|
||||
if (eoseReceived.size >= indexRelays.size) {
|
||||
allEose.complete(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.subscribe(subId, filterMap, listener)
|
||||
withTimeoutOrNull(timeoutMs) { allEose.await() }
|
||||
client.unsubscribe(subId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear queued items. Call when switching feeds.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user