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
+35 -24

No files matched your search

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