git.iko.soy/opencode-android
on master help sign in
Reduce markdown re-parse work, wasted preview fetches during streaming, and diff allocation
commit34c5c509f34e251013de9cc7e569cd1afddd6188 authoriko <mail@iko.soy> dateWed 1 Jul 2026, 00:19 (3 months ago) parentdce783d
4 files changed+16−5
app/src/main/java/soy/iko/opencode/data/network/NetworkConfig.kt+9−2 app/src/main/java/soy/iko/opencode/ui/components/DiffView.kt+1−1 app/src/main/java/soy/iko/opencode/ui/components/MarkdownText.kt+1−1 app/src/main/java/soy/iko/opencode/ui/session/SessionListViewModel.kt+5−1
app/src/main/java/soy/iko/opencode/data/network/NetworkConfig.kt+9−2
@@ -53,6 +53,10 @@ object NetworkConfig {
5353 const val maxConcurrentPreviews = 8
5454 /** Max characters of a session's last message to keep as a list preview. */
5555 const val previewTextMaxLength = 200
56+ /** Debounce delay before fetching a session preview after an SSE update, so a burst
57+ * of SessionUpdated events (e.g. during active streaming) coalesces into one fetch
58+ * instead of launching one per event (each downloading the full message history). */
59+ const val previewDebounceMs = 500L
5660
5761 // --- In-memory message store (SessionRepository.MessageStore) ---
5862
@@ -88,8 +92,11 @@ object NetworkConfig {
8892
8993 // --- Markdown rendering (MarkdownText) ---
9094
91− /** Throttle delay (~60fps frame) to coalesce streaming tokens into one re-parse. */
92− const val streamingThrottleMs = 16L
95+ /** Throttle delay to coalesce streaming tokens into one re-parse. The markdown
96+ * library re-parses the full AST on every content change, so during a long streaming
97+ * response this is O(n²) work. 50ms (~20fps) is smooth for progressively appearing
98+ * text while cutting parse work ~3x versus a per-frame (16ms) throttle. */
99+ const val streamingThrottleMs = 50L
93100
94101 // --- Snackbar one-shot events (ViewModels) ---
95102
app/src/main/java/soy/iko/opencode/ui/components/DiffView.kt+1−1
@@ -57,7 +57,7 @@ private val gitMetaPrefixes = listOf(
5757/** Parse a unified diff string into typed [DiffLine]s. */
5858fun parseDiff(diff: String): List<DiffLine> {
5959 val result = mutableListOf<DiffLine>()
60− for (raw in diff.split("\n")) {
60+ for (raw in diff.lineSequence()) {
6161 when {
6262 raw.startsWith("@@") -> result.add(DiffLine.Hunk(raw))
6363 raw.startsWith("---") || raw.startsWith("+++") -> result.add(DiffLine.FileHeader(raw))
app/src/main/java/soy/iko/opencode/ui/components/MarkdownText.kt+1−1
@@ -57,7 +57,7 @@ import soy.iko.opencode.data.network.NetworkConfig
5757 * During streaming, the full markdown is re-parsed on every token (the library re-parses
5858 * whenever the content string changes). To avoid O(n²) work during long responses, the
5959 * rendered content is throttled — the latest [markdown] is committed to the renderer at
60− * most once per frame (~16ms), so a burst of tokens coalesces into a single re-parse.
60+ * most once every ~50ms, so a burst of tokens coalesces into a single re-parse.
6161 *
6262 * The throttle uses a single long-lived `LaunchedEffect(Unit)` that observes the markdown
6363 * parameter via `rememberUpdatedState` + `snapshotFlow` + `conflate` + `collect`. A keyed
app/src/main/java/soy/iko/opencode/ui/session/SessionListViewModel.kt+5−1
@@ -18,6 +18,7 @@ import kotlinx.coroutines.CancellationException
1818import kotlinx.coroutines.ExperimentalCoroutinesApi
1919import kotlinx.coroutines.Job
2020import kotlinx.coroutines.channels.BufferOverflow
21+import kotlinx.coroutines.delay
2122import kotlinx.coroutines.flow.MutableSharedFlow
2223import kotlinx.coroutines.flow.MutableStateFlow
2324import kotlinx.coroutines.flow.SharedFlow
@@ -228,7 +229,9 @@ class SessionListViewModel(private val container: AppContainer) : ViewModel() {
228229 /**
229230 * Reload the preview for a single session (used by the live SSE handler).
230231 * Cancels any prior in-flight load for the same session so a burst of
231− * [SessionUpdated] events coalesces into a single request.
232+ * [SessionUpdated] events coalesces into a single request. A short debounce
233+ * delay before the fetch avoids launching one full-history download per event
234+ * during active streaming — the fetch runs only after events quiet down.
232235 */
233236 private fun loadPreview(sessionId: String) {
234237 val conn = container.activeConnection.value ?: return
@@ -238,6 +241,7 @@ class SessionListViewModel(private val container: AppContainer) : ViewModel() {
238241 livePreviewJobs[sessionId] = viewModelScope.launch {
239242 val job = coroutineContext[kotlinx.coroutines.Job]
240243 try {
244+ delay(NetworkConfig.previewDebounceMs)
241245 val preview = fetchSessionPreview(api, sessionId) ?: return@launch
242246 _state.update { s -> s.copy(previews = s.previews + (sessionId to preview)) }
243247 } catch (e: CancellationException) {