perf(ui): suspend polling outside visible screens (#806)

This commit is contained in:
AlexDev_
2026-08-04 19:20:01 +03:00
committed by GitHub
parent be0bbcb2c9
commit 9370523ce9
3 changed files with 20 additions and 14 deletions
@@ -66,7 +66,10 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.NavigationController import com.m57.hermescontrol.NavigationController
import com.m57.hermescontrol.R import com.m57.hermescontrol.R
@@ -102,7 +105,7 @@ import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
private const val SESSION_SYNC_INTERVAL_MS = 5_000L private const val SESSION_SYNC_INTERVAL_MS = 30_000L
/** /**
* Chat screen — the primary conversation surface of Hermes Control. * Chat screen — the primary conversation surface of Hermes Control.
@@ -126,6 +129,7 @@ fun ChatScreen(
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
val streamingState by viewModel.streamingState.collectAsStateWithLifecycle() val streamingState by viewModel.streamingState.collectAsStateWithLifecycle()
val credentialWarning by HermesWsClient.credentialWarning.collectAsStateWithLifecycle() val credentialWarning by HermesWsClient.credentialWarning.collectAsStateWithLifecycle()
val lifecycleOwner = LocalLifecycleOwner.current
val listState = rememberLazyListState() val listState = rememberLazyListState()
val scrollScope = rememberCoroutineScope() val scrollScope = rememberCoroutineScope()
val scrollController = rememberChatScrollController(listState, scrollScope) val scrollController = rememberChatScrollController(listState, scrollScope)
@@ -133,7 +137,8 @@ fun ChatScreen(
var showContextSheet by remember { mutableStateOf(false) } var showContextSheet by remember { mutableStateOf(false) }
// Periodic session sync while connected. // Periodic session sync while connected.
LaunchedEffect(state.currentSessionId, state.connectionStatus) { LaunchedEffect(lifecycleOwner, state.currentSessionId, state.connectionStatus) {
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
if (state.currentSessionId != null && state.connectionStatus == ConnectionStatus.CONNECTED) { if (state.currentSessionId != null && state.connectionStatus == ConnectionStatus.CONNECTED) {
viewModel.syncCurrentSession() viewModel.syncCurrentSession()
viewModel.fetchContextUsage() viewModel.fetchContextUsage()
@@ -144,6 +149,7 @@ fun ChatScreen(
viewModel.fetchContextUsage() viewModel.fetchContextUsage()
} }
} }
}
// Arm + trigger older-message paging when the user scrolls near the top. // Arm + trigger older-message paging when the user scrolls near the top.
// Before prepending, capture the anchor (first visible message id + offset) // Before prepending, capture the anchor (first visible message id + offset)
@@ -2238,7 +2238,7 @@ class ChatViewModel(
* Both calls are independent and best-effort: a failure on one must not * Both calls are independent and best-effort: a failure on one must not
* wipe the other's already-shown value, and neither blocks the chat. The * wipe the other's already-shown value, and neither blocks the chat. The
* two fetches are launched separately so a slow/erroring one can't starve * two fetches are launched separately so a slow/erroring one can't starve
* the other. Polled from [syncCurrentSession] via the 5s loop and re-fired * the other. Polled from [syncCurrentSession] via the 30s loop and re-fired
* on model switch (the denominator changes). * on model switch (the denominator changes).
*/ */
fun fetchContextUsage() { fun fetchContextUsage() {
@@ -31,7 +31,6 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -45,6 +44,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.R import com.m57.hermescontrol.R
@@ -120,9 +120,9 @@ fun LogsScreen(
} }
// Auto-refresh logs via polling while the screen is visible // Auto-refresh logs via polling while the screen is visible
DisposableEffect(Unit) { LifecycleStartEffect(viewModel) {
viewModel.startAutoRefresh() viewModel.startAutoRefresh()
onDispose { viewModel.stopAutoRefresh() } onStopOrDispose { viewModel.stopAutoRefresh() }
} }
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast) ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)