UI: add quick action buttons to benchmark screen's result card

This commit is contained in:
Han Yin 2025-08-29 16:21:57 -07:00
parent 659f59e22a
commit c848005d11
4 changed files with 95 additions and 28 deletions

View File

@ -259,9 +259,7 @@ fun AppContent(
// Benchmark screen // Benchmark screen
currentRoute.startsWith(AppDestinations.BENCHMARK_ROUTE) -> { currentRoute.startsWith(AppDestinations.BENCHMARK_ROUTE) -> {
val engineState by benchmarkViewModel.engineState.collectAsState()
val showModelCard by benchmarkViewModel.showModelCard.collectAsState() val showModelCard by benchmarkViewModel.showModelCard.collectAsState()
val benchmarkResults by benchmarkViewModel.benchmarkResults.collectAsState()
ScaffoldConfig( ScaffoldConfig(
topBarConfig = TopBarConfig.Performance( topBarConfig = TopBarConfig.Performance(
@ -274,22 +272,9 @@ fun AppContent(
), ),
bottomBarConfig = BottomBarConfig.Benchmark( bottomBarConfig = BottomBarConfig.Benchmark(
engineIdle = !engineState.isUninterruptible, engineIdle = !engineState.isUninterruptible,
onShare = { onShare = { benchmarkViewModel.shareResult(handleScaffoldEvent) },
benchmarkResults.lastOrNull()?.let { onRerun = { benchmarkViewModel.rerunBenchmark(handleScaffoldEvent) },
handleScaffoldEvent(ScaffoldEvent.ShareText(it.text)) onClear = { benchmarkViewModel.clearResults(handleScaffoldEvent) },
}
},
onRerun = {
if (engineState.isUninterruptible) {
handleScaffoldEvent(ScaffoldEvent.ShowSnackbar(
message = "Benchmark already in progress!\n" +
"Please wait for the current run to complete."
))
} else {
benchmarkViewModel.runBenchmark()
}
},
onClear = benchmarkViewModel::clearResults,
showModelCard = showModelCard, showModelCard = showModelCard,
onToggleModelCard = benchmarkViewModel::toggleModelCard, onToggleModelCard = benchmarkViewModel::toggleModelCard,
) )
@ -478,6 +463,7 @@ fun AppContent(
BenchmarkScreen( BenchmarkScreen(
loadingMetrics = metrics, loadingMetrics = metrics,
onScaffoldEvent = handleScaffoldEvent,
onNavigateBack = { navigationActions.navigateUp() }, onNavigateBack = { navigationActions.navigateUp() },
viewModel = benchmarkViewModel viewModel = benchmarkViewModel
) )

View File

@ -27,22 +27,25 @@ fun BenchmarkBottomBar(
showModelCard: Boolean, showModelCard: Boolean,
onToggleModelCard: (Boolean) -> Unit, onToggleModelCard: (Boolean) -> Unit,
) { ) {
val controlTint =
if (engineIdle) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f)
BottomAppBar( BottomAppBar(
actions = { actions = {
IconButton(onClick = onRerun) { IconButton(onClick = onRerun) {
Icon( Icon(
imageVector = Icons.Default.Replay, imageVector = Icons.Default.Replay,
contentDescription = "Run the benchmark again", contentDescription = "Run the benchmark again",
tint = tint = controlTint
if (engineIdle) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f)
) )
} }
IconButton(onClick = onClear) { IconButton(onClick = onClear) {
Icon( Icon(
imageVector = Icons.Default.ClearAll, imageVector = Icons.Default.ClearAll,
contentDescription = "Clear benchmark results" contentDescription = "Clear benchmark results",
tint = controlTint
) )
} }

View File

@ -1,6 +1,8 @@
package com.example.llama.ui.screens package com.example.llama.ui.screens
import android.content.Intent
import android.llama.cpp.InferenceEngine.State import android.llama.cpp.InferenceEngine.State
import android.widget.Toast
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
@ -19,11 +21,17 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Replay
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.ProgressIndicatorDefaults import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -36,10 +44,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import com.example.llama.data.model.ModelInfo import com.example.llama.data.model.ModelInfo
import com.example.llama.engine.ModelLoadingMetrics import com.example.llama.engine.ModelLoadingMetrics
import com.example.llama.ui.components.ModelCardContentArchitectureRow import com.example.llama.ui.components.ModelCardContentArchitectureRow
@ -47,6 +57,7 @@ import com.example.llama.ui.components.ModelCardContentContextRow
import com.example.llama.ui.components.ModelCardContentField import com.example.llama.ui.components.ModelCardContentField
import com.example.llama.ui.components.ModelCardCoreExpandable import com.example.llama.ui.components.ModelCardCoreExpandable
import com.example.llama.ui.components.ModelUnloadDialogHandler import com.example.llama.ui.components.ModelUnloadDialogHandler
import com.example.llama.ui.scaffold.ScaffoldEvent
import com.example.llama.util.TableData import com.example.llama.util.TableData
import com.example.llama.util.formatMilliSeconds import com.example.llama.util.formatMilliSeconds
import com.example.llama.util.parseMarkdownTable import com.example.llama.util.parseMarkdownTable
@ -57,9 +68,12 @@ import com.example.llama.viewmodel.BenchmarkViewModel
@Composable @Composable
fun BenchmarkScreen( fun BenchmarkScreen(
loadingMetrics: ModelLoadingMetrics, loadingMetrics: ModelLoadingMetrics,
onScaffoldEvent: (ScaffoldEvent) -> Unit,
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
viewModel: BenchmarkViewModel viewModel: BenchmarkViewModel
) { ) {
val context = LocalContext.current
// View model states // View model states
val engineState by viewModel.engineState.collectAsState() val engineState by viewModel.engineState.collectAsState()
val unloadDialogState by viewModel.unloadModelState.collectAsState() val unloadDialogState by viewModel.unloadModelState.collectAsState()
@ -86,6 +100,12 @@ fun BenchmarkScreen(
viewModel.onBackPressed(onNavigateBack) viewModel.onBackPressed(onNavigateBack)
} }
val onInfo = {
Toast.makeText(context, "Please refer to this post for more details on the benchmark methodology", Toast.LENGTH_SHORT).show()
val intent = Intent(Intent.ACTION_VIEW, "https://blog.steelph0enix.dev/posts/llama-cpp-guide/#llama-bench".toUri())
context.startActivity(intent)
}
Box( Box(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
@ -96,7 +116,11 @@ fun BenchmarkScreen(
verticalArrangement = Arrangement.Bottom, verticalArrangement = Arrangement.Bottom,
) { ) {
items(items = benchmarkResults) { items(items = benchmarkResults) {
BenchmarkResultCard(it) BenchmarkResultCard(
result = it,
onRerun = { viewModel.rerunBenchmark(onScaffoldEvent) },
onInfo = onInfo,
)
} }
} }
@ -186,7 +210,11 @@ private fun ModelCardWithLoadingMetrics(
@Composable @Composable
fun BenchmarkResultCard(result: BenchmarkResult) { fun BenchmarkResultCard(
result: BenchmarkResult,
onRerun: () -> Unit,
onInfo: () -> Unit,
) {
val rawTable = parseMarkdownTable(result.text.trimIndent()) val rawTable = parseMarkdownTable(result.text.trimIndent())
val model = rawTable.getColumn("model").firstOrNull() ?: "Unknown" val model = rawTable.getColumn("model").firstOrNull() ?: "Unknown"
val parameters = rawTable.getColumn("params").firstOrNull() ?: "-" val parameters = rawTable.getColumn("params").firstOrNull() ?: "-"
@ -236,6 +264,28 @@ fun BenchmarkResultCard(result: BenchmarkResult) {
BenchmarkResultTable(rawTable) BenchmarkResultTable(rawTable)
ModelCardContentField("Time spent: ", formatMilliSeconds(result.duration)) ModelCardContentField("Time spent: ", formatMilliSeconds(result.duration))
Spacer(modifier = Modifier.height(8.dp))
Row {
OutlinedButton(onClick = onRerun) {
Icon(
imageVector = Icons.Default.Replay,
contentDescription = "Run the benchmark again"
)
Text("Run again", modifier = Modifier.padding(start = 6.dp))
}
Spacer(modifier = Modifier.weight(1f))
FilledTonalButton(onClick = onInfo) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = "Information about what the result means"
)
Text("How to interpret", modifier = Modifier.padding(start = 6.dp))
}
}
} }
} }
} }

View File

@ -4,6 +4,7 @@ import android.llama.cpp.isUninterruptible
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.llama.data.model.ModelInfo import com.example.llama.data.model.ModelInfo
import com.example.llama.engine.BenchmarkService import com.example.llama.engine.BenchmarkService
import com.example.llama.ui.scaffold.ScaffoldEvent
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -30,7 +31,7 @@ class BenchmarkViewModel @Inject constructor(
val benchmarkResults: StateFlow<List<BenchmarkResult>> = _benchmarkResults.asStateFlow() val benchmarkResults: StateFlow<List<BenchmarkResult>> = _benchmarkResults.asStateFlow()
// UI state: Model card // UI state: Model card
private val _showModelCard = MutableStateFlow(true) private val _showModelCard = MutableStateFlow(false)
val showModelCard = _showModelCard.asStateFlow() val showModelCard = _showModelCard.asStateFlow()
fun toggleModelCard(show: Boolean) { fun toggleModelCard(show: Boolean) {
@ -68,10 +69,37 @@ class BenchmarkViewModel @Inject constructor(
return true return true
} }
override suspend fun performCleanup() = clearResults() override suspend fun performCleanup() { clearResults(null) }
fun clearResults() { fun clearResults(onScaffoldEvent: ((ScaffoldEvent) -> Unit)?) =
if (engineState.value.isUninterruptible) {
false
} else {
_benchmarkResults.value = emptyList() _benchmarkResults.value = emptyList()
onScaffoldEvent?.invoke(ScaffoldEvent.ShowSnackbar(
message = "All benchmark results cleared."
))
true
}
/**
* Rerun the benchmark
*/
fun rerunBenchmark(onScaffoldEvent: (ScaffoldEvent) -> Unit) {
if (engineState.value.isUninterruptible) {
onScaffoldEvent(ScaffoldEvent.ShowSnackbar(
message = "Benchmark already in progress!\n" +
"Please wait for the current run to complete."
))
} else {
runBenchmark()
}
}
fun shareResult(onScaffoldEvent: (ScaffoldEvent) -> Unit) {
_benchmarkResults.value.lastOrNull()?.let{
onScaffoldEvent(ScaffoldEvent.ShareText(it.text))
}
} }
} }