perf(model): optimize model tab load speed with two-phase fetch and loadQuick (#677)
This commit is contained in:
@@ -443,7 +443,7 @@ interface HermesApiService {
|
|||||||
@GET("api/model/options")
|
@GET("api/model/options")
|
||||||
suspend fun getModelOptions(
|
suspend fun getModelOptions(
|
||||||
@Query("refresh") refresh: Boolean = false,
|
@Query("refresh") refresh: Boolean = false,
|
||||||
@Query("include_unconfigured") includeUnconfigured: Boolean = true,
|
@Query("include_unconfigured") includeUnconfigured: Boolean = false,
|
||||||
): Response<ModelOptionsResponse>
|
): Response<ModelOptionsResponse>
|
||||||
|
|
||||||
@GET("api/model/auxiliary")
|
@GET("api/model/auxiliary")
|
||||||
|
|||||||
@@ -79,10 +79,7 @@ class ModelViewModel :
|
|||||||
fun loadAll(refresh: Boolean = false) {
|
fun loadAll(refresh: Boolean = false) {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val optionsDeferred =
|
// Phase 1: Launch fast lightweight calls first (profiles, aux, moa)
|
||||||
async(Dispatchers.IO) {
|
|
||||||
safeApiCall { ApiClient.hermesApi.getModelOptions(refresh = refresh) }
|
|
||||||
}
|
|
||||||
val activeProfileDeferred =
|
val activeProfileDeferred =
|
||||||
async(Dispatchers.IO) {
|
async(Dispatchers.IO) {
|
||||||
safeApiCall { ApiClient.hermesApi.getActiveProfile() }
|
safeApiCall { ApiClient.hermesApi.getActiveProfile() }
|
||||||
@@ -100,14 +97,18 @@ class ModelViewModel :
|
|||||||
safeApiCall { ApiClient.hermesApi.getMoaModels() }
|
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 activeProfileResult = activeProfileDeferred.await()
|
||||||
val profilesResult = profilesDeferred.await()
|
val profilesResult = profilesDeferred.await()
|
||||||
val auxResult = auxDeferred.await()
|
val auxResult = auxDeferred.await()
|
||||||
val moaResult = moaDeferred.await()
|
val moaResult = moaDeferred.await()
|
||||||
|
|
||||||
when (optionsResult) {
|
|
||||||
is NetworkResult.Success -> {
|
|
||||||
val activeName =
|
val activeName =
|
||||||
if (activeProfileResult is NetworkResult.Success) {
|
if (activeProfileResult is NetworkResult.Success) {
|
||||||
activeProfileResult.data.active
|
activeProfileResult.data.active
|
||||||
@@ -139,16 +140,26 @@ class ModelViewModel :
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render Phase 1 results right away so UI controls update without waiting for model options
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
activeProfile = activeProfile ?: it.activeProfile,
|
||||||
providers = optionsResult.data.providers.orEmpty(),
|
|
||||||
activeProfile = activeProfile,
|
|
||||||
pinnedModels = AuthManager.getPinnedModels(),
|
pinnedModels = AuthManager.getPinnedModels(),
|
||||||
mainModelProvider = mainModel.first,
|
mainModelProvider = mainModel.first,
|
||||||
mainModelModel = mainModel.second,
|
mainModelModel = mainModel.second,
|
||||||
auxTasks = auxTasks,
|
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 {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
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. */
|
/** Open the main model picker dialog. */
|
||||||
fun openMainModelPicker() {
|
fun openMainModelPicker() {
|
||||||
_uiState.update { it.copy(showMainModelPicker = true) }
|
_uiState.update { it.copy(showMainModelPicker = true) }
|
||||||
@@ -222,7 +295,7 @@ class ModelViewModel :
|
|||||||
modelPickerBusy = false,
|
modelPickerBusy = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
loadAll()
|
loadQuick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,7 +338,7 @@ class ModelViewModel :
|
|||||||
modelPickerBusy = false,
|
modelPickerBusy = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
loadAll()
|
loadQuick()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
@@ -341,7 +414,7 @@ class ModelViewModel :
|
|||||||
auxPickerTask = "",
|
auxPickerTask = "",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
loadAll()
|
loadQuick()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
@@ -377,7 +450,7 @@ class ModelViewModel :
|
|||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(toastMessage = "All auxiliary tasks reset to auto", modelPickerBusy = false)
|
it.copy(toastMessage = "All auxiliary tasks reset to auto", modelPickerBusy = false)
|
||||||
}
|
}
|
||||||
loadAll()
|
loadQuick()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
@@ -442,6 +515,11 @@ class ModelViewModel :
|
|||||||
modelName: String,
|
modelName: String,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
val cachedActiveProfileName = _uiState.value.activeProfile?.name
|
||||||
|
val activeProfileName =
|
||||||
|
if (!cachedActiveProfileName.isNullOrBlank()) {
|
||||||
|
cachedActiveProfileName
|
||||||
|
} else {
|
||||||
val activeProfileNameResResult =
|
val activeProfileNameResResult =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
safeApiCall { ApiClient.hermesApi.getActiveProfile() }
|
safeApiCall { ApiClient.hermesApi.getActiveProfile() }
|
||||||
@@ -457,8 +535,10 @@ class ModelViewModel :
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Success -> {
|
is NetworkResult.Success -> activeProfileNameResResult.data.active
|
||||||
val activeProfileName = activeProfileNameResResult.data.active
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val updateResResult =
|
val updateResResult =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
safeApiCall {
|
safeApiCall {
|
||||||
@@ -471,7 +551,7 @@ class ModelViewModel :
|
|||||||
when (updateResResult) {
|
when (updateResResult) {
|
||||||
is NetworkResult.Success -> {
|
is NetworkResult.Success -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Successfully set profile model to $modelName") }
|
_uiState.update { it.copy(toastMessage = "Successfully set profile model to $modelName") }
|
||||||
loadAll()
|
loadQuick()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
@@ -484,8 +564,6 @@ class ModelViewModel :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun clearToast() {
|
override fun clearToast() {
|
||||||
_uiState.update { it.copy(toastMessage = null) }
|
_uiState.update { it.copy(toastMessage = null) }
|
||||||
|
|||||||
Reference in New Issue
Block a user