fix(perf): collect debug memory snapshot off the main thread

The debug-only MemoryUsageChip ("X/YMB" top-bar indicator, gated on
isDebug) polls collectMemorySnapshot() every 2s from a produceState
block, which runs on the main thread. That reads coil3.disk.DiskLruCache
.size(), a @Synchronized call. On cold start the Coil disk cache holds
that monitor for several seconds (journal init + the burst of image
writes from the initial relay event flood), so the UI thread blocked
inside size() — the "Loading account" frame couldn't repaint until it
returned. Profiling showed a single ~8s render frame and the UI thread
"blocking from coil3.disk.DiskLruCache.size()".

Collect the snapshot via withContext(Dispatchers.IO) so the synchronized
read blocks a background thread instead of the UI. The "Loading account"
stall on cold start drops from ~15-20s to ~5s. Debug-only path, so this
never affected release builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-06-27 12:26:44 -04:00
co-authored by Claude Opus 4.8
parent 9d0f444b6f
commit de1362561f
@@ -41,7 +41,9 @@ import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.MemorySnapshot
import com.vitorpamplona.amethyst.collectMemorySnapshot
import com.vitorpamplona.amethyst.isDebug
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
@Composable
fun MemoryUsageChip() {
@@ -52,7 +54,12 @@ fun MemoryUsageChip() {
val snapshot by produceState<MemorySnapshot?>(null) {
while (true) {
value = collectMemorySnapshot(context)
// collectMemorySnapshot reads coil3.disk.DiskLruCache.size(), which is @Synchronized and
// contends with the disk cache's own journal I/O. On cold start that lock is held by a
// background worker for seconds (initial journal read + the burst of image writes), so
// running this on the produceState default (main) dispatcher froze the UI thread —
// the "Loading account" frame couldn't repaint until size() returned. Collect off-main.
value = withContext(Dispatchers.IO) { collectMemorySnapshot(context) }
delay(2_000)
}
}