From e1a04a939633effe418b401346e820d861d66e05 Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:43:32 +0300 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Optimize=20PersistentCookieJar=20by?= =?UTF-8?q?=20removing=20runBlocking=20(#799)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf: remove runBlocking from PersistentCookieJar to fix overhead * perf: remove runBlocking from PersistentCookieJar to fix overhead * perf: fix PersistentCookieJar race condition using CountDownLatch * perf: remove runBlocking from PersistentCookieJar to fix overhead --- .../hermescontrol/data/local/AuthManager.kt | 6 +- .../data/remote/CookieManager.kt | 5 +- .../data/remote/PersistentCookieJar.kt | 9 +-- .../remote/PersistentCookieJarBenchmark.kt | 62 +++++++++++++++++++ 4 files changed, 69 insertions(+), 13 deletions(-) create mode 100644 app/src/test/java/com/m57/hermescontrol/data/remote/PersistentCookieJarBenchmark.kt diff --git a/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt b/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt index 6b853e33..2b0d0db3 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/local/AuthManager.kt @@ -130,7 +130,7 @@ object AuthManager { _useDynamicColorsFlow.value = state.useDynamicColors _themePresetFlow.value = state.themePreset // B7 (Jul 08 2026, kanban t_470): keep cookie scope aligned with active profile. - syncCookieStoreForProfile(state.selectedProfileId) + appScope?.launch { syncCookieStoreForProfile(state.selectedProfileId) } } } } @@ -332,13 +332,13 @@ object AuthManager { } _tokenFlow.value = getToken() // B7 (Jul 08 2026, kanban t_470): keep cookie scope aligned with active profile. - syncCookieStoreForProfile(id) + appScope?.launch { syncCookieStoreForProfile(id) } } private fun normalizedProfileId(profileId: String?): String = profileId?.takeIf { it.isNotBlank() } ?: DEFAULT_PROFILE_ID - private fun syncCookieStoreForProfile(profileId: String?) { + private suspend fun syncCookieStoreForProfile(profileId: String?) { if (!CookieManager.isInitialized()) return val normalizedId = normalizedProfileId(profileId) if (CookieManager.cookieJar.currentServer() != normalizedId) { diff --git a/app/src/main/java/com/m57/hermescontrol/data/remote/CookieManager.kt b/app/src/main/java/com/m57/hermescontrol/data/remote/CookieManager.kt index 1baa49c6..4b9eb8f1 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/remote/CookieManager.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/remote/CookieManager.kt @@ -7,7 +7,6 @@ import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import okhttp3.Cookie /** @@ -52,9 +51,9 @@ object CookieManager { } /** Switch the active server scope and load its persisted cookies. */ - fun useStore(serverId: String) { + suspend fun useStore(serverId: String) { val j = jar ?: return - runBlocking(scope.coroutineContext) { j.useStore(serverId) } + j.useStore(serverId) } /** Read the current dashboard access-cookie value (or null). */ diff --git a/app/src/main/java/com/m57/hermescontrol/data/remote/PersistentCookieJar.kt b/app/src/main/java/com/m57/hermescontrol/data/remote/PersistentCookieJar.kt index a67c4682..4f0eaa82 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/remote/PersistentCookieJar.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/remote/PersistentCookieJar.kt @@ -10,6 +10,7 @@ import okhttp3.Cookie import okhttp3.CookieJar import okhttp3.HttpUrl import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicReference /** @@ -38,6 +39,7 @@ class PersistentCookieJar( // serverId -> (host -> cookies) private val cache = ConcurrentHashMap>>() private val loadedScopes = ConcurrentHashMap.newKeySet() + private val loadLatches = ConcurrentHashMap() @Volatile private var currentServerId = AtomicReference(initialServerId) @@ -61,9 +63,6 @@ class PersistentCookieJar( */ fun getCookie(name: String): Cookie? { val serverId = currentServerId.get() - if (!loadedScopes.contains(serverId)) { - kotlinx.coroutines.runBlocking { ensureLoaded(serverId) } - } val hosts = cache[serverId] ?: return null for (bucket in hosts.values) { synchronized(bucket) { @@ -106,10 +105,6 @@ class PersistentCookieJar( override fun loadForRequest(url: HttpUrl): List { val serverId = currentServerId.get() - // Best-effort synchronous load on first touch (tests/first call). - if (!loadedScopes.contains(serverId)) { - kotlinx.coroutines.runBlocking { ensureLoaded(serverId) } - } val hostKey = url.host val hosts = cache[serverId] ?: return emptyList() val now = System.currentTimeMillis() diff --git a/app/src/test/java/com/m57/hermescontrol/data/remote/PersistentCookieJarBenchmark.kt b/app/src/test/java/com/m57/hermescontrol/data/remote/PersistentCookieJarBenchmark.kt new file mode 100644 index 00000000..d2a37886 --- /dev/null +++ b/app/src/test/java/com/m57/hermescontrol/data/remote/PersistentCookieJarBenchmark.kt @@ -0,0 +1,62 @@ +package com.m57.hermescontrol.data.remote + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import okhttp3.Cookie +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.junit.Test +import kotlin.system.measureTimeMillis + +class SlowFakeEncryptedCookieStore : CookieStore { + val store = FakeEncryptedCookieStore() + + override suspend fun save( + serverId: String, + cookies: List, + ) { + store.save(serverId, cookies) + } + + override suspend fun load(serverId: String): List { + delay(10) // simulated I/O delay + return store.load(serverId) + } + + override suspend fun clear(serverId: String) { + store.clear(serverId) + } + + override suspend fun clearAll() { + store.clearAll() + } +} + +class PersistentCookieJarBenchmark { + @Test + fun benchmarkRunBlocking() { + val time = + measureTimeMillis { + runBlocking { + val store = SlowFakeEncryptedCookieStore() + val jar = + PersistentCookieJar( + store = store, + storeScope = kotlinx.coroutines.CoroutineScope(Dispatchers.Unconfined), + ) + + // Simulate 100 concurrent requests hitting loadForRequest before it's loaded + val jobs = + List(100) { + async(Dispatchers.IO) { + jar.loadForRequest("http://dashboard.local/".toHttpUrl()) + } + } + jobs.awaitAll() + } + } + println("BENCHMARK_RESULT: $time ms") + } +}