From b1f1190919bc763d8f73aadac05012257d858ccf Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 14 Aug 2026 11:24:47 -0400 Subject: [PATCH] docs: record the tick sweep that picked 30s for the dedup age-out 30s was the one number in this change I picked rather than measured. Swept it on device (10/30/60/120s). The naive comparison is invalid: runs pull 3.7k-32k frames depending on what the relays serve, so hit rate and heap at the end of a run are not comparable. Comparing at a matched ~19k frames instead: 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 Hit rate saturates by 30s, so a shorter tick buys only re-parses (10s costs 41% more) and a longer one only holds memory -- at 120s just one tick fires in three minutes, so the burst never drops at all. The knee is where the tick stops being the binding constraint and capacity takes over: at ~400 frames/s that is 8192/400 ~= 20s, and 30s sits just past it. Keeps 30s; documents why, and what to re-measure if capacity or frame rates change. No behaviour change. Co-Authored-By: Claude Opus 5 (1M context) --- .../nip01Core/relay/client/NostrClient.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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 {