perf: optimize screen load times with progressive un-gating and SWR caching (#1192)

This commit is contained in:
M57 authored and GitHub committed 2026-09-18 23:25:30 +03:00
1 parent abce60ad7a
commit f5eaecbbef
55 files changed
+2494 -401

No files matched your search

@@ -48,6 +48,7 @@ import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.local.AuthManager
import com.m57.hermescontrol.theme.LocalSpacing
import com.m57.hermescontrol.ui.common.EmptyState
import com.m57.hermescontrol.ui.common.ErrorState
@@ -70,6 +71,7 @@ fun LogsScreen(
viewModel: LogsViewModel = viewModel { LogsViewModel() },
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
val dataScope by AuthManager.dataScopeFlow.collectAsStateWithLifecycle()
val spacing = LocalSpacing.current
val listState = rememberLazyListState()
@@ -115,7 +117,8 @@ fun LogsScreen(
}
}
LaunchedEffect(Unit) {
LaunchedEffect(dataScope) {
viewModel.clearScopeOwnedState()
viewModel.loadLogs()
}
@@ -146,7 +149,7 @@ fun LogsScreen(
title = { Text(stringResource(R.string.screen_logs)) },
navigationIcon = onOpenDrawer?.let { NavIcon.Menu(it) },
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadLogs() },
onRefresh = { viewModel.loadLogs(forceRefresh = true) },
actions = {
// Pause / Resume toggle
IconButton(onClick = { pauseScroll = !pauseScroll }) {
@@ -2,10 +2,12 @@ package com.m57.hermescontrol.ui.logs
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.m57.hermescontrol.data.local.SwrCache
import com.m57.hermescontrol.data.model.LogResponse
import com.m57.hermescontrol.data.remote.ApiClient
import com.m57.hermescontrol.data.remote.safeApiCall
import com.m57.hermescontrol.ui.common.ToastHost
import com.m57.hermescontrol.ui.common.safeLaunchLoad
import com.m57.hermescontrol.ui.common.safeLaunchSwrLoad
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
@@ -40,43 +42,51 @@ class LogsViewModel :
private val _uiState = MutableStateFlow(LogsUiState())
val uiState: StateFlow<LogsUiState> = _uiState.asStateFlow()
@Volatile
private var loadInProgress = false
private var loadJob: Job? = null
private var autoRefreshJob: Job? = null
private val logsCache = SwrCache<String, LogResponse>()
fun loadLogs() {
if (loadInProgress) return
loadInProgress = true
fun clearScopeOwnedState() {
_uiState.update { it.copy(isLoading = false, logs = emptyList(), errorMessage = null) }
}
fun loadLogs(forceRefresh: Boolean = false) {
val filters = _uiState.value.filters
safeLaunchLoad(
apiCall = {
safeApiCall {
ApiClient.hermesApi.getLogs(
file = filters.file,
lines = filters.lines,
level = filters.level,
component = filters.component,
)
}
},
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
onSuccess = { data ->
val body = data
val logsList = body.lines ?: body.logs ?: emptyList()
_uiState.update { it.copy(isLoading = false, logs = logsList) }
loadInProgress = false
},
onError = { errorMsg ->
_uiState.update {
it.copy(
isLoading = false,
errorMessage = "Failed to load logs: $errorMsg",
)
}
loadInProgress = false
},
)
val cacheKey = "${filters.file}:${filters.level}:${filters.component}:${filters.lines}"
loadJob =
safeLaunchSwrLoad(
cache = logsCache,
cacheKey = cacheKey,
forceRefresh = forceRefresh,
currentJob = loadJob,
onCacheHit = { data ->
val logsList = data.lines ?: data.logs ?: emptyList()
_uiState.update { it.copy(isLoading = false, logs = logsList, errorMessage = null) }
},
apiCall = {
safeApiCall {
ApiClient.hermesApi.getLogs(
file = filters.file,
lines = filters.lines,
level = filters.level,
component = filters.component,
)
}
},
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
onSuccess = { data ->
val logsList = data.lines ?: data.logs ?: emptyList()
_uiState.update { it.copy(isLoading = false, logs = logsList) }
},
onError = { errorMsg ->
_uiState.update {
it.copy(
isLoading = false,
errorMessage = "Failed to load logs: $errorMsg",
)
}
},
)
}
/** Update the server-side filters and immediately reload. */