fix(#283): apply minor performance optimizations for Chat (#315)

* fix(#283): apply minor performance optimizations for Chat

- Optimized `ChatScreen.kt` background gradient to use `remember` block correctly keyed by individual colors to avoid reallocation on recomposition.
- Removed redundant `luminance()` calculation caching inside `ChatBubble.kt` to preserve dynamic theme changes correctly while keeping code concise.
- Refactored `pendingRequests` timeout loop in `ChatViewModel.kt` to use `iterator.remove()` instead of generating a new filtered list.
- Converted `SideEffect` for `initialSessionId` assignment in `ChatScreen` to `remember` block to guarantee synchronous setup before WebSocket processes it.
- Initialized an explicit OkHttp `ConnectionPool` in `ApiClient` to survive client rebuilds.

These changes resolve Low-severity performance findings (PERF-22 to PERF-26) mentioned in issue #283 while ensuring maintainability and preserving dynamic theming.

* fix(#283): apply minor performance optimizations for Chat

- Optimized `ChatScreen.kt` background gradient to use `remember` block correctly keyed by individual colors to avoid reallocation on recomposition.
- Removed redundant `luminance()` calculation caching inside `ChatBubble.kt` to preserve dynamic theme changes correctly while keeping code concise.
- Refactored `pendingRequests` timeout loop in `ChatViewModel.kt` to use `iterator.remove()` instead of generating a new filtered list.
- Converted `SideEffect` for `initialSessionId` assignment in `ChatScreen` to `remember` block to guarantee synchronous setup before WebSocket processes it.
- Initialized an explicit OkHttp `ConnectionPool` in `ApiClient` to survive client rebuilds.

These changes resolve Low-severity performance findings (PERF-22 to PERF-26) mentioned in issue #283 while ensuring maintainability and preserving dynamic theming.
This commit is contained in:
M57 authored and GitHub committed 2026-06-23 12:34:43 +03:00
1 parent 5e93fb2a4e
commit 4faa3a156f
4 files changed
+30 -19

No files matched your search

@@ -24,6 +24,8 @@ object ApiClient {
@Volatile @Volatile
private var service: HermesApiService? = null private var service: HermesApiService? = null
private val connectionPool = okhttp3.ConnectionPool()
/** The current [HermesApiService] instance. Lazily created on first access. */ /** The current [HermesApiService] instance. Lazily created on first access. */
val hermesApi: HermesApiService val hermesApi: HermesApiService
get() { get() {
@@ -81,6 +83,7 @@ object ApiClient {
val okHttp = val okHttp =
OkHttpClient OkHttpClient
.Builder() .Builder()
.connectionPool(connectionPool)
.addInterceptor(authInterceptor) .addInterceptor(authInterceptor)
.addInterceptor(logging) .addInterceptor(logging)
.certificatePinner(certificatePinner) .certificatePinner(certificatePinner)
@@ -173,17 +173,18 @@ private fun UserBubble(
) )
val primary = MaterialTheme.colorScheme.primary val primary = MaterialTheme.colorScheme.primary
val secondary = MaterialTheme.colorScheme.secondary val secondary = MaterialTheme.colorScheme.secondary
val onPrimary = MaterialTheme.colorScheme.onPrimary
val avgLuminance = (primary.luminance() + secondary.luminance()) / 2f val avgLuminance = (primary.luminance() + secondary.luminance()) / 2f
val userBubbleTextColor = val userBubbleTextColor =
if (avgLuminance > 0.5f) { if (avgLuminance > 0.5f) {
if (MaterialTheme.colorScheme.onPrimary.luminance() < 0.5f) { if (onPrimary.luminance() < 0.5f) {
MaterialTheme.colorScheme.onPrimary onPrimary
} else { } else {
Color(0xFF1A1A24) Color(0xFF1A1A24)
} }
} else { } else {
if (MaterialTheme.colorScheme.onPrimary.luminance() > 0.5f) { if (onPrimary.luminance() > 0.5f) {
MaterialTheme.colorScheme.onPrimary onPrimary
} else { } else {
Color.White Color.White
} }
@@ -279,16 +280,17 @@ private fun AssistantBubble(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val bubbleColor = if (isDarkTheme) AssistantBubble else AssistantBubbleLight val bubbleColor = if (isDarkTheme) AssistantBubble else AssistantBubbleLight
val onSurface = MaterialTheme.colorScheme.onSurface
val textColor = val textColor =
if (bubbleColor.luminance() > 0.5f) { if (bubbleColor.luminance() > 0.5f) {
if (MaterialTheme.colorScheme.onSurface.luminance() < 0.5f) { if (onSurface.luminance() < 0.5f) {
MaterialTheme.colorScheme.onSurface onSurface
} else { } else {
Color(0xFF1A1A24) Color(0xFF1A1A24)
} }
} else { } else {
if (MaterialTheme.colorScheme.onSurface.luminance() > 0.5f) { if (onSurface.luminance() > 0.5f) {
MaterialTheme.colorScheme.onSurface onSurface
} else { } else {
Color(0xFFE8E6EE) Color(0xFFE8E6EE)
} }
@@ -75,7 +75,6 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
@@ -140,8 +139,9 @@ fun ChatScreen(
// during composition — BEFORE any WebSocket event (GatewayReady) can be // during composition — BEFORE any WebSocket event (GatewayReady) can be
// processed. This ensures GatewayReady sees the pending session and // processed. This ensures GatewayReady sees the pending session and
// resumes it instead of creating a new empty chat (issue #240). // resumes it instead of creating a new empty chat (issue #240).
SideEffect { remember(sessionId) {
viewModel.initialSessionId = sessionId viewModel.initialSessionId = sessionId
true
} }
// Switch to the session from a notification tap or history screen when provided. // Switch to the session from a notification tap or history screen when provided.
@@ -245,15 +245,21 @@ fun ChatScreen(
) )
} }
val backgroundColor = MaterialTheme.colorScheme.background
val surfaceVariantColor = MaterialTheme.colorScheme.surfaceVariant
val primaryColor = MaterialTheme.colorScheme.primary
val backgroundGradient = val backgroundGradient =
remember(backgroundColor, surfaceVariantColor, primaryColor) {
Brush.verticalGradient( Brush.verticalGradient(
colors = colors =
listOf( listOf(
MaterialTheme.colorScheme.background, backgroundColor,
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), surfaceVariantColor.copy(alpha = 0.3f),
MaterialTheme.colorScheme.primary.copy(alpha = 0.05f), primaryColor.copy(alpha = 0.05f),
), ),
) )
}
// Scroll to current search match // Scroll to current search match
LaunchedEffect(state.isSearchActive, state.currentSearchMatchIndex, state.searchMatchIndices) { LaunchedEffect(state.isSearchActive, state.currentSearchMatchIndex, state.searchMatchIndices) {
@@ -1105,17 +1105,17 @@ class ChatViewModel(
while (true) { while (true) {
delay(PENDING_REQUEST_TIMEOUT_MS) delay(PENDING_REQUEST_TIMEOUT_MS)
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val stale = val iterator = pendingRequests.entries.iterator()
pendingRequests.entries.filter { while (iterator.hasNext()) {
now - it.value.createdAt > PENDING_REQUEST_TIMEOUT_MS val entry = iterator.next()
} if (now - entry.value.createdAt > PENDING_REQUEST_TIMEOUT_MS) {
for (entry in stale) { iterator.remove()
pendingRequests.remove(entry.key)
Log.w(TAG, "Request timed out: ${entry.value.method} (id=${entry.key})") Log.w(TAG, "Request timed out: ${entry.value.method} (id=${entry.key})")
} }
} }
} }
} }
}
// ── Search ──────────────────────────────────────────────────────────── // ── Search ────────────────────────────────────────────────────────────