UI: change benchmark screen from raw markdown to table view

This commit is contained in:
Han Yin 2025-08-19 11:25:30 -07:00
parent 28198e7643
commit 1e1be75456
2 changed files with 144 additions and 45 deletions

View File

@ -3,22 +3,26 @@ package com.example.llama.ui.screens
import android.llama.cpp.InferenceEngine.State import android.llama.cpp.InferenceEngine.State
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.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
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.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.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProgressIndicatorDefaults import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -32,6 +36,8 @@ 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.text.font.FontStyle
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 com.example.llama.data.model.ModelInfo import com.example.llama.data.model.ModelInfo
@ -41,10 +47,13 @@ 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.theme.MonospacedTextStyle 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.viewmodel.BenchmarkResult
import com.example.llama.viewmodel.BenchmarkViewModel import com.example.llama.viewmodel.BenchmarkViewModel
@Composable @Composable
fun BenchmarkScreen( fun BenchmarkScreen(
loadingMetrics: ModelLoadingMetrics, loadingMetrics: ModelLoadingMetrics,
@ -86,30 +95,8 @@ fun BenchmarkScreen(
contentPadding = PaddingValues(8.dp), contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.Bottom, verticalArrangement = Arrangement.Bottom,
) { ) {
items(items = benchmarkResults) { result -> items(items = benchmarkResults) {
Card( BenchmarkResultCard(it)
modifier = Modifier.fillMaxWidth().padding(8.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(8.dp)
)
.padding(16.dp)
) {
Text(
text = result.text,
style = MonospacedTextStyle,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(4.dp))
ModelCardContentField("Time spent: ", formatMilliSeconds(result.duration))
}
}
} }
} }
@ -153,9 +140,7 @@ fun BenchmarkScreen(
// Selected model card and loading metrics // Selected model card and loading metrics
if (showModelCard) { if (showModelCard) {
selectedModel?.let { model -> selectedModel?.let { model ->
Box( Box(modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp)) {
modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp)
) {
ModelCardWithLoadingMetrics( ModelCardWithLoadingMetrics(
model = model, model = model,
loadingMetrics = loadingMetrics, loadingMetrics = loadingMetrics,
@ -198,3 +183,106 @@ private fun ModelCardWithLoadingMetrics(
// Row 4: Model loading time // Row 4: Model loading time
ModelCardContentField("Loading time", formatMilliSeconds(loadingMetrics.modelLoadingTimeMs)) ModelCardContentField("Loading time", formatMilliSeconds(loadingMetrics.modelLoadingTimeMs))
} }
@Composable
fun BenchmarkResultCard(result: BenchmarkResult) {
val rawTable = parseMarkdownTable(result.text.trimIndent())
val model = rawTable.getColumn("model").firstOrNull() ?: "Unknown"
val parameters = rawTable.getColumn("params").firstOrNull() ?: "-"
val size = rawTable.getColumn("size").firstOrNull() ?: "-"
Card(
modifier = Modifier.fillMaxWidth().padding(8.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(8.dp)
)
.padding(16.dp)
) {
Row {
Text(
text = "Model",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Normal,
)
Spacer(modifier = Modifier.width(16.dp))
Text(
modifier = Modifier.weight(1f),
text = model,
textAlign = TextAlign.Start,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Light,
fontStyle = FontStyle.Italic,
)
}
Spacer(modifier = Modifier.height(8.dp))
Row {
ModelCardContentField("Parameters", parameters)
Spacer(modifier = Modifier.weight(1f))
ModelCardContentField("Size", size)
}
BenchmarkResultTable(rawTable)
ModelCardContentField("Time spent: ", formatMilliSeconds(result.duration))
}
}
}
// Needs to be aligned with `bench` implementation
private val COLUMNS_TO_KEEP = setOf("backend", "test", "t/s")
private val WEIGHTS_EACH_COLUMN = listOf(1f, 1f, 2f)
@Composable
fun BenchmarkResultTable(
rawTable: TableData,
columnsToKeep: Set<String> = COLUMNS_TO_KEEP,
columnWeights: List<Float> = WEIGHTS_EACH_COLUMN
) {
val (headers, rows) = rawTable.filterColumns(columnsToKeep)
Column(
modifier = Modifier
.padding(horizontal = 12.dp, vertical = 16.dp)
.border(1.dp, MaterialTheme.colorScheme.outline, shape = RoundedCornerShape(4.dp))
.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
BenchmarkResultTableRow(headers, columnWeights, isHeader = true)
HorizontalDivider(thickness = 1.dp)
rows.forEach { BenchmarkResultTableRow(it, columnWeights) }
}
}
@Composable
fun BenchmarkResultTableRow(
cells: List<String>,
weights: List<Float>? = null,
isHeader: Boolean = false,
) {
val effectiveWeights = weights ?: List(cells.size) { 1f }
Row(modifier = Modifier.fillMaxWidth()) {
cells.forEachIndexed { index, cell ->
Text(
modifier = Modifier.weight(effectiveWeights.getOrElse(index) { 1f }),
text = cell,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge,
fontWeight = if (isHeader) FontWeight.Normal else FontWeight.Light,
fontStyle = if (isHeader) FontStyle.Normal else FontStyle.Italic
)
}
}
}

View File

@ -2,37 +2,48 @@ package com.example.llama.util
/** /**
* A basic * A basic table data holder separating rows and columns
*/ */
data class MarkdownTableData( data class TableData(
val headers: List<String>, val headers: List<String>,
val rows: List<List<String>> val rows: List<List<String>>
) { ) {
val columnCount: Int get() = headers.size val columnCount: Int get() = headers.size
val rowCount: Int get() = rows.size val rowCount: Int get() = rows.size
/**
* Generate a copy of the original table with only the [keep] columns
*/
fun filterColumns(keep: Set<String>): TableData =
headers.mapIndexedNotNull { index, name ->
if (name in keep) index else null
}.let { keepIndices ->
val newHeaders = keepIndices.map { headers[it] }
val newRows = rows.map { row -> keepIndices.map { row.getOrElse(it) { "" } } }
TableData(newHeaders, newRows)
}
/**
* Obtain the data in the specified column
*/
fun getColumn(name: String): List<String> {
val index = headers.indexOf(name)
if (index == -1) return emptyList()
return rows.mapNotNull { it.getOrNull(index) }
}
} }
/** /**
* Formats llama-bench's markdown output into structured [MarkdownTableData] * Formats llama-bench's markdown output into structured [MarkdownTableData]
*/ */
fun parseMarkdownTableFiltered( fun parseMarkdownTable(markdown: String): TableData {
markdown: String,
keepColumns: Set<String>
): MarkdownTableData {
val lines = markdown.trim().lines().filter { it.startsWith("|") } val lines = markdown.trim().lines().filter { it.startsWith("|") }
if (lines.size < 2) return MarkdownTableData(emptyList(), emptyList()) if (lines.size < 2) return TableData(emptyList(), emptyList())
val rawHeaders = lines[0].split("|").map { it.trim() }.filter { it.isNotEmpty() }
val keepIndices = rawHeaders.mapIndexedNotNull { index, name ->
if (name in keepColumns) index else null
}
val headers = keepIndices.map { rawHeaders[it] }
val headers = lines[0].split("|").map { it.trim() }.filter { it.isNotEmpty() }
val rows = lines.drop(2).map { line -> val rows = lines.drop(2).map { line ->
val cells = line.split("|").map { it.trim() }.filter { it.isNotEmpty() } line.split("|").map { it.trim() }.filter { it.isNotEmpty() }
keepIndices.map { cells.getOrElse(it) { "" } }
} }
return MarkdownTableData(headers, rows) return TableData(headers, rows)
} }