From 322e33678aad16316b9b8b5f211cbabe484c245b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 14:18:31 +0000 Subject: [PATCH] perf(quartz): make the negentropy idle watchdog allocation-free MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IdleClock.bump() is called for every message the relay sends — the connection listener bumps it per event, so a multi-million-event download bumped it millions of times. It stored a ValueTimeMark into an AtomicReference, and since the value class boxes when used as a generic type argument, every bump allocated a heap object. That is needless GC pressure on the hottest path (and battery/jank on Android). Replace it with a single monotonic base mark taken once (stored unboxed) plus a @Volatile Long of nanos-since-start updated on each bump — zero allocation per bump, and only visibility (not atomicity) is needed since each relay's bumps come from its single reader thread and the driver only reads. Behavior is unchanged; negentropy + concurrency suites pass. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JmSyzdmKyiz3pPxUZ8Mg8Z --- .../NostrClientNegentropySyncExt.kt | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientNegentropySyncExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientNegentropySyncExt.kt index 689eeafdb6..a5b218cf76 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientNegentropySyncExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientNegentropySyncExt.kt @@ -42,8 +42,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull -import kotlin.concurrent.atomics.AtomicReference -import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.concurrent.Volatile import kotlin.coroutines.coroutineContext import kotlin.math.min import kotlin.time.TimeSource @@ -680,18 +679,26 @@ private const val DEFAULT_DOWNLOAD_IDLE_MS = 60_000L /** * Monotonic "last activity" marker for the idle watchdog. [bump] on every sign of - * life from the relay; [elapsedMs] reports the silence since the last bump. Thread - * safe: bumped from relay reader threads, read from the driver coroutine. + * life from the relay; [elapsedMs] reports the silence since the last bump. + * + * [bump] is on the per-event hot path (the connection listener bumps for every + * message the relay sends — millions during a large download), so it must not + * allocate: a single [start] mark is taken once (unboxed field) and each bump only + * writes a `Long` of nanos-since-start into a `@Volatile` field. Reader threads + * write, the driver coroutine reads — visibility is all we need, so a plain volatile + * Long beats boxing a `ValueTimeMark` into an `AtomicReference` on every event. */ -@OptIn(ExperimentalAtomicApi::class) private class IdleClock { - private val last = AtomicReference(TimeSource.Monotonic.markNow()) + private val start = TimeSource.Monotonic.markNow() + + @Volatile + private var lastNanos = 0L fun bump() { - last.store(TimeSource.Monotonic.markNow()) + lastNanos = start.elapsedNow().inWholeNanoseconds } - fun elapsedMs(): Long = last.load().elapsedNow().inWholeMilliseconds + fun elapsedMs(): Long = (start.elapsedNow().inWholeNanoseconds - lastNanos) / 1_000_000 } /**