fix(chat): prevent timeline prefetch ANRs (#985)
This commit is contained in:
@@ -92,6 +92,7 @@ import com.m57.hermescontrol.ui.chat.components.ChatLifecycleEffects
|
|||||||
import com.m57.hermescontrol.ui.chat.components.ChatLoadingOverlay
|
import com.m57.hermescontrol.ui.chat.components.ChatLoadingOverlay
|
||||||
import com.m57.hermescontrol.ui.chat.components.ChatResumeErrorOverlay
|
import com.m57.hermescontrol.ui.chat.components.ChatResumeErrorOverlay
|
||||||
import com.m57.hermescontrol.ui.chat.components.ChatScrollToBottomFab
|
import com.m57.hermescontrol.ui.chat.components.ChatScrollToBottomFab
|
||||||
|
import com.m57.hermescontrol.ui.chat.components.ChatTimelineNoPrefetchStrategy
|
||||||
import com.m57.hermescontrol.ui.chat.components.ContextDetailSheet
|
import com.m57.hermescontrol.ui.chat.components.ContextDetailSheet
|
||||||
import com.m57.hermescontrol.ui.chat.components.ContextUsageChip
|
import com.m57.hermescontrol.ui.chat.components.ContextUsageChip
|
||||||
import com.m57.hermescontrol.ui.chat.components.ReactionHeartsOverlay
|
import com.m57.hermescontrol.ui.chat.components.ReactionHeartsOverlay
|
||||||
@@ -158,7 +159,7 @@ fun ChatScreen(
|
|||||||
// read its fields recompose on search changes (bar, matched bubbles).
|
// read its fields recompose on search changes (bar, matched bubbles).
|
||||||
val searchState = viewModel.searchState
|
val searchState = viewModel.searchState
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState(prefetchStrategy = ChatTimelineNoPrefetchStrategy)
|
||||||
val scrollScope = rememberCoroutineScope()
|
val scrollScope = rememberCoroutineScope()
|
||||||
val scrollController = rememberChatScrollController(listState, scrollScope)
|
val scrollController = rememberChatScrollController(listState, scrollScope)
|
||||||
var isOlderPagingArmed by remember(state.currentSessionId) { mutableStateOf(false) }
|
var isOlderPagingArmed by remember(state.currentSessionId) { mutableStateOf(false) }
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package com.m57.hermescontrol.ui.chat.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.lazy.LazyListLayoutInfo
|
||||||
|
import androidx.compose.foundation.lazy.LazyListPrefetchScope
|
||||||
|
import androidx.compose.foundation.lazy.LazyListPrefetchStrategy
|
||||||
|
import androidx.compose.foundation.lazy.layout.NestedPrefetchScope
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chat messages can contain very large selectable Markdown blocks. Measuring a
|
||||||
|
* neighboring item during LazyColumn prefetch can monopolize the main thread
|
||||||
|
* long enough to trigger an input-dispatch ANR, even though that item is still
|
||||||
|
* off screen. Measure chat items only when they enter the viewport instead.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
internal object ChatTimelineNoPrefetchStrategy : LazyListPrefetchStrategy {
|
||||||
|
override fun LazyListPrefetchScope.onScroll(
|
||||||
|
delta: Float,
|
||||||
|
layoutInfo: LazyListLayoutInfo,
|
||||||
|
) = Unit
|
||||||
|
|
||||||
|
override fun LazyListPrefetchScope.onVisibleItemsUpdated(layoutInfo: LazyListLayoutInfo) = Unit
|
||||||
|
|
||||||
|
override fun NestedPrefetchScope.onNestedPrefetch(firstVisibleItemIndex: Int) = Unit
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package com.m57.hermescontrol.ui.chat.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.lazy.LazyListLayoutInfo
|
||||||
|
import androidx.compose.foundation.lazy.LazyListPrefetchScope
|
||||||
|
import androidx.compose.foundation.lazy.layout.NestedPrefetchScope
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
class ChatTimelinePrefetchStrategyTest {
|
||||||
|
@Test
|
||||||
|
fun strategy_neverSchedulesOffscreenChatItems() {
|
||||||
|
val scope = mockk<LazyListPrefetchScope>(relaxed = true)
|
||||||
|
val layoutInfo = mockk<LazyListLayoutInfo>(relaxed = true)
|
||||||
|
val nestedScope = mockk<NestedPrefetchScope>(relaxed = true)
|
||||||
|
|
||||||
|
with(ChatTimelineNoPrefetchStrategy) {
|
||||||
|
with(scope) {
|
||||||
|
onScroll(delta = 120f, layoutInfo)
|
||||||
|
onVisibleItemsUpdated(layoutInfo)
|
||||||
|
}
|
||||||
|
with(nestedScope) {
|
||||||
|
onNestedPrefetch(firstVisibleItemIndex = 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(exactly = 0) { scope.schedulePrefetch(any(), any()) }
|
||||||
|
verify(exactly = 0) { nestedScope.schedulePrecomposition(any()) }
|
||||||
|
verify(exactly = 0) { nestedScope.schedulePrecompositionAndPremeasure(any(), any()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user