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) <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-08-14 11:26:01 -04:00
co-authored by Claude Opus 5
parent ccebc3d344
commit b1f1190919
@@ -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 {