From c848005d1130f30bf340e3cde555b0531bb6c423 Mon Sep 17 00:00:00 2001 From: Han Yin Date: Fri, 29 Aug 2025 16:21:57 -0700 Subject: [PATCH] UI: add quick action buttons to benchmark screen's result card --- .../java/com/example/llama/MainActivity.kt | 22 ++------ .../scaffold/bottombar/BenchmarkBottomBar.kt | 11 ++-- .../llama/ui/screens/BenchmarkScreen.kt | 54 ++++++++++++++++++- .../llama/viewmodel/BenchmarkViewModel.kt | 36 +++++++++++-- 4 files changed, 95 insertions(+), 28 deletions(-) diff --git a/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt b/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt index 0547e9bbaf..e1bf10b07e 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt @@ -259,9 +259,7 @@ fun AppContent( // Benchmark screen currentRoute.startsWith(AppDestinations.BENCHMARK_ROUTE) -> { - val engineState by benchmarkViewModel.engineState.collectAsState() val showModelCard by benchmarkViewModel.showModelCard.collectAsState() - val benchmarkResults by benchmarkViewModel.benchmarkResults.collectAsState() ScaffoldConfig( topBarConfig = TopBarConfig.Performance( @@ -274,22 +272,9 @@ fun AppContent( ), bottomBarConfig = BottomBarConfig.Benchmark( engineIdle = !engineState.isUninterruptible, - onShare = { - benchmarkResults.lastOrNull()?.let { - handleScaffoldEvent(ScaffoldEvent.ShareText(it.text)) - } - }, - 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, + onShare = { benchmarkViewModel.shareResult(handleScaffoldEvent) }, + onRerun = { benchmarkViewModel.rerunBenchmark(handleScaffoldEvent) }, + onClear = { benchmarkViewModel.clearResults(handleScaffoldEvent) }, showModelCard = showModelCard, onToggleModelCard = benchmarkViewModel::toggleModelCard, ) @@ -478,6 +463,7 @@ fun AppContent( BenchmarkScreen( loadingMetrics = metrics, + onScaffoldEvent = handleScaffoldEvent, onNavigateBack = { navigationActions.navigateUp() }, viewModel = benchmarkViewModel ) diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/BenchmarkBottomBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/BenchmarkBottomBar.kt index 933f23ef92..a4ce8ae38b 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/BenchmarkBottomBar.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/BenchmarkBottomBar.kt @@ -27,22 +27,25 @@ fun BenchmarkBottomBar( showModelCard: Boolean, onToggleModelCard: (Boolean) -> Unit, ) { + val controlTint = + if (engineIdle) MaterialTheme.colorScheme.onSurface + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + BottomAppBar( actions = { IconButton(onClick = onRerun) { Icon( imageVector = Icons.Default.Replay, contentDescription = "Run the benchmark again", - tint = - if (engineIdle) MaterialTheme.colorScheme.onSurface - else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + tint = controlTint ) } IconButton(onClick = onClear) { Icon( imageVector = Icons.Default.ClearAll, - contentDescription = "Clear benchmark results" + contentDescription = "Clear benchmark results", + tint = controlTint ) } diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/BenchmarkScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/BenchmarkScreen.kt index 475dc9a726..b5cdddb383 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/BenchmarkScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/BenchmarkScreen.kt @@ -1,6 +1,8 @@ package com.example.llama.ui.screens +import android.content.Intent import android.llama.cpp.InferenceEngine.State +import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.compose.foundation.background 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.items 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.CardDefaults import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.ProgressIndicatorDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -36,10 +44,12 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import com.example.llama.data.model.ModelInfo import com.example.llama.engine.ModelLoadingMetrics 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.ModelCardCoreExpandable 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.formatMilliSeconds import com.example.llama.util.parseMarkdownTable @@ -57,9 +68,12 @@ import com.example.llama.viewmodel.BenchmarkViewModel @Composable fun BenchmarkScreen( loadingMetrics: ModelLoadingMetrics, + onScaffoldEvent: (ScaffoldEvent) -> Unit, onNavigateBack: () -> Unit, viewModel: BenchmarkViewModel ) { + val context = LocalContext.current + // View model states val engineState by viewModel.engineState.collectAsState() val unloadDialogState by viewModel.unloadModelState.collectAsState() @@ -86,6 +100,12 @@ fun BenchmarkScreen( 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( modifier = Modifier.fillMaxSize() ) { @@ -96,7 +116,11 @@ fun BenchmarkScreen( verticalArrangement = Arrangement.Bottom, ) { items(items = benchmarkResults) { - BenchmarkResultCard(it) + BenchmarkResultCard( + result = it, + onRerun = { viewModel.rerunBenchmark(onScaffoldEvent) }, + onInfo = onInfo, + ) } } @@ -186,7 +210,11 @@ private fun ModelCardWithLoadingMetrics( @Composable -fun BenchmarkResultCard(result: BenchmarkResult) { +fun BenchmarkResultCard( + result: BenchmarkResult, + onRerun: () -> Unit, + onInfo: () -> Unit, +) { val rawTable = parseMarkdownTable(result.text.trimIndent()) val model = rawTable.getColumn("model").firstOrNull() ?: "Unknown" val parameters = rawTable.getColumn("params").firstOrNull() ?: "-" @@ -236,6 +264,28 @@ fun BenchmarkResultCard(result: BenchmarkResult) { BenchmarkResultTable(rawTable) 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)) + } + } } } } diff --git a/examples/llama.android/app/src/main/java/com/example/llama/viewmodel/BenchmarkViewModel.kt b/examples/llama.android/app/src/main/java/com/example/llama/viewmodel/BenchmarkViewModel.kt index a086dfc46d..ff527f8629 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/viewmodel/BenchmarkViewModel.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/viewmodel/BenchmarkViewModel.kt @@ -4,6 +4,7 @@ import android.llama.cpp.isUninterruptible import androidx.lifecycle.viewModelScope import com.example.llama.data.model.ModelInfo import com.example.llama.engine.BenchmarkService +import com.example.llama.ui.scaffold.ScaffoldEvent import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -30,7 +31,7 @@ class BenchmarkViewModel @Inject constructor( val benchmarkResults: StateFlow> = _benchmarkResults.asStateFlow() // UI state: Model card - private val _showModelCard = MutableStateFlow(true) + private val _showModelCard = MutableStateFlow(false) val showModelCard = _showModelCard.asStateFlow() fun toggleModelCard(show: Boolean) { @@ -68,10 +69,37 @@ class BenchmarkViewModel @Inject constructor( return true } - override suspend fun performCleanup() = clearResults() + override suspend fun performCleanup() { clearResults(null) } - fun clearResults() { - _benchmarkResults.value = emptyList() + fun clearResults(onScaffoldEvent: ((ScaffoldEvent) -> Unit)?) = + if (engineState.value.isUninterruptible) { + false + } else { + _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)) + } } }