Added input, hardware fields to data
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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()
|
||||
|
||||
+38
-12
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+129
-15
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user