perf(model): optimize model tab load speed with two-phase fetch and loadQuick (#677)

This commit is contained in:
M57
2026-07-22 14:46:38 +03:00
committed by GitHub
parent 0bb418b4a7
commit f7afbfae85
2 changed files with 157 additions and 79 deletions
@@ -443,7 +443,7 @@ interface HermesApiService {
@GET("api/model/options")
suspend fun getModelOptions(
@Query("refresh") refresh: Boolean = false,
@Query("include_unconfigured") includeUnconfigured: Boolean = true,
@Query("include_unconfigured") includeUnconfigured: Boolean = false,
): Response<ModelOptionsResponse>
@GET("api/model/auxiliary")
@@ -79,10 +79,7 @@ class ModelViewModel :
fun loadAll(refresh: Boolean = false) {
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
viewModelScope.launch {
val optionsDeferred =
async(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getModelOptions(refresh = refresh) }
}
// Phase 1: Launch fast lightweight calls first (profiles, aux, moa)
val activeProfileDeferred =
async(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getActiveProfile() }
@@ -100,14 +97,18 @@ class ModelViewModel :
safeApiCall { ApiClient.hermesApi.getMoaModels() }
}
val optionsResult = optionsDeferred.await()
// Phase 2: Launch model options concurrently
val optionsDeferred =
async(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getModelOptions(refresh = refresh, includeUnconfigured = false) }
}
// Await Phase 1 calls (~40-190ms total) and update UI immediately with available state
val activeProfileResult = activeProfileDeferred.await()
val profilesResult = profilesDeferred.await()
val auxResult = auxDeferred.await()
val moaResult = moaDeferred.await()
when (optionsResult) {
is NetworkResult.Success -> {
val activeName =
if (activeProfileResult is NetworkResult.Success) {
activeProfileResult.data.active
@@ -139,16 +140,26 @@ class ModelViewModel :
null
}
// Render Phase 1 results right away so UI controls update without waiting for model options
_uiState.update {
it.copy(
isLoading = false,
providers = optionsResult.data.providers.orEmpty(),
activeProfile = activeProfile,
activeProfile = activeProfile ?: it.activeProfile,
pinnedModels = AuthManager.getPinnedModels(),
mainModelProvider = mainModel.first,
mainModelModel = mainModel.second,
auxTasks = auxTasks,
moaConfig = moaConfig,
moaConfig = moaConfig ?: it.moaConfig,
)
}
// Await Phase 2 (model catalog option provider tree)
val optionsResult = optionsDeferred.await()
when (optionsResult) {
is NetworkResult.Success -> {
_uiState.update {
it.copy(
isLoading = false,
providers = optionsResult.data.providers.orEmpty(),
)
}
}
@@ -157,7 +168,12 @@ class ModelViewModel :
_uiState.update {
it.copy(
isLoading = false,
errorMessage = "Failed to load model options: ${optionsResult.error.message}",
errorMessage =
if (it.providers.isEmpty()) {
"Failed to load model options: ${optionsResult.error.message}"
} else {
null
},
)
}
}
@@ -165,6 +181,63 @@ class ModelViewModel :
}
}
/** Fast refresh after model assignments/updates without re-fetching full provider catalog. */
fun loadQuick() {
viewModelScope.launch {
val activeProfileDeferred =
async(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getActiveProfile() }
}
val profilesDeferred =
async(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getProfiles() }
}
val auxDeferred =
async(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getAuxiliaryModels() }
}
val activeProfileResult = activeProfileDeferred.await()
val profilesResult = profilesDeferred.await()
val auxResult = auxDeferred.await()
val activeName =
if (activeProfileResult is NetworkResult.Success) {
activeProfileResult.data.active
} else {
null
}
val activeProfile =
if (profilesResult is NetworkResult.Success && activeName != null) {
profilesResult.data.profiles.find { it.name == activeName }
} else {
null
}
val mainModel =
if (auxResult is NetworkResult.Success) {
Pair(auxResult.data.main.provider, auxResult.data.main.model)
} else {
Pair("", "")
}
val auxTasks =
if (auxResult is NetworkResult.Success) {
auxResult.data.tasks
} else {
emptyList()
}
_uiState.update {
it.copy(
activeProfile = activeProfile ?: it.activeProfile,
mainModelProvider = mainModel.first,
mainModelModel = mainModel.second,
auxTasks = auxTasks,
modelPickerBusy = false,
)
}
}
}
/** Open the main model picker dialog. */
fun openMainModelPicker() {
_uiState.update { it.copy(showMainModelPicker = true) }
@@ -222,7 +295,7 @@ class ModelViewModel :
modelPickerBusy = false,
)
}
loadAll()
loadQuick()
}
}
@@ -265,7 +338,7 @@ class ModelViewModel :
modelPickerBusy = false,
)
}
loadAll()
loadQuick()
}
is NetworkResult.Failure -> {
@@ -341,7 +414,7 @@ class ModelViewModel :
auxPickerTask = "",
)
}
loadAll()
loadQuick()
}
is NetworkResult.Failure -> {
@@ -377,7 +450,7 @@ class ModelViewModel :
_uiState.update {
it.copy(toastMessage = "All auxiliary tasks reset to auto", modelPickerBusy = false)
}
loadAll()
loadQuick()
}
is NetworkResult.Failure -> {
@@ -442,6 +515,11 @@ class ModelViewModel :
modelName: String,
) {
viewModelScope.launch {
val cachedActiveProfileName = _uiState.value.activeProfile?.name
val activeProfileName =
if (!cachedActiveProfileName.isNullOrBlank()) {
cachedActiveProfileName
} else {
val activeProfileNameResResult =
withContext(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getActiveProfile() }
@@ -457,8 +535,10 @@ class ModelViewModel :
return@launch
}
is NetworkResult.Success -> {
val activeProfileName = activeProfileNameResResult.data.active
is NetworkResult.Success -> activeProfileNameResResult.data.active
}
}
val updateResResult =
withContext(Dispatchers.IO) {
safeApiCall {
@@ -471,7 +551,7 @@ class ModelViewModel :
when (updateResResult) {
is NetworkResult.Success -> {
_uiState.update { it.copy(toastMessage = "Successfully set profile model to $modelName") }
loadAll()
loadQuick()
}
is NetworkResult.Failure -> {
@@ -484,8 +564,6 @@ class ModelViewModel :
}
}
}
}
}
override fun clearToast() {
_uiState.update { it.copy(toastMessage = null) }