diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt index 1f02032687..30da4b8992 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt @@ -228,6 +228,24 @@ class NostrClient( * Ids survive one tick and die on the next, so [DECODER_AGE_OUT_MS] bounds their * lifetime at ~2x that. Suspends on [isActiveFlow] like [keepAliveJob], so no timer * fires while the client is down — [disconnect] clears outright on the way there. + * + * 30s is measured, not guessed. Sweeping the interval on device and comparing hit + * rate at a matched ~19k frames (runs pull different volumes, so raw endpoints are + * not comparable): + * + * ``` + * tick hit rate parses ids still cached at rest + * 10s 44.9% 10,663 ~2 + * 30s 58.5% 7,569 ~2 + * 60s 60.4% 7,520 ~3 + * 120s 60.6% 7,555 7,438 (one tick in 3 min -> never releases) + * ``` + * + * Hit rate saturates by 30s, so a shorter tick only buys re-parses (10s costs 41% + * more), and a longer one only holds memory. The knee is where the tick stops being + * the binding constraint and [CachingEventDecoder.capacity] takes over: at the + * observed ~400 frames/s that crossover is 8192/400 ~= 20s, so 30s sits just past + * it. Re-measure if capacity or typical frame rates change. */ private val decoderTrimJob = scope.launch {