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))
}
}
}
}
}
@@ -27,7 +27,7 @@ data class LabelRequest(val name: String, val color: String, val projectId: Uuid
@Serializable @ToPartialize
data class EntryRequest(val label: Uuid, val timestamp: Instant, val projectId: Uuid, val warmups: List<Float>,
val measurements: List<Float>, val unit: TimeUnit)
val measurements: List<Float>, val unit: TimeUnit, val input: String, val hardware: String)
@Serializable
data class LogoutRequest(val refresh: Uuid)
@@ -54,7 +54,7 @@ data class LabelResponse(val id: Uuid, val name: String, val color: String)
@Serializable
data class EntryResponse(val id: Uuid, val labelId: Uuid, val timestamp: Instant, val warmups: List<Float>,
val measurements: List<Float>, val unit: TimeUnit)
val measurements: List<Float>, val unit: TimeUnit, val input: String, val hardware: String)
@Serializable
data class HandshakeResponse(val version: String = ProtocolVersion.VERSION) {
@@ -65,6 +65,8 @@ class Entry(id: EntityID<Uuid>) : Entity<Uuid>(id) {
var warmups by Entries.warmups
var measurements by Entries.measurements
var unit by Entries.unit
var input by Entries.input
var hardware by Entries.hardware
var project by Project referencedOn Entries.projectId
var label by Label referencedOn Entries.labelId
@@ -47,4 +47,6 @@ object Entries : UuidTable() {
val warmups = list<Float>("warmups")
val measurements = list<Float>("measurements")
val unit = enumeration<TimeUnit>("unit")
val input = varchar("input", 255).default("")
val hardware = varchar("hardware", 255).default("")
}
@@ -56,7 +56,7 @@ object ProjectHandler {
private fun Project.toResponse(user: User) = ProjectResponse(
id.value, name, NamedID(owner.username, owner.id.value), isPublic, isEditableBy(user),
labels.orderBy(Labels.label to SortOrder.ASC).map { LabelResponse(it.id.value, it.label, it.color) },
entries.orderBy(Entries.timestamp to SortOrder.ASC).map { EntryResponse(it.id.value, it.label.id.value, it.timestamp, it.warmups, it.measurements, it.unit) })
entries.orderBy(Entries.timestamp to SortOrder.ASC).map { EntryResponse(it.id.value, it.label.id.value, it.timestamp, it.warmups, it.measurements, it.unit, it.input, it.hardware) })
fun home(user: User, req: EmptyRequest) = transaction {
val own = user.projects.orderBy(Projects.name to SortOrder.ASC).map {
@@ -188,9 +188,11 @@ object ProjectHandler {
timestamp = req.timestamp
warmups = req.warmups
unit = req.unit
input = req.input
hardware = req.hardware
}
val response = EntryResponse(entry.id.value, entry.label.id.value, entry.timestamp, entry.warmups, entry.measurements, entry.unit)
val response = EntryResponse(entry.id.value, entry.label.id.value, entry.timestamp, entry.warmups, entry.measurements, entry.unit, entry.input, entry.hardware)
ServerScope.launch {
Buses.projectBus(proj.id.value).sendAll(ProjectEvent.NewEntry(response))
}