⚡ Optimize PersistentCookieJar by removing runBlocking (#799)
* 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
This commit is contained in:
@@ -130,7 +130,7 @@ object AuthManager {
|
|||||||
_useDynamicColorsFlow.value = state.useDynamicColors
|
_useDynamicColorsFlow.value = state.useDynamicColors
|
||||||
_themePresetFlow.value = state.themePreset
|
_themePresetFlow.value = state.themePreset
|
||||||
// B7 (Jul 08 2026, kanban t_470): keep cookie scope aligned with active profile.
|
// 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()
|
_tokenFlow.value = getToken()
|
||||||
// B7 (Jul 08 2026, kanban t_470): keep cookie scope aligned with active profile.
|
// 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 =
|
private fun normalizedProfileId(profileId: String?): String =
|
||||||
profileId?.takeIf { it.isNotBlank() } ?: DEFAULT_PROFILE_ID
|
profileId?.takeIf { it.isNotBlank() } ?: DEFAULT_PROFILE_ID
|
||||||
|
|
||||||
private fun syncCookieStoreForProfile(profileId: String?) {
|
private suspend fun syncCookieStoreForProfile(profileId: String?) {
|
||||||
if (!CookieManager.isInitialized()) return
|
if (!CookieManager.isInitialized()) return
|
||||||
val normalizedId = normalizedProfileId(profileId)
|
val normalizedId = normalizedProfileId(profileId)
|
||||||
if (CookieManager.cookieJar.currentServer() != normalizedId) {
|
if (CookieManager.cookieJar.currentServer() != normalizedId) {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import kotlinx.coroutines.Deferred
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import okhttp3.Cookie
|
import okhttp3.Cookie
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,9 +51,9 @@ object CookieManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Switch the active server scope and load its persisted cookies. */
|
/** Switch the active server scope and load its persisted cookies. */
|
||||||
fun useStore(serverId: String) {
|
suspend fun useStore(serverId: String) {
|
||||||
val j = jar ?: return
|
val j = jar ?: return
|
||||||
runBlocking(scope.coroutineContext) { j.useStore(serverId) }
|
j.useStore(serverId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Read the current dashboard access-cookie value (or null). */
|
/** Read the current dashboard access-cookie value (or null). */
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import okhttp3.Cookie
|
|||||||
import okhttp3.CookieJar
|
import okhttp3.CookieJar
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,6 +39,7 @@ class PersistentCookieJar(
|
|||||||
// serverId -> (host -> cookies)
|
// serverId -> (host -> cookies)
|
||||||
private val cache = ConcurrentHashMap<String, MutableMap<String, MutableList<Cookie>>>()
|
private val cache = ConcurrentHashMap<String, MutableMap<String, MutableList<Cookie>>>()
|
||||||
private val loadedScopes = ConcurrentHashMap.newKeySet<String>()
|
private val loadedScopes = ConcurrentHashMap.newKeySet<String>()
|
||||||
|
private val loadLatches = ConcurrentHashMap<String, CountDownLatch>()
|
||||||
|
|
||||||
@Volatile private var currentServerId = AtomicReference(initialServerId)
|
@Volatile private var currentServerId = AtomicReference(initialServerId)
|
||||||
|
|
||||||
@@ -61,9 +63,6 @@ class PersistentCookieJar(
|
|||||||
*/
|
*/
|
||||||
fun getCookie(name: String): Cookie? {
|
fun getCookie(name: String): Cookie? {
|
||||||
val serverId = currentServerId.get()
|
val serverId = currentServerId.get()
|
||||||
if (!loadedScopes.contains(serverId)) {
|
|
||||||
kotlinx.coroutines.runBlocking { ensureLoaded(serverId) }
|
|
||||||
}
|
|
||||||
val hosts = cache[serverId] ?: return null
|
val hosts = cache[serverId] ?: return null
|
||||||
for (bucket in hosts.values) {
|
for (bucket in hosts.values) {
|
||||||
synchronized(bucket) {
|
synchronized(bucket) {
|
||||||
@@ -106,10 +105,6 @@ class PersistentCookieJar(
|
|||||||
|
|
||||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||||
val serverId = currentServerId.get()
|
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 hostKey = url.host
|
||||||
val hosts = cache[serverId] ?: return emptyList()
|
val hosts = cache[serverId] ?: return emptyList()
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
|
|||||||
@@ -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<Cookie>,
|
||||||
|
) {
|
||||||
|
store.save(serverId, cookies)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(serverId: String): List<Cookie> {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user