Skip to content

Commit 0f80865

Browse files
slipNETclaude
andcommitted
Optimize debug log sheet: batch updates and remove scroll animation
AppLog: replace per-append ArrayList copy with dirty flag + periodic flush (100ms timer in DebugLogSheet). Batches rapid log calls into a single copy + recomposition instead of one per log line. DebugLogSheet: animateScrollToItem → scrollToItem to prevent jank when logs flow rapidly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f5ff066 commit 0f80865

2 files changed

Lines changed: 30 additions & 2 deletions

File tree

app/src/main/java/app/slipnet/presentation/home/DebugLogSheet.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import androidx.compose.runtime.DisposableEffect
2929
import androidx.compose.runtime.LaunchedEffect
3030
import androidx.compose.runtime.collectAsState
3131
import androidx.compose.runtime.getValue
32+
import kotlinx.coroutines.delay
3233
import androidx.compose.ui.Alignment
3334
import androidx.compose.ui.Modifier
3435
import androidx.compose.ui.graphics.Color
@@ -52,14 +53,22 @@ fun DebugLogSheet(onDismiss: () -> Unit) {
5253
onDispose { AppLog.removeObserver() }
5354
}
5455

56+
// Flush dirty buffer → StateFlow every 100ms (batches rapid log calls)
57+
LaunchedEffect(Unit) {
58+
while (true) {
59+
delay(100)
60+
AppLog.flushIfDirty()
61+
}
62+
}
63+
5564
val lines by AppLog.lines.collectAsState()
5665
val listState = rememberLazyListState()
5766
val clipboardManager = LocalClipboardManager.current
5867

5968
// Auto-scroll to bottom when new lines arrive
6069
LaunchedEffect(lines.size) {
6170
if (lines.isNotEmpty()) {
62-
listState.animateScrollToItem(lines.size - 1)
71+
listState.scrollToItem(lines.size - 1)
6372
}
6473
}
6574

app/src/main/java/app/slipnet/util/AppLog.kt

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.asStateFlow
66
import java.text.SimpleDateFormat
77
import java.util.Date
88
import java.util.Locale
9+
import java.util.concurrent.atomic.AtomicBoolean
910
import java.util.concurrent.atomic.AtomicLong
1011

1112
data class LogEntry(val id: Long, val raw: String, val level: Char)
@@ -34,6 +35,11 @@ object AppLog {
3435
@Volatile var observerCount = 0
3536
private set
3637

38+
// Dirty flag: set by append(), cleared by flush().
39+
// Avoids creating an ArrayList copy on every single log call — instead
40+
// the UI polls via flushIfDirty() on each collection (every frame).
41+
private val dirty = AtomicBoolean(false)
42+
3743
private val dateFormat = object : ThreadLocal<SimpleDateFormat>() {
3844
override fun initialValue() = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
3945
}
@@ -50,7 +56,20 @@ object AppLog {
5056
synchronized(buffer) {
5157
buffer.addLast(entry)
5258
while (buffer.size > MAX_LINES) buffer.removeFirst()
53-
if (observerCount > 0) {
59+
}
60+
if (observerCount > 0) {
61+
dirty.set(true)
62+
}
63+
}
64+
65+
/**
66+
* Copy the buffer to the StateFlow if anything changed since the last flush.
67+
* Called by the debug sheet on a periodic timer (~100ms) so we batch many
68+
* rapid log calls into a single ArrayList copy + recomposition.
69+
*/
70+
fun flushIfDirty() {
71+
if (dirty.compareAndSet(true, false)) {
72+
synchronized(buffer) {
5473
_lines.value = ArrayList(buffer)
5574
}
5675
}

0 commit comments

Comments
 (0)