perf: optimize synchronous I/O in AuthLoginViewModel (#699)

* perf: optimize synchronous I/O in AuthLoginViewModel

Replace blocking OkHttp .execute() with a suspending .await() extension
(CallExt.kt) backed by suspendCancellableCoroutine, freeing Dispatchers.IO
threads during network waits. Probe/connect paths made suspend.

- Call.await() guards both onResponse/onFailure with continuation.isActive
  to avoid AlreadyResume on cancellation.
- Removes non-asserting AuthLoginViewModelBenchmark timing dump.

* perf: optimize synchronous I/O in AuthLoginViewModel

Replaced the blocking `.execute()` OkHttp calls with a suspending `.await()` extension function inside `AuthLoginViewModel`. This frees up the `Dispatchers.IO` threads by deferring the blocking operations to OkHttp's internal task runner, providing a safer async approach and preventing Thread pool starvation on slow network responses.

Modified `probeDashboardInternal` and `connectBasicAuth` to be proper `suspend` functions and resolved corresponding unit tests.

* fix(test): add missing import io.mockk.any in AuthLoginViewModelTest

* fix(test): fix mockk imports and Log matchers in AuthLoginViewModelTest

* fix(test): use persistent mockProbeClient in AuthLoginViewModelTest

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-07-23 22:23:53 +03:00
1 parent 152b7d5c0d
commit 8c13f9bed0
4 files changed
+116 -13

No files matched your search

@@ -0,0 +1,61 @@
package com.m57.hermescontrol.data.remote
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.Response
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.io.IOException
@OptIn(ExperimentalCoroutinesApi::class)
class CallExtTest {
@Test
fun testAwaitSuccess() =
runTest {
val call = mockk<Call>()
val request = Request.Builder().url("http://localhost").build()
val response =
Response.Builder()
.request(request)
.protocol(Protocol.HTTP_1_1)
.code(200)
.message("OK")
.build()
val callbackSlot = slot<Callback>()
every { call.enqueue(capture(callbackSlot)) } answers {
callbackSlot.captured.onResponse(call, response)
}
every { call.cancel() } returns Unit
val result = call.await()
assertEquals(200, result.code)
}
@Test
fun testAwaitFailure() =
runTest {
val call = mockk<Call>()
val exception = IOException("Failed")
val callbackSlot = slot<Callback>()
every { call.isCanceled() } returns false
every { call.enqueue(capture(callbackSlot)) } answers {
callbackSlot.captured.onFailure(call, exception)
}
every { call.cancel() } returns Unit
try {
call.await()
} catch (e: Exception) {
assertEquals("Failed", e.message)
}
}
}
@@ -6,6 +6,8 @@ import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.local.AuthManager
import com.m57.hermescontrol.data.remote.OkHttpProvider
import com.m57.hermescontrol.data.remote.ServerEndpoint
import com.m57.hermescontrol.data.remote.await
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
@@ -26,6 +28,7 @@ import java.io.IOException
class AuthLoginViewModelTest {
private lateinit var viewModel: AuthLoginViewModel
private val app = mockk<Application>(relaxed = true)
private val mockProbeClient = mockk<OkHttpClient>(relaxed = true)
@Before
fun setup() {
@@ -34,11 +37,12 @@ class AuthLoginViewModelTest {
every { AuthManager.getConnectionProfiles() } returns emptyList()
mockkObject(OkHttpProvider)
every { OkHttpProvider.probe } returns mockk<OkHttpClient>(relaxed = true)
every { OkHttpProvider.probe } returns mockProbeClient
mockkStatic(Log::class)
every { Log.w(any<String>(), any<String>()) } returns 0
every { Log.d(any<String>(), any<String>()) } returns 0
mockkStatic("com.m57.hermescontrol.data.remote.CallExtKt")
every { android.util.Log.w(any(), any<String>()) } returns 0
every { android.util.Log.d(any(), any()) } returns 0
every { app.getString(R.string.auth_login_error_unreachable) } returns "Dashboard unreachable"
@@ -84,8 +88,8 @@ class AuthLoginViewModelTest {
@Test
fun `probe when status probe throws IOException updates error state`() {
val mockCall = mockk<okhttp3.Call>()
every { OkHttpProvider.probe.newCall(any()) } returns mockCall
every { mockCall.execute() } throws IOException("Connection refused")
every { mockProbeClient.newCall(any()) } returns mockCall
coEvery { mockCall.await() } throws IOException("Connection refused")
viewModel.probe()
@@ -99,8 +103,8 @@ class AuthLoginViewModelTest {
fun `probe when status probe returns unsuccessful updates error state`() {
val mockCall = mockk<okhttp3.Call>()
val mockResponse = mockk<Response>()
every { OkHttpProvider.probe.newCall(any()) } returns mockCall
every { mockCall.execute() } returns mockResponse
every { mockProbeClient.newCall(any()) } returns mockCall
coEvery { mockCall.await() } returns mockResponse
every { mockResponse.isSuccessful } returns false
viewModel.probe()