mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
perf(quartz): make the negentropy idle watchdog allocation-free
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmSyzdmKyiz3pPxUZ8Mg8Z
This commit is contained in:
+15
-8
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user