Added input, hardware fields to data

This commit is contained in:
2026-08-09 17:25:18 +02:00
parent 8f4c6cc630
commit 60b1a69bc6
10 changed files with 184 additions and 36 deletions
@@ -110,7 +110,7 @@ object CLI {
data class FileData<T>(val file: InputStream, val parse: (String) -> T?) : IData<T> {
override fun toList(): List<T> {
val raw = file.bufferedReader().use { it.readText() }.split(',')
val parsed = ArrayList<T>(raw.size)
val errors = mutableListOf<String>()
raw.forEach {
@@ -201,6 +201,8 @@ object CLI {
option("--min", "--m", "--minutes", help = "Set the time unit to minutes").flag().convert { TimeUnit.MINUTES },
option("--h", "--hour", help = "Set the time unit to hours").flag().convert { TimeUnit.HOURS }
).single()
val input by option("--input", help = "Set the input used to benchmark")
val hardware by option("--hardware", "--hw", help = "Set the hardware on which the benchmark was run")
override fun run() = ProjectHandlers.newData(parent.finder, label, warmup, measurement, unit)
}
@@ -76,7 +76,7 @@ class Client private constructor(private val _auth: AuthProvider) {
println("Call to ${route.pattern} [$ctr] was cancelled")
ErrorResponse(COROUTINE_CANCELLED).error()
} catch (e: Exception) {
println("Call to ${route.pattern} [$ctr] ran into an exception")
println("Call to ${route.pattern} [$ctr] ran into an exception: ${e.message}")
ErrorResponse(e.message ?: "Unknown error.").error()
}
}
@@ -30,7 +30,7 @@ interface IProjectRepo {
suspend fun deleteLabel(id: Uuid): Either<ErrorResponse, Unit>
suspend fun newEntry(label: Uuid, timestamp: Instant, warmups: List<Float>, measurements: List<Float>,
unit: TimeUnit): Either<ErrorResponse, EntryResponse>
unit: TimeUnit, input: String, hardware: String): Either<ErrorResponse, EntryResponse>
suspend fun deleteEntry(id: Uuid): Either<ErrorResponse, Unit>
companion object {
@@ -54,9 +54,9 @@ interface IProjectRepo {
_client.callRoute(Routes.Label.delete, id).ignoreValue()
override suspend fun newEntry(label: Uuid, timestamp: Instant, warmups: List<Float>,
measurements: List<Float>, unit: TimeUnit
measurements: List<Float>, unit: TimeUnit, input: String, hardware: String
): Either<ErrorResponse, EntryResponse> =
_client.callRoute(Routes.Entry.new, EntryRequest(label, timestamp, _projectId, warmups, measurements, unit))
_client.callRoute(Routes.Entry.new, EntryRequest(label, timestamp, _projectId, warmups, measurements, unit, input, hardware))
override suspend fun deleteEntry(id: Uuid): Either<ErrorResponse, Unit> =
_client.callRoute(Routes.Entry.delete, id).ignoreValue()
@@ -57,11 +57,11 @@ class ProjectVM(
}
data class Entry(val id: Uuid, val label: Label, val timeStamp: Instant, val warmups: List<Float>,
val measurements: List<Float>, val nativeUnit: TimeUnit) {
val measurements: List<Float>, val nativeUnit: TimeUnit, val input: String, val hardware: String) {
companion object {
fun fromResponse(it: EntryResponse, map: Map<Uuid, Label>) = Entry(
it.id, map[it.labelId] ?: Label.invalid, it.timestamp,
it.warmups, it.measurements, it.unit
it.warmups, it.measurements, it.unit, it.input, it.hardware
)
}
}
@@ -72,6 +72,8 @@ class ProjectVM(
private val _editable = mutableStateOf(false)
private val _labels = mutableStateOf(mapOf<Uuid, Label>())
private val _entries = mutableStateOf(listOf<Entry>())
private val _inputs = mutableStateOf(setOf<String>())
private val _hardware = mutableStateOf(setOf<String>())
val name = _name.immutable()
val owner = _owner.immutable()
@@ -79,6 +81,8 @@ class ProjectVM(
val editable = _editable.immutable()
val labels = _labels.immutable()
val entries = _entries.immutable()
val inputs = _inputs.immutable()
val hardware = _hardware.immutable()
private var _job: Job? = null
@@ -101,7 +105,15 @@ class ProjectVM(
_public.value = it.isPublic
_editable.value = it.isEditable
_labels.value = it.usedLabels.associate { l -> l.id to Label.fromResponse(l) }
_entries.value = it.entries.map { e -> Entry.fromResponse(e, _labels.value) }
val localInputs = mutableSetOf<String>()
val localHardware = mutableSetOf<String>()
_entries.value = it.entries.map { e ->
localInputs += e.input
localHardware += e.hardware
Entry.fromResponse(e, _labels.value)
}
_inputs.value = localInputs
_hardware.value = localHardware
}
}
}
@@ -112,7 +124,11 @@ class ProjectVM(
is ProjectEvent.EntryDeleted -> _entries.value = _entries.value.filter { it.id != event.id }
is ProjectEvent.LabelChanged -> _labels.value += (event.label.id to Label.fromResponse(event.label))
is ProjectEvent.LabelDeleted -> _labels.value = _labels.value.filter { it.value.id != event.id }
is ProjectEvent.NewEntry -> _entries.value = _entries.value.insort(Entry.fromResponse(event.entry, _labels.value), Entry::timeStamp)
is ProjectEvent.NewEntry -> {
_entries.value = _entries.value.insort(Entry.fromResponse(event.entry, _labels.value), Entry::timeStamp)
_inputs.value += event.entry.input
_hardware.value += event.entry.hardware
}
is ProjectEvent.NewLabel -> _labels.value += (event.label.id to Label.fromResponse(event.label))
is ProjectEvent.Updated -> {
_name.value = event.changes.name
@@ -124,7 +140,9 @@ class ProjectVM(
fun update(name: String?, isPublic: Boolean?) {
withScope {
_repo.update(name, isPublic).snackOr { refresh() }
_repo.update(name, isPublic).snackOr {
if(_job == null) refresh()
}
}
}
@@ -142,7 +160,8 @@ class ProjectVM(
fun mkLabel(name: String, color: Color) {
withScope {
_repo.newLabel(name, color).snackOr {
_labels.value += it.id to Label(it.id, it.name, it.color)
if(_job == null)
_labels.value += it.id to Label(it.id, it.name, it.color)
}
}
}
@@ -151,7 +170,8 @@ class ProjectVM(
withScope {
val old = _labels.value[id] ?: return@withScope
_repo.updateLabel(id, name, color).snackOr {
_labels.value += id to Label(id, name ?: old.name, color?.hexString() ?: old.colorStr, color ?: old.uiColor)
if(_job == null)
_labels.value += id to Label(id, name ?: old.name, color?.hexString() ?: old.colorStr, color ?: old.uiColor)
}
}
}
@@ -159,15 +179,20 @@ class ProjectVM(
fun deleteLabel(id: Uuid) {
withScope {
_repo.deleteLabel(id).snackOr {
_labels.value -= id
if(_job == null)
_labels.value -= id
}
}
}
fun mkEntry(label: Label, warmups: List<Float>, measurements: List<Float>, unit: TimeUnit) {
fun mkEntry(label: Label, warmups: List<Float>, measurements: List<Float>, unit: TimeUnit, input: String, hardware: String) {
withScope {
_repo.newEntry(label.id, Clock.System.now(), warmups, measurements, unit).snackOr {
_entries.value += Entry.fromResponse(it, _labels.value)
_repo.newEntry(label.id, Clock.System.now(), warmups, measurements, unit, input, hardware).snackOr {
if(_job == null) {
_entries.value += Entry.fromResponse(it, _labels.value)
_inputs.value += input
_hardware.value += hardware
}
}
}
}
@@ -175,7 +200,8 @@ class ProjectVM(
fun deleteEntry(id: Uuid) {
withScope {
_repo.deleteEntry(id).snackOr {
_entries.value = _entries.value.filter { it.id != id }
if(_job == null)
_entries.value = _entries.value.filter { it.id != id }
}
}
}
@@ -13,13 +13,16 @@ import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.backhandler.BackHandler
import androidx.compose.ui.draw.scale
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.toLowerCase
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -27,6 +30,7 @@ import com.composables.icons.lucide.*
import com.jaytux.phoebench.clients.darken
import com.jaytux.phoebench.clients.data.ProjectVM
import com.jaytux.phoebench.clients.data.mutableStateSetFrom
import com.jaytux.phoebench.clients.data.mutableStateSetOf
import com.jaytux.phoebench.clients.dualLerp
import com.jaytux.phoebench.clients.fmt
import com.jaytux.phoebench.clients.fmtRange
@@ -153,11 +157,14 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) {
addOpen = false
}
if(addingData) AddEntryDialog(labels,
val usedInputs by vm.inputs
val usedHardware by vm.hardware
if(addingData) AddEntryDialog(labels, usedInputs, usedHardware,
onCancel = { addingData = false; addOpen = false },
onAddLbl = { name, lbl -> vm.mkLabel(name, lbl) }
) { label, warmups, measurements, unit ->
vm.mkEntry(label, warmups, measurements, unit)
) { label, warmups, measurements, unit, input, hardware ->
vm.mkEntry(label, warmups, measurements, unit, input, hardware)
addOpen = false
}
}
@@ -227,26 +234,33 @@ fun AddLabelDialog(onCancel: () -> Unit, onAdd: (name: String, color: Color) ->
}
@Composable
fun QuickLabel(lbl: ProjectVM.Label) {
fun QuickLabel(lbl: ProjectVM.Label, hollow: Boolean = false) {
Row(Modifier.height(IntrinsicSize.Min).padding(vertical = 3.dp), verticalAlignment = Alignment.Bottom) {
Box(Modifier.fillMaxHeight().aspectRatio(1f).background(lbl.uiColor)) {}
Box(Modifier.fillMaxHeight().aspectRatio(1f).background(lbl.uiColor)) {
if(hollow) {
Box(Modifier.fillMaxHeight(0.75f).aspectRatio(1f).align(Alignment.Center).background(Color.Gray.copy(alpha = 0.5f)))
}
}
Spacer(Modifier.width(15.dp))
Text(lbl.name)
Text(" ${lbl.colorStr}", style = MaterialTheme.typography.bodySmall, color = LocalContentColor.current.copy(alpha = 0.75f))
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddEntryDialog(
labels: Map<Uuid, ProjectVM.Label>,
labels: Map<Uuid, ProjectVM.Label>, usedInputs: Set<String>, usedHardware: Set<String>,
onCancel: () -> Unit, onAddLbl: (name: String, color: Color) -> Unit,
onAdd: (label: ProjectVM.Label, warmups: List<Float>, measurements: List<Float>, unit: TimeUnit) -> Unit
onAdd: (label: ProjectVM.Label, warmups: List<Float>, measurements: List<Float>, unit: TimeUnit, input: String, hardware: String) -> Unit
) {
Dialog(onDismissRequest = onCancel) {
var label by remember { mutableStateOf<ProjectVM.Label?>(null) }
var warmups by remember { mutableStateOf("") }
var measurements by remember { mutableStateOf("") }
var unit by remember { mutableStateOf(TimeUnit.SECONDS) }
var input by remember { mutableStateOf("") }
var hardware by remember { mutableStateOf("") }
var addingLabel by remember { mutableStateOf(false) }
var warmupParsed by remember { mutableStateOf<Either<String, List<Float>>>(listOf<Float>().value()) }
@@ -308,6 +322,39 @@ fun AddEntryDialog(
}
}
Row {
var inputFocused by remember { mutableStateOf(false) }
var hardwareFocused by remember { mutableStateOf(false) }
Box(Modifier.weight(0.5f)) {
ExposedDropdownMenuBox(inputFocused, {}) {
OutlinedTextField(input, { input = it },
Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable).onFocusChanged { inputFocused = it.isFocused },
label = { Text("Input") }
)
ExposedDropdownMenu(inputFocused, {}) {
usedInputs.filter { input.lowercase() in it.lowercase() }.sorted().forEach { inp ->
DropdownMenuItem(text = { Text(inp) }, onClick = { input = inp })
}
}
}
}
Spacer(Modifier.width(5.dp))
Box(Modifier.weight(0.5f)) {
ExposedDropdownMenuBox(hardwareFocused, { inputFocused = false }) {
OutlinedTextField(hardware, { hardware = it },
Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable).onFocusChanged { hardwareFocused = it.isFocused },
label = { Text("Hardware") }
)
ExposedDropdownMenu(hardwareFocused, { hardwareFocused = false }) {
usedHardware.filter { hardware.lowercase() in it.lowercase() }.sorted().forEach { hw ->
DropdownMenuItem(text = { Text(hw) }, onClick = { hardware = hw })
}
}
}
}
}
Spacer(Modifier.height(7.dp))
Text("Data (separate data points by commas)")
Spacer(Modifier.height(5.dp))
@@ -326,8 +373,8 @@ fun AddEntryDialog(
Spacer(Modifier.height(5.dp))
CancelConfirmXRow(onCancel, {
onAdd(label!!, warmupParsed.asValue()!!, measureParsed.asValue()!!, unit)
}, confirmText = "Create", canConfirm = label != null && warmupParsed.isValue() && measureParsed.isValue())
onAdd(label!!, warmupParsed.asValue()!!, measureParsed.asValue()!!, unit, input, hardware)
}, confirmText = "Create", canConfirm = label != null && warmupParsed.isValue() && measureParsed.isValue() && input.isNotBlank() && hardware.isNotBlank())
}
}
@@ -356,17 +403,33 @@ fun ProjectPlotArea(vm: ProjectVM){
val labelFilter = remember(labels) { mutableStateSetFrom(labels.values) }
val labelFilterKey by labelFilter.revision
LaunchedEffect(entries, displayWarmups, currentUnit, timeFilter, timeMin, timeMax, labelFilterKey) {
val allInputs by vm.inputs
val allHardware by vm.hardware
val enabledInputs = remember { mutableStateSetOf<String>() }
val enabledHardware = remember { mutableStateSetOf<String>() }
var filterInput by remember { mutableStateOf<String?>(null) }
var filterHardware by remember { mutableStateOf<String?>(null) }
LaunchedEffect(entries, displayWarmups, currentUnit, timeFilter, timeMin, timeMax, labelFilterKey, filterInput, filterHardware) {
var maxX = 0f
var minY = 0f
var maxY = 0f
val timeRange = dualLerp(timeMin, timeMax, timeFilter.start, timeFilter.endInclusive)
enabledInputs.clear()
enabledHardware.clear()
renderableEntries = entries.mapNotNull { entry ->
val use = if(displayWarmups) entry.warmups else entry.measurements
maxX = maxOf(maxX, use.size.toFloat())
if(entry.label !in labelFilter) return@mapNotNull null
if(!(entry.timeStamp inRange timeRange)) return@mapNotNull null
if(filterInput != null && entry.input != filterInput) return@mapNotNull null
if(filterHardware != null && entry.hardware != filterHardware) return@mapNotNull null
maxX = maxOf(maxX, use.size.toFloat())
enabledInputs.add(entry.input)
enabledHardware.add(entry.hardware)
use.mapIndexed { idx, it ->
val converted = entry.nativeUnit.convertTo(currentUnit, it)
@@ -482,9 +545,11 @@ fun ProjectPlotArea(vm: ProjectVM){
Spacer(Modifier.width(20.dp))
Box(Modifier.weight(0.33f).fillMaxHeight()) {
Column {
Row(Modifier.weight(0.33f).fillMaxHeight()) {
Column(Modifier.weight(0.5f)) {
Text("Labels", style = MaterialTheme.typography.headlineSmall)
Text("Click a label to toggle its visibility.", fontStyle = FontStyle.Italic,
color = LocalContentColor.current.copy(alpha = 0.5f), style = MaterialTheme.typography.bodySmall)
Spacer(Modifier.height(10.dp))
if(labels.isEmpty()) {
Box(Modifier.fillMaxWidth().fillMaxHeight(0.25f)) {
@@ -494,9 +559,18 @@ fun ProjectPlotArea(vm: ProjectVM){
else {
LazyColumn(Modifier.padding(start = 5.dp)) {
items(labels.toList().sortedBy { it.second.name }) { (_, lbl) ->
Box(Modifier.fillMaxWidth().clickable { labelFilter.toggle(lbl) }) {
Box(Modifier.fillMaxWidth().clickable {
if(labelFilter.size == labels.size) {
labelFilter.clear()
labelFilter.add(lbl)
}
else {
labelFilter.toggle(lbl)
if(labelFilter.isEmpty()) labelFilter.addAll(labels.values)
}
}) {
Box {
QuickLabel(lbl)
QuickLabel(lbl, lbl !in labelFilter)
if(lbl !in labelFilter) {
Box(Modifier.matchParentSize()) {
@@ -513,6 +587,46 @@ fun ProjectPlotArea(vm: ProjectVM){
}
}
}
Spacer(Modifier.width(15.dp))
LazyColumn(Modifier.weight(0.5f)) {
item { Text("Inputs", style = MaterialTheme.typography.headlineSmall) }
item {
Text("Click an input to set or clear the filter.", fontStyle = FontStyle.Italic,
color = LocalContentColor.current.copy(0.5f), style = MaterialTheme.typography.bodySmall)
}
item { Spacer(Modifier.height(10.dp)) }
items((allInputs intersect enabledInputs.toSet()).toList()) {
Row(Modifier.fillMaxWidth().clickable { filterInput = if(filterInput == null) it else null }) {
Text(it, Modifier.padding(start = 5.dp))
}
}
items((allInputs - enabledInputs.toSet()).toList()) {
Text(it, Modifier.padding(start = 5.dp, top = 2.dp, bottom = 2.dp), color = LocalContentColor.current.copy(0.66f))
}
item { Spacer(Modifier.height(15.dp)) }
item { Text("Hardware", style = MaterialTheme.typography.headlineSmall) }
item {
Text("Click an item to set or clear the filter.", fontStyle = FontStyle.Italic,
color = LocalContentColor.current.copy(0.5f), style = MaterialTheme.typography.bodySmall)
}
item { Spacer(Modifier.height(10.dp)) }
items((allHardware intersect enabledHardware.toSet()).toList()) {
Row(Modifier.fillMaxWidth().clickable { filterHardware = if(filterHardware == null) it else null }) {
Text(it, Modifier.padding(start = 5.dp))
}
}
items((allHardware - enabledHardware.toSet()).toList()) {
Text(it, Modifier.padding(start = 5.dp, top = 2.dp, bottom = 2.dp), color = LocalContentColor.current.copy(0.66f))
}
}
}
}
}