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

This commit is contained in:
M57 authored and GitHub committed 2026-07-22 14:46:38 +03:00
1 parent 0bb418b4a7
commit f7afbfae85
2 files changed
+157 -79

No files matched your search

@@ -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,55 +97,69 @@ 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()
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()
}
val moaConfig =
if (moaResult is NetworkResult.Success) {
moaResult.data
} else {
null
}
// Render Phase 1 results right away so UI controls update without waiting for model options
_uiState.update {
it.copy(
activeProfile = activeProfile ?: it.activeProfile,
pinnedModels = AuthManager.getPinnedModels(),
mainModelProvider = mainModel.first,
mainModelModel = mainModel.second,
auxTasks = auxTasks,
moaConfig = moaConfig ?: it.moaConfig,
)
}
// Await Phase 2 (model catalog option provider tree)
val optionsResult = optionsDeferred.await()
when (optionsResult) {
is NetworkResult.Success -> {
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()
}
val moaConfig =
if (moaResult is NetworkResult.Success) {
moaResult.data
} else {
null
}
_uiState.update {
it.copy(
isLoading = false,
providers = optionsResult.data.providers.orEmpty(),
activeProfile = activeProfile,
pinnedModels = AuthManager.getPinnedModels(),
mainModelProvider = mainModel.first,
mainModelModel = mainModel.second,
auxTasks = auxTasks,
moaConfig = moaConfig,
)
}
}
@@ -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,45 +515,50 @@ class ModelViewModel :
modelName: String,
) {
viewModelScope.launch {
val activeProfileNameResResult =
withContext(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getActiveProfile() }
}
when (activeProfileNameResResult) {
is NetworkResult.Failure -> {
_uiState.update {
it.copy(
toastMessage =
"Failed to fetch active profile: ${activeProfileNameResResult.error.message}",
)
}
return@launch
}
is NetworkResult.Success -> {
val activeProfileName = activeProfileNameResResult.data.active
val updateResResult =
val cachedActiveProfileName = _uiState.value.activeProfile?.name
val activeProfileName =
if (!cachedActiveProfileName.isNullOrBlank()) {
cachedActiveProfileName
} else {
val activeProfileNameResResult =
withContext(Dispatchers.IO) {
safeApiCall {
ApiClient.hermesApi.updateProfileModel(
activeProfileName,
UpdateProfileModelRequest(providerSlug, modelName),
)
}
safeApiCall { ApiClient.hermesApi.getActiveProfile() }
}
when (updateResResult) {
is NetworkResult.Success -> {
_uiState.update { it.copy(toastMessage = "Successfully set profile model to $modelName") }
loadAll()
}
when (activeProfileNameResResult) {
is NetworkResult.Failure -> {
_uiState.update {
it.copy(
toastMessage = "Failed to set profile model: ${updateResResult.error.message}",
toastMessage =
"Failed to fetch active profile: ${activeProfileNameResResult.error.message}",
)
}
return@launch
}
is NetworkResult.Success -> activeProfileNameResResult.data.active
}
}
val updateResResult =
withContext(Dispatchers.IO) {
safeApiCall {
ApiClient.hermesApi.updateProfileModel(
activeProfileName,
UpdateProfileModelRequest(providerSlug, modelName),
)
}
}
when (updateResResult) {
is NetworkResult.Success -> {
_uiState.update { it.copy(toastMessage = "Successfully set profile model to $modelName") }
loadQuick()
}
is NetworkResult.Failure -> {
_uiState.update {
it.copy(
toastMessage = "Failed to set profile model: ${updateResResult.error.message}",
)
}
}
}