Output:
Chunk ID: b550b2
Wall time: 0.3455 seconds
Process exited with code 0
Original token count: 2124
Output:
                modifier = Modifier.padding(padding),
            )
        } else {
            LazyColumn(
                state = listState,
                modifier =
                    Modifier
                        .fillMaxSize()
                        .padding(padding),
                contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
            ) {
                if (state.isLoading) {
                    itemsIndexed(List(4) { it }) { index, _ ->
                        VehicleCardSkeleton(
                            shape = cardShape(isFirst = index == 0, isLast = index == 3),
                            showDivider = false,
                        )
                        if (index < 3) {
                            Spacer(Modifier.height(2.dp))
                        }
                    }
                } else {
                    if (state.isReadOnly) {
                        item("paywall_status_card") {
                            Spacer(Modifier.height(8.dp))
                            PaywallStatusCard(
                                isPendingPurchase = state.isPremiumPending,
                                onUpgradeClick = { viewModel.onEvent(VehicleListEvent.BlockedWriteAttempted) },
                            )
                            Spacer(Modifier.height(8.dp))
                        }
                    }
                    val total = state.vehicles.size
                    itemsIndexed(state.vehicles, key = { _, item -> item.id }) { index, item ->
                        val shape = cardShape(isFirst = index == 0, isLast = index == total - 1)
                        val dismissState = rememberSwipeToDismissBoxState()
                        LaunchedEffect(dismissState.currentValue) {
                            if (dismissState.currentValue == SwipeToDismissBoxValue.EndToStart) {
                                viewModel.onEvent(VehicleListEvent.RequestDeleteVehicle(item.id))
                            }
                        }
                        LaunchedEffect(state.confirmDeleteVehicleId) {
                            if (state.confirmDeleteVehicleId != item.id &&
                                dismissState.currentValue == SwipeToDismissBoxValue.EndToStart
                            ) {
                                dismissState.reset()
                            }
                        }
                        SwipeToDismissBox(
                            state = dismissState,
                            enableDismissFromStartToEnd = false,
                            enableDismissFromEndToStart = !state.isReadOnly,
                            backgroundContent = {
                                Box(
                                    modifier =
                                        Modifier
                                            .fillMaxSize()
                                            .background(MaterialTheme.colorScheme.errorContainer, shape)
                                            .padding(horizontal = 20.dp),
                                    contentAlignment = Alignment.CenterEnd,
                                ) {
                                    Icon(
                                        imageVector = Icons.Default.Delete,
                                        contentDescription = stringResource(R.string.vehicle_delete),
                                        tint = MaterialTheme.colorScheme.onErrorContainer,
                                    )
                                }
                            },
                        ) {
                            VehicleCard(
                                item = item,
                                onClick = { viewModel.onEvent(VehicleListEvent.SelectVehicle(item.id)) },
                                shape = shape,
                                showDivider = false,
                                modifier = Modifier.testTag(TestTags.vehicleItem(item.id)),
                            )
                        }
                        if (index < total - 1) {
                            Spacer(Modifier.height(2.dp))
                        }
                    }
                }
            }
        }
    }
}

@Composable
private fun VehicleCardSkeleton(
    shape: Shape,
    showDivider: Boolean,
) {
    val brush = shimmerBrush()
    Card(
        modifier = Modifier.fillMaxWidth(),
        shape = shape,
        colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
    ) {
        Column {
            Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {
                Box(
                    modifier =
                        Modifier
                            .width(160.dp)
                            .height(18.dp)
                            .background(brush, RoundedCornerShape(4.dp)),
                )
                Spacer(Modifier.height(6.dp))
                Box(
                    modifier =
                        Modifier
                            .width(120.dp)
                            .height(14.dp)
                            .background(brush, RoundedCornerShape(4.dp)),
                )
                Spacer(Modifier.height(6.dp))
                Box(
                    modifier =
                        Modifier
                            .width(100.dp)
                            .height(12.dp)
                            .background(brush, RoundedCornerShape(4.dp)),
                )
            }
            if (showDivider) {
                HorizontalDivider(color = MaterialTheme.colorScheme.surfaceContainerHigh)
            }
        }
    }
}

@Composable
private fun VehicleCard(
    item: VehiclePreview,
    onClick: () -> Unit,
    shape: Shape,
    showDivider: Boolean,
    modifier: Modifier = Modifier,
) {
    Card(
        onClick = onClick,
        modifier = modifier.fillMaxWidth(),
        shape = shape,
        colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
    ) {
        Column {
            Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {
                Text(
                    text = item.nickname,
                    style = MaterialTheme.typography.titleLarge,
                    color = MaterialTheme.colorScheme.primary,
                )
                val subtitle =
                    listOfNotNull(
                        item.make.takeIf { it.isNotBlank() },
                        item.model.takeIf { it.isNotBlank() },
                        item.year.takeIf { it.isNotBlank() },
                    ).joinToString(" • ")
                if (subtitle.isNotBlank()) {
                    Spacer(Modifier.height(2.dp))
                    Text(
                        text = subtitle,
                        style = MaterialTheme.typography.bodyMedium,
                        color = MaterialTheme.colorScheme.onSurfaceVariant,
                    )
                }
                Spacer(Modifier.height(6.dp))
                Row(verticalAlignment = Alignment.CenterVertically) {
                    val dotColor =
                        if (item.lastServicedDate != null) {
                            MaterialTheme.colorScheme.tertiary
                        } else {
                            MaterialTheme.colorScheme.outlineVariant
                        }
                    Box(
                        modifier =
                            Modifier
                                .size(8.dp)
                                .background(dotColor, CircleShape),
                    )
                    Spacer(Modifier.width(4.dp))
                    Text(
                        text =
                            if (item.lastServicedDate != null) {
                                stringResource(R.string.vehicle_last_service, item.lastServicedDate)
                            } else {
                                stringResource(R.string.vehicle_no_service)
                            },
                        style = MaterialTheme.typography.labelMedium,
                        color = MaterialTheme.colorScheme.onSurfaceVariant,
                    )
                }
            }
            if (showDivider) {
                HorizontalDivider(color = MaterialTheme.colorScheme.surfaceContainerHigh)
            }
        }
    }
}


Output:
Chunk ID: 6a26a0
Wall time: 0.2890 seconds
Process exited with code 0
Original token count: 2526
Output:
                val groupedRecords =
                    remember(state.records) {
                        state.records
                            .filter { !it.isError }
                            .groupBy { DateFormatter.toYear(it.record.date) }
                            .entries
                            .sortedByDescending { it.key }
                    }
                val errorRecords =
                    remember(state.records) {
                        state.records.filter { it.isError }
                    }
                val odometerUnit = state.vehicle?.odometerUnit?.label ?: "km"
                val outlineVariant = MaterialTheme.colorScheme.outlineVariant

                LazyColumn(
                    modifier =
                        Modifier
                            .fillMaxSize()
                            .weight(1f),
                ) {
                    groupedRecords.forEachIndexed { groupIndex, (year, items) ->
                        stickyHeader(key = "header_$year") {
                            YearHeader(year)
                        }
                        items.forEachIndexed { itemIndex, item ->
                            val isFirst = itemIndex == 0
                            val isLast = itemIndex == items.lastIndex
                            val isAbsoluteFirst = groupIndex == 0 && itemIndex == 0
                            item(key = item.record.id) {
                                TimelineListItem(
                                    item = item,
                                    odometerUnit = odometerUnit,
                                    isFirst = isFirst,
                                    isLast = isLast,
                                    isAbsoluteFirst = isAbsoluteFirst,
                                    outlineVariant = outlineVariant,
                                    onClick = { viewModel.onEvent(VehicleDetailEvent.SelectRecord(item.record.id)) },
                                    modifier = Modifier.testTag(TestTags.recordItem(item.record.id)),
                                )
                            }
                        }
                    }
                    errorRecords.forEach { item ->
                        item(key = item.record.id) {
                            ErrorListItem(errorMessage = item.rawError)
                        }
                    }
                }
            }
        }
    }
}

@Composable
private fun YearHeader(year: Int) {
    Text(
        text = year.toString(),
        style = MaterialTheme.typography.titleSmall,
        color = MaterialTheme.colorScheme.onSurfaceVariant,
        modifier =
            Modifier
                .fillMaxWidth()
                .background(MaterialTheme.colorScheme.surfaceContainerLow)
                .padding(horizontal = 16.dp, vertical = 8.dp),
    )
}

@Composable
private fun TimelineListItem(
    item: RecordWithCategory,
    odometerUnit: String,
    isFirst: Boolean,
    isLast: Boolean,
    isAbsoluteFirst: Boolean,
    outlineVariant: Color,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    val onlySingleItem = isFirst && isLast

    Row(
        modifier =
            modifier
                .fillMaxWidth()
                .height(IntrinsicSize.Min)
                .clickable(onClick = onClick),
        verticalAlignment = Alignment.Top,
    ) {
        // Left column: timeline line + category icon
        Box(
            modifier =
                Modifier
                    .width(56.dp)
                    .fillMaxHeight()
                    .drawBehind {
                        if (!onlySingleItem) {
                            val x = size.width / 2f
                            val strokeWidth = 2.dp.toPx()
                            if (isAbsoluteFirst) {
                                // Gradient fade from transparent (top) to solid (center)
                                drawLine(
                                    brush =
                                        Brush.verticalGradient(
                                            colors = listOf(Color.Transparent, outlineVariant),
                                            startY = 0f,
                                            endY = size.height / 2f,
                                        ),
                                    start = Offset(x, 0f),
                                    end = Offset(x, size.height / 2f),
                                    strokeWidth = strokeWidth,
                                )
                                // Solid line from center down
                                val endY = if (isLast) size.height / 2f else size.height
                                drawLine(
                                    color = outlineVariant,
                                    start = Offset(x, size.height / 2f),
                                    end = Offset(x, endY),
                                    strokeWidth = strokeWidth,
                                )
                            } else {
                                val startY = if (isFirst) size.height / 2f else 0f
                                val endY = if (isLast) size.height / 2f else size.height
                                drawLine(
                                    color = outlineVariant,
                                    start = Offset(x, startY),
                                    end = Offset(x, endY),
                                    strokeWidth = strokeWidth,
                                )
                            }
                        }
                    },
            contentAlignment = Alignment.TopCenter,
        ) {
            Box(
                modifier =
                    Modifier
                        .padding(top = 12.dp)
                        .size(32.dp)
                        .background(MaterialTheme.colorScheme.surfaceContainerLow, CircleShape),
                contentAlignment = Alignment.Center,
            ) {
                Icon(
                    imageVector = categoryIcon(item.record.category),
                    contentDescription = null,
                    modifier = Modifier.size(18.dp),
                    tint = MaterialTheme.colorScheme.onSurfaceVariant,
                )
            }
        }

        // Middle column: date, category, odometer + performer
        Column(
            modifier =
                Modifier
                    .weight(1f)
                    .padding(start = 8.dp, top = 12.dp, bottom = 12.dp, end = 8.dp),
        ) {
            Text(
                text = DateFormatter.toShortMonthDay(item.record.date),
                style = MaterialTheme.typography.bodyLarge,
                fontWeight = FontWeight.Bold,
            )
            val workLabel =
                remember(item.record.workItems, item.categoryLabel) {
                    item.record.workItems
                        .takeIf { it.isNotEmpty() }
                        ?.joinToString { it.name }
                        ?: item.categoryLabel
                }
            Text(
                text = workLabel,
                style = MaterialTheme.typography.bodyMedium,
            )
            val performer = getDisplayPerformer(item.record.performer)
            val subtext =
                buildString {
                    append("${item.record.odometer} $odometerUnit")
                    if (!performer.isNullOrBlank()) append(" • $performer")
                }
            Text(
                text = subtext,
                style = MaterialTheme.typography.bodySmall,
                color = MaterialTheme.colorScheme.onSurfaceVariant,
            )
        }

        // Right column: cost
        Box(
            modifier = Modifier.padding(end = 16.dp, top = 12.dp, bottom = 12.dp),
            contentAlignment = Alignment.TopEnd,
        ) {
            val costText =
                item.record.cost?.let { cost ->
                    buildString {
                        if (!item.record.currency.isNullOrBlank()) append("${item.record.currency} ")
                        append(cost)
                    }
                }
            Text(
                text = costText ?: "—",
                style = MaterialTheme.typography.titleMedium,
                color = if (costText == null) MaterialTheme.colorScheme.outline else LocalContentColor.current,
            )
        }
    }
}

@Composable
private fun TimelineItemSkeleton() {
    val brush = shimmerBrush()
    Row(
        modifier =
            Modifier
                .fillMaxWidth()
                .height(IntrinsicSize.Min),
        verticalAlignment = Alignment.Top,
    ) {
        // Left column: ghost circle
        Box(
            modifier =
                Modifier
                    .width(56.dp)
                    .fillMaxHeight(),
            contentAlignment = Alignment.TopCenter,
        ) {
            Box(
                modifier =
                    Modifier
                        .padding(top = 12.dp)
                        .size(32.dp)
                        .background(brush, CircleShape),
            )
        }
        // Middle column: ghost lines
        Column(
            modifier =
                Modifier
                    .weight(1f)
                    .padding(start = 8.dp, top = 14.dp, bottom = 14.dp, end = 8.dp),
        ) {
            Box(
                modifier =
                    Modifier
                        .width(80.dp)
                        .height(16.dp)
                        .background(brush, RoundedCornerShape(4.dp)),
            )
            Spacer(Modifier.height(6.dp))
            Box(
                modifier =
                    Modifier
                        .width(120.dp)
                        .height(14.dp)
                        .background(brush, RoundedCornerShape(4.dp)),
            )
            Spacer(Modifier.height(6.dp))
            Box(
                modifier =
                    Modifier
                        .width(90.dp)


Output:
Chunk ID: d4f8ee
Wall time: 0.3204 seconds
Process exited with code 0
Original token count: 275
Output:
package com.yorvana.data.model

import kotlinx.serialization.Serializable

enum class PredefinedCategory(
    val id: String,
    val labelKey: String,
) {
    OIL_CHANGE("oil-change", "category_oil_change"),
    TIRE("tire-change-rotation", "category_tire"),
    BRAKE("brake-service", "category_brake"),
    BATTERY("battery", "category_battery"),
    INSPECTION("inspection", "category_inspection"),
    FLUID("fluid-top-up", "category_fluid"),
    FILTER("filter-replacement", "category_filter"),
    OTHER("other", "category_other"),
}

@Serializable
data class CustomCategory(
    val id: String,
    val label: String,
    val createdAt: String,
)

@Serializable
data class CategoriesFile(
    val schemaVersion: Int = 1,
    val custom: List<CustomCategory> = emptyList(),
)

sealed class Category {
    abstract val id: String
    abstract val label: String

    data class Predefined(
        override val id: String,
        override val label: String,
    ) : Category()

    data class Custom(
        override val id: String,
        override val label: String,
    ) : Category()
}


Output:
Chunk ID: b6dfe1
Wall time: 0.3581 seconds
Process exited with code 0
Original token count: 2610
Output:
app/src/test/java/com/yorvana/data/storage/VaultStorageImplIT.kt:174:    fun readCategories_should_return_parsed_CategoriesFile_on_success() =
app/src/test/java/com/yorvana/data/storage/VaultStorageImplIT.kt:185:    fun readCategories_should_return_default_CategoriesFile_when_file_is_missing() =
app/src/test/java/com/yorvana/data/storage/VaultStorageImplIT.kt:205:                com.yorvana.data.model.CategoriesFile(
app/src/test/java/com/yorvana/data/storage/VaultStorageImplIT.kt:257:                    category = "oil",
app/src/test/java/com/yorvana/data/storage/VaultStorageImplIT.kt:374:                    category = "oil",
app/src/test/java/com/yorvana/data/storage/VaultStorageImplIT.kt:384:                    category = "oil",
app/src/test/java/com/yorvana/data/storage/VaultStorageImplIT.kt:413:                    category = "oil",
app/src/test/java/com/yorvana/ui/records/RecordDetailViewModelTest.kt:70:                category = "oil",
app/src/test/java/com/yorvana/ui/records/RecordDetailViewModelTest.kt:86:            categoryRepo.getCategoryLabel("oil")
app/src/test/java/com/yorvana/ui/records/RecordDetailViewModelTest.kt:146:                    category = "oil",
app/src/test/java/com/yorvana/ui/records/RecordDetailViewModelIT.kt:72:                category = "oil",
app/src/test/java/com/yorvana/ui/records/RecordDetailViewModelIT.kt:82:            categoryRepo.getCategoryLabel("oil")
app/src/test/java/com/yorvana/data/storage/VaultStorageImplTest.kt:7:import com.yorvana.data.model.CategoriesFile
app/src/test/java/com/yorvana/data/storage/VaultStorageImplTest.kt:98:    fun readCategories_should_return_parsed_CategoriesFile_on_success() =
app/src/test/java/com/yorvana/data/storage/VaultStorageImplTest.kt:112:                CategoriesFile(
app/src/test/java/com/yorvana/data/storage/VaultStorageImplTest.kt:133:                    category = "oil",
app/src/test/java/com/yorvana/ui/records/AddEditRecordViewModelTest.kt:108:                    category = "oil",
app/src/test/java/com/yorvana/ui/records/AddEditRecordViewModelTest.kt:325:            val oilChangeDef = PREDEFINED_WORK_ITEMS.first { it.id == "oil-change" }
app/src/test/java/com/yorvana/ui/records/AddEditRecordViewModelTest.kt:326:            viewModel.onEvent(AddEditRecordEvent.TogglePredefinedItem(oilChangeDef.id))
app/src/test/java/com/yorvana/ui/records/AddEditRecordViewModelTest.kt:327:            viewModel.onEvent(AddEditRecordEvent.UpdatePredefinedField(oilChangeDef.id, "Brand", "Castrol"))
app/src/test/java/com/yorvana/ui/records/AddEditRecordViewModelTest.kt:345:                            record.workItems.any { it.id == "oil-change" && it.fields["Brand"] == "Castrol" } &&
app/src/test/java/com/yorvana/ui/records/AddEditRecordViewModelTest.kt:347:                            record.category == "oil-change"
app/src/test/java/com/yorvana/ui/records/AddEditRecordViewModelTest.kt:361:                    category = "oil-change",
app/src/test/java/com/yorvana/ui/records/AddEditRecordViewModelTest.kt:377:            ).isEqualTo("oil-change")
app/src/test/java/com/yorvana/ui/records/AddEditRecordViewModelTest.kt:575:                    category = "oil-change",
app/src/test/java/com/yorvana/ui/records/AddEditRecordViewModelTest.kt:581:                            WorkItem(id = "oil-change", name = "Oil Change", fields = mapOf("Brand" to "Castrol")),
app/src/test/java/com/yorvana/ui/records/AddEditRecordViewModelTest.kt:593:                    .any { it is ActiveWorkItemState.Predefined && it.id == "oil-change" },
app/src/test/java/com/yorvana/data/repository/RecordRepositoryImplTest.kt:64:                    category = "oil",
app/src/test/java/com/yorvana/data/repository/RecordRepositoryImplTest.kt:71:            coEvery { categoryRepository.getCategoryLabel("oil") } returns "Oil Change"
app/src/test/java/com/yorvana/data/repository/RecordRepositoryImplTest.kt:90:                    category = "oil",
app/src/test/java/com/yorvana/data/repository/RecordRepositoryImplTest.kt:144:                    category = "oil",
app/src/test/java/com/yorvana/data/repository/RecordRepositoryImplTest.kt:262:                    category = "oil",
app/src/test/java/com/yorvana/data/repository/RecordRepositoryImplTest.kt:272:                    category = "oil",
app/src/test/java/com/yorvana/data/repository/DataIntegrationIT.kt:66:                    category = "oil",
app/src/test/java/com/yorvana/ui/settings/SettingsViewModelIT.kt:13:import com.yorvana.data.model.CategoriesFile
app/src/test/java/com/yorvana/ui/settings/SettingsViewModelIT.kt:403:        override suspend fun readCategories(): Result<CategoriesFile> = Result.success(CategoriesFile())
app/src/test/java/com/yorvana/ui/settings/SettingsViewModelIT.kt:405:        override suspend fun writeCategories(categories: CategoriesFile) = Result.success(Unit)
app/src/test/java/com/yorvana/data/repository/CategoryRepositoryImplTest.kt:6:import com.yorvana.data.model.CategoriesFile
app/src/test/java/com/yorvana/data/repository/CategoryRepositoryImplTest.kt:61:            val file = CategoriesFile(custom = listOf(customCat))
app/src/test/java/com/yorvana/data/repository/CategoryRepositoryImplTest.kt:87:            coEvery { vaultStorage.readCategories() } returns Result.success(CategoriesFile())
app/src/test/java/com/yorvana/data/repository/CategoryRepositoryImplTest.kt:89:                val written = arg<CategoriesFile>(0)
app/src/test/java/com/yorvana/data/repository/CategoryRepositoryImplTest.kt:124:            val file = CategoriesFile(custom = listOf(customCat1, customCat2))
app/src/test/java/com/yorvana/data/repository/CategoryRepositoryImplTest.kt:131:                val written = arg<CategoriesFile>(0)
app/src/test/java/com/yorvana/data/repository/CategoryRepositoryImplTest.kt:162:            val file = CategoriesFile(custom = listOf(customCat))
app/src/main/java/com/yorvana/ui/records/ImageViewerScreen.kt:34:import coil.compose.AsyncImage
app/src/test/java/com/yorvana/ui/records/VehicleDetailViewModelTest.kt:97:                    category = "oil",
app/src/main/java/com/yorvana/ui/records/VehicleDetailScreen.kt:582:        "oil-change", "fluid-top-up" -> Icons.Default.WaterDrop
app/src/test/java/com/yorvana/ui/records/VehicleDetailScreenshotTest.kt:43:                                        category = "oil",
app/src/test/java/com/yorvana/ui/records/VehicleDetailScreenTest.kt:83:                category = "oil-change",
app/src/test/java/com/yorvana/ui/records/VehicleDetailScreenTest.kt:167:                category = "oil-change",
app/src/main/java/com/yorvana/ui/records/AddEditRecordViewModel.kt:176:        WorkItemDef("oil-change", "Oil Change", listOf("Brand", "Viscosity (e.g. 5W-30)")),
app/src/test/java/com/yorvana/ui/records/AddEditRecordScreenshotTest.kt:28:                    notes = if (isEditMode) "Used synthetic oil" else "",
app/src/test/java/com/yorvana/ui/records/VehicleDetailScreenIT.kt:49:                category = "oil-change",
app/src/test/java/com/yorvana/ui/records/VehicleDetailScreenIT.kt:130:                category = "oil-change",
app/src/test/java/com/yorvana/ui/records/VehicleDetailScreenIT.kt:169:                category = "oil-change",
app/src/main/java/com/yorvana/ui/records/RecordDetailScreen.kt:62:import coil.compose.AsyncImage
app/src/main/java/com/yorvana/ui/records/RecordDetailScreen.kt:426:        "oil-change", "fluid-top-up" -> Icons.Default.WaterDrop
app/src/main/java/com/yorvana/ui/records/RecordDetailScreen.kt:461:            "Used synthetic oil (5W-30) and OEM Honda filter. " +
app/src/test/java/com/yorvana/ui/records/RecordDetailScreenshotTest.kt:27:                            category = "oil",
app/src/test/java/com/yorvana/ui/records/RecordDetailScreenshotTest.kt:31:                            notes = "Used synthetic oil",
app/src/test/java/com/yorvana/ui/records/RecordDetailScreenIT.kt:34:                category = "oil-change",
app/src/test/java/com/yorvana/ui/records/RecordDetailScreenIT.kt:74:                category = "oil-change",
app/src/test/java/com/yorvana/ui/settings/CategoriesScreenshotTest.kt:25:                    predefined = listOf(Category.Predefined("oil", "Oil Change")),
app/src/main/java/com/yorvana/data/storage/VaultStorageImpl.kt:7:import com.yorvana.data.model.CategoriesFile
app/src/main/java/com/yorvana/data/storage/VaultStorageImpl.kt:441:    override suspend fun readCategories(): Result<CategoriesFile> =
app/src/main/java/com/yorvana/data/storage/VaultStorageImpl.kt:444:                val file = getVaultRoot()?.findFile(FILE_CATEGORIES) ?: return@runCatching CategoriesFile()
app/src/main/java/com/yorvana/data/storage/VaultStorageImpl.kt:448:                json.decodeFromJsonElement<CategoriesFile>(migrated)
app/src/main/java/com/yorvana/data/storage/VaultStorageImpl.kt:452:    override suspend fun writeCategories(categories: CategoriesFile): Result<Unit> =
app/src/main/java/com/yorvana/data/storage/VaultStorageImpl.kt:457:                writeText(file, json.encodeToString(CategoriesFile.serializer(), categories))
app/src/main/java/com/yorvana/data/storage/VaultStorage.kt:6:import com.yorvana.data.model.CategoriesFile
app/src/main/java/com/yorvana/data/storage/VaultStorage.kt:60:    suspend fun readCategories(): Result<CategoriesFile>
app/src/main/java/com/yorvana/data/storage/VaultStorage.kt:62:    suspend fun writeCategories(categories: CategoriesFile): Result<Unit>
app/src/main/java/com/yorvana/ui/components/AttachmentThumbnail.kt:19:import coil.compose.AsyncImage
app/src/main/java/com/yorvana/data/repository/CategoryRepositoryImpl.kt:4:import com.yorvana.data.model.CategoriesFile
app/src/main/java/com/yorvana/data/repository/CategoryRepositoryImpl.kt:33:        val current = storage.readCategories().getOrElse { CategoriesFile() }
app/src/main/java/com/yorvana/data/repository/CategoryRepositoryImpl.kt:49:        val current = storage.readCategories().getOrElse { CategoriesFile() }
app/src/main/java/com/yorvana/data/repository/CategoryRepositoryImpl.kt:59:        val custom = storage.readCategories().getOrElse { CategoriesFile() }
app/src/main/java/com/yorvana/data/repository/CategoryRepositoryImpl.kt:72:        val customFile = storage.readCategories().getOrElse { CategoriesFile() }
app/src/main/java/com/yorvana/data/model/Category.kt:9:    OIL_CHANGE("oil-change", "category_oil_change"),
app/src/main/java/com/yorvana/data/model/Category.kt:27:data class CategoriesFile(


I’m editing the Gradle wiring and app surfaces first, then I’ll add the new module files. The benchmark seed hook will live only in the benchmark source set so normal debug/release/staging APKs don’t expose it.


Input:
*** Begin Patch
*** Update File: /home/Messier82/projects/service-book/settings.gradle.kts
@@
 rootProject.name = "Yorvana"
 include(":app")
+include(":macrobenchmark")
*** Update File: /home/Messier82/projects/service-book/gradle/libs.versions.toml
@@
 billing = "9.0.0"
+benchmark = "1.4.1"
@@
 google-billing = { module = "com.android.billingclient:billing-ktx", version.ref = "billing" }
+androidx-benchmark-macro = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "benchmark" }
@@
 android-application = { id = "com.android.application", version.ref = "agp" }
+android-test = { id = "com.android.test", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
 kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
*** Update File: /home/Messier82/projects/service-book/build.gradle.kts
@@
 plugins {
     alias(libs.plugins.android.application) apply false
+    alias(libs.plugins.android.test) apply false
+    alias(libs.plugins.kotlin.android) apply false
     alias(libs.plugins.kotlin.serialization) apply false
*** Update File: /home/Messier82/projects/service-book/app/build.gradle.kts
@@
 val quarantinedAnnotation = "com.yorvana.testsupport.tiers.Quarantined"
 val regressionDeviceTaskName = "pixel2api33DebugAndroidTest"
+val macrobenchmarkTaskPath = ":macrobenchmark:pixel2api33BenchmarkAndroidTest"
@@
         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+        buildConfigField("Boolean", "BENCHMARK", "false")
         vectorDrawables {
             useSupportLibrary = true
         }
@@
         create("staging") {
             initWith(getByName("release"))
             matchingFallbacks += listOf("release")
             isMinifyEnabled = false
@@
             buildConfigField("String", "SENTRY_DSN", "\"$dsnRelease\"")
         }
+        create("benchmark") {
+            initWith(getByName("release"))
+            matchingFallbacks += listOf("release")
+            signingConfig = signingConfigs.getByName("debug")
+            isDebuggable = false
+            isMinifyEnabled = false
+            val dsnRelease =
+                (localProperties.getProperty("sentry.dsn.release") ?: "")
+                    .replace("\\", "\\\\")
+                    .replace("\"", "\\\"")
+            buildConfigField("String", "SENTRY_DSN", "\"$dsnRelease\"")
+            buildConfigField("Boolean", "BENCHMARK", "true")
+        }
@@
 tasks.register("regressionFullCheck") {
     group = "Verification"
-    description = "Runs Phase 1 full regression checks; sandbox and macrobenchmark siblings are deferred."
-    dependsOn(regressionFullCoreCheck, regressionFullExtendedCheck)
+    description = "Runs full regression checks, including the macrobenchmark sibling task."
+    dependsOn(regressionFullCoreCheck, regressionFullExtendedCheck, macrobenchmarkTaskPath)
 }
*** Update File: /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/ui/TestTags.kt
@@
     // Vehicle list / detail
+    const val VEHICLE_LIST = "VehicleList"
+    const val RECORD_LIST = "RecordList"
     const val ADD_VEHICLE_FAB = "AddVehicleFab"
     const val ADD_RECORD_FAB = "AddRecordFab"
*** Update File: /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/MainActivity.kt
@@
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTagsAsResourceId
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
@@
         key(dest, preferences.vaultUriString) {
-            AppNavGraph(startDestination = dest)
+            AppNavGraph(
+                startDestination = dest,
+                modifier = Modifier.semantics { testTagsAsResourceId = true },
+            )
         }
     }
 }
*** Update File: /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/ui/navigation/AppNavGraph.kt
@@
 @OptIn(ExperimentalMaterial3Api::class)
 @Suppress("SpreadOperator")
 @Composable
-fun AppNavGraph(startDestination: NavKey) {
+fun AppNavGraph(
+    startDestination: NavKey,
+    modifier: Modifier = Modifier,
+) {
@@
         NavDisplay<NavKey>(
             backStack = backStack,
-            modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background),
+            modifier = modifier.fillMaxSize().background(MaterialTheme.colorScheme.background),
*** Update File: /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/ui/vehicles/VehicleListScreen.kt
@@
                 modifier =
                     Modifier
                         .fillMaxSize()
+                        .testTag(TestTags.VEHICLE_LIST)
                         .padding(padding),
*** Update File: /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/ui/records/VehicleDetailScreen.kt
@@
                 LazyColumn(
                     modifier =
                         Modifier
                             .fillMaxSize()
+                            .testTag(TestTags.RECORD_LIST)
                             .weight(1f),
                 ) {
*** Update File: /home/Messier82/projects/service-book/.github/workflows/regression.yml
@@
       - name: Upload Managed Device Reports
-        if: failure()
+        if: always()
         uses: actions/upload-artifact@v7
         with:
           name: gmd-regression-test-reports
           path: |
             app/build/reports/androidTests/managedDevice/
             app/build/outputs/androidTest-results/managedDevice/
             app/build/outputs/managed_device_android_test_additional_output/
+            macrobenchmark/build/reports/androidTests/managedDevice/
+            macrobenchmark/build/outputs/androidTest-results/managedDevice/
+            macrobenchmark/build/outputs/managed_device_android_test_additional_output/
           if-no-files-found: warn
           retention-days: 7
*** End Patch


Success. Updated the following files:
M /home/Messier82/projects/service-book/settings.gradle.kts
M /home/Messier82/projects/service-book/gradle/libs.versions.toml
M /home/Messier82/projects/service-book/build.gradle.kts
M /home/Messier82/projects/service-book/app/build.gradle.kts
M /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/ui/TestTags.kt
M /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/MainActivity.kt
M /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/ui/navigation/AppNavGraph.kt
M /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/ui/vehicles/VehicleListScreen.kt
M /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/ui/records/VehicleDetailScreen.kt
M /home/Messier82/projects/service-book/.github/workflows/regression.yml

@@ -24 +24,2 @@
 include(":app")
+include(":macrobenchmark")

@@ -58,3 +58,3 @@
       - name: Upload Managed Device Reports
-        if: failure()
+        if: always()
         uses: actions/upload-artifact@v7
@@ -66,2 +66,5 @@
             app/build/outputs/managed_device_android_test_additional_output/
+            macrobenchmark/build/reports/androidTests/managedDevice/
+            macrobenchmark/build/outputs/androidTest-results/managedDevice/
+            macrobenchmark/build/outputs/managed_device_android_test_additional_output/
           if-no-files-found: warn

@@ -130,3 +130,6 @@
 @Composable
-fun AppNavGraph(startDestination: NavKey) {
+fun AppNavGraph(
+    startDestination: NavKey,
+    modifier: Modifier = Modifier,
+) {
     val context = LocalContext.current
@@ -150,3 +153,3 @@
             backStack = backStack,
-            modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background),
+            modifier = modifier.fillMaxSize().background(MaterialTheme.colorScheme.background),
             onBack = {

@@ -2,2 +2,4 @@
     alias(libs.plugins.android.application) apply false
+    alias(libs.plugins.android.test) apply false
+    alias(libs.plugins.kotlin.android) apply false
     alias(libs.plugins.kotlin.serialization) apply false

@@ -27,2 +27,4 @@
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTagsAsResourceId
 import androidx.compose.ui.unit.dp
@@ -201,3 +203,6 @@
         key(dest, preferences.vaultUriString) {
-            AppNavGraph(startDestination = dest)
+            AppNavGraph(
+                startDestination = dest,
+                modifier = Modifier.semantics { testTagsAsResourceId = true },
+            )
         }

@@ -257,2 +257,3 @@
                         .fillMaxSize()
+                        .testTag(TestTags.VEHICLE_LIST)
                         .padding(padding),

@@ -25,2 +25,4 @@
     // Vehicle list / detail
+    const val VEHICLE_LIST = "VehicleList"
+    const val RECORD_LIST = "RecordList"
     const val ADD_VEHICLE_FAB = "AddVehicleFab"

@@ -23,2 +23,3 @@
 billing = "9.0.0"
+benchmark = "1.4.1"
 sentry = "8.41.0"
@@ -61,2 +62,3 @@
 google-billing = { module = "com.android.billingclient:billing-ktx", version.ref = "billing" }
+androidx-benchmark-macro = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "benchmark" }

@@ -64,2 +66,4 @@
 android-application = { id = "com.android.application", version.ref = "agp" }
+android-test = { id = "com.android.test", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
 kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

@@ -61,2 +61,3 @@
 val regressionDeviceTaskName = "pixel2api33DebugAndroidTest"
+val macrobenchmarkTaskPath = ":macrobenchmark:pixel2api33BenchmarkAndroidTest"

@@ -74,2 +75,3 @@
         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+        buildConfigField("Boolean", "BENCHMARK", "false")
         vectorDrawables {
@@ -139,2 +141,15 @@
         }
+        create("benchmark") {
+            initWith(getByName("release"))
+            matchingFallbacks += listOf("release")
+            signingConfig = signingConfigs.getByName("debug")
+            isDebuggable = false
+            isMinifyEnabled = false
+            val dsnRelease =
+                (localProperties.getProperty("sentry.dsn.release") ?: "")
+                    .replace("\\", "\\\\")
+                    .replace("\"", "\\\"")
+            buildConfigField("String", "SENTRY_DSN", "\"$dsnRelease\"")
+            buildConfigField("Boolean", "BENCHMARK", "true")
+        }
     }
@@ -359,4 +374,4 @@
     group = "Verification"
-    description = "Runs Phase 1 full regression checks; sandbox and macrobenchmark siblings are deferred."
-    dependsOn(regressionFullCoreCheck, regressionFullExtendedCheck)
+    description = "Runs full regression checks, including the macrobenchmark sibling task."
+    dependsOn(regressionFullCoreCheck, regressionFullExtendedCheck, macrobenchmarkTaskPath)
 }

@@ -318,2 +318,3 @@
                             .fillMaxSize()
+                            .testTag(TestTags.RECORD_LIST)
                             .weight(1f),


Output:
Exit code: 0
Wall time: 8.5 seconds
Output:
Success. Updated the following files:
M /home/Messier82/projects/service-book/settings.gradle.kts
M /home/Messier82/projects/service-book/gradle/libs.versions.toml
M /home/Messier82/projects/service-book/build.gradle.kts
M /home/Messier82/projects/service-book/app/build.gradle.kts
M /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/ui/TestTags.kt
M /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/MainActivity.kt
M /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/ui/navigation/AppNavGraph.kt
M /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/ui/vehicles/VehicleListScreen.kt
M /home/Messier82/projects/service-book/app/src/main/java/com/yorvana/ui/records/VehicleDetailScreen.kt
M /home/Messier82/projects/service-book/.github/workflows/regression.yml


The root/app wiring is in place. Next I’m adding the benchmark-only seed activity plus the `:macrobenchmark` module with the four benchmark scenarios and a README/baseline scaffold.


Input:
*** Begin Patch
*** Add File: /home/Messier82/projects/service-book/app/src/benchmark/AndroidManifest.xml
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <application>
+        <profileable android:shell="true" />
+
+        <activity
+            android:name=".benchmark.BenchmarkSeedActivity"
+            android:exported="true"
+            android:theme="@style/Theme.Yorvana">
+            <intent-filter>
+                <action android:name="com.yorvana.benchmark.SEED_VAULT" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
*** Add File: /home/Messier82/projects/service-book/app/src/benchmark/java/com/yorvana/benchmark/BenchmarkSeedActivity.kt
+package com.yorvana.benchmark
+
+import android.app.Activity
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import com.yorvana.MainActivity
+import com.yorvana.YorvanaApplication
+import com.yorvana.data.model.ServiceRecord
+import com.yorvana.data.model.Vehicle
+import com.yorvana.data.preferences.CURRENT_CONSENT_VERSION
+import kotlinx.coroutines.runBlocking
+import kotlinx.serialization.json.Json
+import java.io.File
+
+class BenchmarkSeedActivity : Activity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        val fixture = intent.getStringExtra(EXTRA_FIXTURE) ?: FIXTURE_20_VEHICLES
+        val vaultRoot = File(filesDir, "benchmark_vault").apply {
+            deleteRecursively()
+            mkdirs()
+        }
+        when (fixture) {
+            FIXTURE_200_RECORDS -> seedRecordsVault(vaultRoot)
+            else -> seedVehiclesVault(vaultRoot)
+        }
+
+        val app = application as YorvanaApplication
+        val vaultUri = Uri.fromFile(vaultRoot).toString()
+        runBlocking {
+            app.preferences.setCrashReportingConsentPromptShownVersion(CURRENT_CONSENT_VERSION)
+            app.preferences.setVaultUri(vaultUri)
+            app.applyVaultUri(vaultUri)
+        }
+        getSharedPreferences("yorvana_navigation", MODE_PRIVATE)
+            .edit()
+            .putString("last_route", "vehicle-list")
+            .apply()
+
+        startActivity(
+            Intent(this, MainActivity::class.java).apply {
+                addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
+            },
+        )
+        finish()
+    }
+
+    private fun seedVehiclesVault(root: File) {
+        repeat(VEHICLE_COUNT) { index ->
+            val ordinal = index + 1
+            writeVehicle(
+                root = root,
+                vehicle =
+                    Vehicle(
+                        id = vehicleId(ordinal),
+                        nickname = "Benchmark Vehicle $ordinal",
+                        make = "Yorvana",
+                        model = "Runner",
+                        year = 2020 + index % 5,
+                        createdAt = CREATED_AT,
+                        lastServiceDate = "2026-01-01",
+                        recordCount = 0,
+                    ),
+            )
+        }
+    }
+
+    private fun seedRecordsVault(root: File) {
+        val vehicle =
+            Vehicle(
+                id = RECORD_VEHICLE_ID,
+                nickname = "Benchmark Record Vehicle",
+                make = "Yorvana",
+                model = "Ledger",
+                year = 2024,
+                createdAt = CREATED_AT,
+                lastServiceDate = "2026-01-01",
+                recordCount = RECORD_COUNT,
+            )
+        writeVehicle(root, vehicle)
+        repeat(RECORD_COUNT) { index ->
+            val ordinal = index + 1
+            val record =
+                ServiceRecord(
+                    id = recordId(ordinal),
+                    vehicleId = RECORD_VEHICLE_ID,
+                    date = "2026-${((index / 28) % 12) + 1}".padMonth() + "-${(index % 28) + 1}".padDay(),
+                    odometer = 10_000 + ordinal * 250,
+                    category = "oil-change",
+                    performer = "Benchmark Shop",
+                    cost = 50.0 + ordinal,
+                    currency = "USD",
+                    createdAt = CREATED_AT,
+                    updatedAt = CREATED_AT,
+                )
+            writeRecord(root, record)
+        }
+    }
+
+    private fun writeVehicle(
+        root: File,
+        vehicle: Vehicle,
+    ) {
+        File(root, "vehicles/${vehicle.id}").mkdirs()
+        File(root, "vehicles/${vehicle.id}/vehicle.json").writeText(json.encodeToString(Vehicle.serializer(), vehicle))
+    }
+
+    private fun writeRecord(
+        root: File,
+        record: ServiceRecord,
+    ) {
+        File(root, "vehicles/${record.vehicleId}/records").mkdirs()
+        File(root, "vehicles/${record.vehicleId}/records/${record.id}.json")
+            .writeText(json.encodeToString(ServiceRecord.serializer(), record))
+    }
+
+    private fun String.padMonth(): String = split("-").let { "${it[0]}-${it[1].padStart(2, '0')}" }
+
+    private fun Int.padDay(): String = toString().padStart(2, '0')
+
+    companion object {
+        const val ACTION_SEED = "com.yorvana.benchmark.SEED_VAULT"
+        const val EXTRA_FIXTURE = "fixture"
+        const val FIXTURE_20_VEHICLES = "large_20v"
+        const val FIXTURE_200_RECORDS = "large_200r"
+        const val RECORD_VEHICLE_ID = "benchmark-vehicle-records"
+        private const val VEHICLE_COUNT = 20
+        private const val RECORD_COUNT = 200
+        private const val CREATED_AT = "2026-01-01T00:00:00Z"
+        private val json = Json { encodeDefaults = true }
+
+        fun vehicleId(ordinal: Int): String = "benchmark-vehicle-${ordinal.toString().padStart(3, '0')}"
+
+        fun recordId(ordinal: Int): String = "benchmark-record-${ordinal.toString().padStart(3, '0')}"
+    }
+}
*** Add File: /home/Messier82/projects/service-book/macrobenchmark/build.gradle.kts
+plugins {
+    alias(libs.plugins.android.test)
+    alias(libs.plugins.kotlin.android)
+}
+
+android {
+    namespace = "com.yorvana.macrobenchmark"
+    compileSdk = 36
+    targetProjectPath = ":app"
+
+    defaultConfig {
+        minSdk = 26
+        targetSdk = 36
+        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+        testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "EMULATOR"
+    }
+
+    buildTypes {
+        create("benchmark") {
+            isDebuggable = false
+            matchingFallbacks += listOf("release")
+        }
+    }
+
+    compileOptions {
+        sourceCompatibility = JavaVersion.VERSION_17
+        targetCompatibility = JavaVersion.VERSION_17
+    }
+
+    testOptions {
+        managedDevices {
+            allDevices {
+                create<com.android.build.api.dsl.ManagedVirtualDevice>("pixel2api33") {
+                    device = "Pixel 2"
+                    apiLevel = 33
+                    systemImageSource = "aosp"
+                    testedAbi = "x86_64"
+                }
+            }
+        }
+    }
+}
+
+tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
+    compilerOptions {
+        jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
+    }
+}
+
+dependencies {
+    implementation(libs.androidx.benchmark.macro)
+    implementation(libs.androidx.test.ext)
+    implementation(libs.uiautomator)
+}
*** Add File: /home/Messier82/projects/service-book/macrobenchmark/src/main/AndroidManifest.xml
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <uses-permission android:name="android.permission.DUMP" />
+    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+
+    <application android:label="Yorvana Macrobenchmarks" />
+
+</manifest>
*** Add File: /home/Messier82/projects/service-book/macrobenchmark/src/main/java/com/yorvana/macrobenchmark/BenchmarkFixtures.kt
+package com.yorvana.macrobenchmark
+
+internal object BenchmarkFixtures {
+    const val PACKAGE_NAME = "com.yorvana"
+    const val SEED_ACTION = "com.yorvana.benchmark.SEED_VAULT"
+    const val EXTRA_FIXTURE = "fixture"
+    const val LARGE_20_VEHICLES = "large_20v"
+    const val LARGE_200_RECORDS = "large_200r"
+    const val RECORD_VEHICLE_ID = "benchmark-vehicle-records"
+
+    fun vehicleItem(ordinal: Int): String = "VehicleItem_benchmark-vehicle-${ordinal.toString().padStart(3, '0')}"
+
+    fun recordItem(ordinal: Int): String = "RecordItem_benchmark-record-${ordinal.toString().padStart(3, '0')}"
+}
*** Add File: /home/Messier82/projects/service-book/macrobenchmark/src/main/java/com/yorvana/macrobenchmark/BenchmarkTags.kt
+package com.yorvana.macrobenchmark
+
+internal object BenchmarkTags {
+    const val VEHICLE_LIST = "VehicleList"
+    const val RECORD_LIST = "RecordList"
+}
*** Add File: /home/Messier82/projects/service-book/macrobenchmark/src/main/java/com/yorvana/macrobenchmark/YorvanaBenchmark.kt
+package com.yorvana.macrobenchmark
+
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.FrameTimingMetric
+import androidx.benchmark.macro.MacrobenchmarkRule
+import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.StartupTimingMetric
+import androidx.benchmark.macro.junit4.measureRepeated
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Direction
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class YorvanaBenchmark {
+    @get:Rule
+    val benchmarkRule = MacrobenchmarkRule()
+
+    @Test
+    fun M01_coldStart() =
+        benchmarkRule.measureRepeated(
+            packageName = BenchmarkFixtures.PACKAGE_NAME,
+            metrics = listOf(StartupTimingMetric()),
+            compilationMode = CompilationMode.Partial(),
+            iterations = ITERATIONS,
+            startupMode = StartupMode.COLD,
+            setupBlock = {
+                pressHome()
+            },
+        ) {
+            startActivityAndWait()
+            device.waitForObject(By.res(BenchmarkTags.VEHICLE_LIST))
+        }
+
+    @Test
+    fun M02_vehicleListScroll() =
+        benchmarkRule.measureRepeated(
+            packageName = BenchmarkFixtures.PACKAGE_NAME,
+            metrics = listOf(FrameTimingMetric()),
+            compilationMode = CompilationMode.Partial(),
+            iterations = ITERATIONS,
+            setupBlock = {
+                seedVault(BenchmarkFixtures.LARGE_20_VEHICLES)
+            },
+        ) {
+            val list = device.waitForObject(By.res(BenchmarkTags.VEHICLE_LIST))
+            repeat(SCROLL_REPEAT_COUNT) {
+                list.fling(Direction.DOWN)
+                device.waitForIdle()
+                list.fling(Direction.UP)
+                device.waitForIdle()
+            }
+        }
+
+    @Test
+    fun M03_recordListScroll() =
+        benchmarkRule.measureRepeated(
+            packageName = BenchmarkFixtures.PACKAGE_NAME,
+            metrics = listOf(FrameTimingMetric()),
+            compilationMode = CompilationMode.Partial(),
+            iterations = ITERATIONS,
+            setupBlock = {
+                seedVault(BenchmarkFixtures.LARGE_200_RECORDS)
+                openRecordVehicle()
+            },
+        ) {
+            val list = device.waitForObject(By.res(BenchmarkTags.RECORD_LIST))
+            repeat(SCROLL_REPEAT_COUNT) {
+                list.fling(Direction.DOWN)
+                device.waitForIdle()
+                list.fling(Direction.UP)
+                device.waitForIdle()
+            }
+        }
+
+    @Test
+    fun M04_navigationGarageDetailRecord() =
+        benchmarkRule.measureRepeated(
+            packageName = BenchmarkFixtures.PACKAGE_NAME,
+            metrics = listOf(FrameTimingMetric()),
+            compilationMode = CompilationMode.Partial(),
+            iterations = ITERATIONS,
+            setupBlock = {
+                seedVault(BenchmarkFixtures.LARGE_200_RECORDS)
+            },
+        ) {
+            device.waitForObject(By.res(BenchmarkFixtures.vehicleItem(1))).click()
+            device.waitForObject(By.res(BenchmarkTags.RECORD_LIST))
+            device.waitForObject(By.res(BenchmarkFixtures.recordItem(1))).click()
+            device.waitForIdle()
+            device.pressBack()
+            device.waitForObject(By.res(BenchmarkTags.RECORD_LIST))
+            device.pressBack()
+            device.waitForObject(By.res(BenchmarkTags.VEHICLE_LIST))
+        }
+
+    private fun MacrobenchmarkRule.Scope.seedVault(fixture: String) {
+        device.pressHome()
+        device.executeShellCommand(
+            "am start -W -a ${BenchmarkFixtures.SEED_ACTION} " +
+                "--es ${BenchmarkFixtures.EXTRA_FIXTURE} $fixture ${BenchmarkFixtures.PACKAGE_NAME}",
+        )
+        device.waitForObject(By.res(BenchmarkTags.VEHICLE_LIST))
+    }
+
+    private fun MacrobenchmarkRule.Scope.openRecordVehicle() {
+        device.waitForObject(By.res(BenchmarkFixtures.vehicleItem(1))).click()
+        device.waitForObject(By.res(BenchmarkTags.RECORD_LIST))
+    }
+
+    private fun UiDevice.waitForObject(selector: BySelectorCompat) =
+        wait(Until.findObject(selector.selector), TIMEOUT_MS) ?: error("Timed out waiting for ${selector.label}")
+
+    private fun By.res(resourceName: String): BySelectorCompat =
+        BySelectorCompat(selector = androidx.test.uiautomator.By.res(resourceName), label = resourceName)
+
+    private data class BySelectorCompat(
+        val selector: androidx.test.uiautomator.BySelector,
+        val label: String,
+    )
+
+    companion object {
+        private const val ITERATIONS = 5
+        private const val SCROLL_REPEAT_COUNT = 2
+        private const val TIMEOUT_MS = 30_000L
+    }
+}
*** Add File: /home/Messier82/projects/service-book/macrobenchmark/baselines/README.md
+# Macrobenchmark Baselines
+
+These baselines are calibrated from five runs on the dedicated Pixel 2 API 33
+Gradle Managed Device with `swiftshader_indirect`.
+
+The first committed values are placeholders for the CI artifact contract. Replace
+them with measured values after running:
+
+```bash
+./gradlew :macrobenchmark:pixel2api33BenchmarkAndroidTest \
+  -Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect
+```
+
+Absolute NFR targets are advisory on swiftshader. Relative regressions greater
+than 20% against the calibrated values are the hard gate.
*** Add File: /home/Messier82/projects/service-book/macrobenchmark/baselines/baselines.json
+{
+  "schemaVersion": 1,
+  "calibration": {
+    "runs": 5,
+    "device": "Pixel 2 API 33 GMD",
+    "gpu": "swiftshader_indirect"
+  },
+  "thresholds": {
+    "relativeRegressionPercent": 20,
+    "absoluteTargetsAreAdvisory": true
+  },
+  "scenarios": {
+    "M01_coldStart": {},
+    "M02_vehicleListScroll": {},
+    "M03_recordListScroll": {},
+    "M04_navigationGarageDetailRecord": {}
+  }
+}
*** Add File: /home/Messier82/projects/service-book/macrobenchmark/README.md
+# Yorvana Macrobenchmarks
+
+This module owns Phase 6 of the regression plan: startup, large-list scrolling,
+and garage -> detail -> record navigation performance checks.
+
+## Device And Variant
+
+Benchmarks run against `:app`'s non-debuggable `benchmark` build type. That
+variant is signed with the debug key for local/GMD installability and includes
+`<profileable shell="true" />` so Macrobenchmark can collect traces without
+benchmarking a debuggable APK.
+
+The module registers its own Pixel 2 API 33 GMD entry, producing:
+
+```bash
+./gradlew :macrobenchmark:pixel2api33BenchmarkAndroidTest
+```
+
+`regressionFullCheck` depends on that task as a sibling of the app regression
+GradleBuild tasks so app instrumentation filters cannot leak into benchmark
+runs.
+
+## Fixtures
+
+Fixture ownership uses the benchmark-variant-only hook strategy from issue
+#119. `app/src/benchmark` exposes `BenchmarkSeedActivity` for the benchmark
+variant only. The hook is absent from `debug`, `release`, and `staging` source
+sets and is not gated on `BuildConfig.DEBUG`.
+
+Supported fixtures:
+
+- `large_20v`: 20 deterministic vehicles for M-02.
+- `large_200r`: one deterministic vehicle with 200 records for M-03 and M-04.
+
+The hook writes a file-backed vault under the app's private files directory,
+marks setup complete, clears the saved route to the garage, and launches the
+app before each measured iteration.
+
+## Selectors
+
+Macrobenchmark drives Compose through UiAutomator `By.res(...)`. Production
+applies `Modifier.semantics { testTagsAsResourceId = true }` at the app-root
+navigation hierarchy, and the benchmarked lists use stable tags:
+
+- `TestTags.VEHICLE_LIST`
+- `TestTags.vehicleItem(id)`
+- `TestTags.RECORD_LIST`
+- `TestTags.recordItem(id)`
+
+## Metrics
+
+- M-01: cold start, `StartupTimingMetric`; P50 <= 2000 ms is advisory.
+- M-02: 20-vehicle list scroll, `FrameTimingMetric`.
+- M-03: 200-record list scroll, `FrameTimingMetric`.
+- M-04: scripted garage -> detail -> record navigation, `FrameTimingMetric`.
+
+M-04 intentionally does not use `TraceSectionMetric`. The current Navigation 3
+`NavDisplay` setup does not expose a reliable span that covers destination
+composition plus transition animation frames; tracing only the navigation event
+would measure the wrong work.
+
+## Threshold Policy
+
+Swiftshader emulator numbers are advisory regression signal only. Absolute NFRs
+are logged in benchmark artifacts and PR notes, but they do not hard-fail CI.
+
+The hard gate is relative regression greater than 20% against baselines
+calibrated from five runs on the same Pixel 2 API 33 GMD shape. Refresh
+`baselines/baselines.json` after running the benchmark task five times on the
+same runner configuration and note the calibration in the PR.
*** End Patch


Success. Updated the following files:
A /home/Messier82/projects/service-book/app/src/benchmark/AndroidManifest.xml
A /home/Messier82/projects/service-book/app/src/benchmark/java/com/yorvana/benchmark/BenchmarkSeedActivity.kt
A /home/Messier82/projects/service-book/macrobenchmark/build.gradle.kts
A /home/Messier82/projects/service-book/macrobenchmark/src/main/AndroidManifest.xml
A /home/Messier82/projects/service-book/macrobenchmark/src/main/java/com/yorvana/macrobenchmark/BenchmarkFixtures.kt
A /home/Messier82/projects/service-book/macrobenchmark/src/main/java/com/yorvana/macrobenchmark/BenchmarkTags.kt
A /home/Messier82/projects/service-book/macrobenchmark/src/main/java/com/yorvana/macrobenchmark/YorvanaBenchmark.kt
A /home/Messier82/projects/service-book/macrobenchmark/baselines/README.md
A /home/Messier82/projects/service-book/macrobenchmark/baselines/baselines.json
A /home/Messier82/projects/service-book/macrobenchmark/README.md

package com.yorvana.macrobenchmark

internal object BenchmarkTags {
    const val VEHICLE_LIST = "VehicleList"
    const val RECORD_LIST = "RecordList"
}

# Yorvana Macrobenchmarks

This module owns Phase 6 of the regression plan: startup, large-list scrolling,
and garage -> detail -> record navigation performance checks.

## Device And Variant

Benchmarks run against `:app`'s non-debuggable `benchmark` build type. That
variant is signed with the debug key for local/GMD installability and includes
`<profileable shell="true" />` so Macrobenchmark can collect traces without
benchmarking a debuggable APK.

The module registers its own Pixel 2 API 33 GMD entry, producing:

```bash
./gradlew :macrobenchmark:pixel2api33BenchmarkAndroidTest
```

`regressionFullCheck` depends on that task as a sibling of the app regression
GradleBuild tasks so app instrumentation filters cannot leak into benchmark
runs.

## Fixtures

Fixture ownership uses the benchmark-variant-only hook strategy from issue
#119. `app/src/benchmark` exposes `BenchmarkSeedActivity` for the benchmark
variant only. The hook is absent from `debug`, `release`, and `staging` source
sets and is not gated on `BuildConfig.DEBUG`.

Supported fixtures:

- `large_20v`: 20 deterministic vehicles for M-02.
- `large_200r`: one deterministic vehicle with 200 records for M-03 and M-04.

The hook writes a file-backed vault under the app's private files directory,
marks setup complete, clears the saved route to the garage, and launches the
app before each measured iteration.

## Selectors

Macrobenchmark drives Compose through UiAutomator `By.res(...)`. Production
applies `Modifier.semantics { testTagsAsResourceId = true }` at the app-root
navigation hierarchy, and the benchmarked lists use stable tags:

- `TestTags.VEHICLE_LIST`
- `TestTags.vehicleItem(id)`
- `TestTags.RECORD_LIST`
- `TestTags.recordItem(id)`

## Metrics

- M-01: cold start, `StartupTimingMetric`; P50 <= 2000 ms is advisory.
- M-02: 20-vehicle list scroll, `FrameTimingMetric`.
- M-03: 200-record list scroll, `FrameTimingMetric`.
- M-04: scripted garage -> detail -> record navigation, `FrameTimingMetric`.

M-04 intentionally does not use `TraceSectionMetric`. The current Navigation 3
`NavDisplay` setup does not expose a reliable span that covers destination
composition plus transition animation frames; tracing only the navigation event
would measure the wrong work.

## Threshold Policy

Swiftshader emulator numbers are advisory regression signal only. Absolute NFRs
are logged in benchmark artifacts and PR notes, but they do not hard-fail CI.

The hard gate is relative regression greater than 20% against baselines
calibrated from five runs on the same Pixel 2 API 33 GMD shape. Refresh
`baselines/baselines.json` after running the benchmark task five times on the
same runner configuration and note the calibration in the PR.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application>
        <profileable android:shell="true" />

        <activity
            android:name=".benchmark.BenchmarkSeedActivity"
            android:exported="true"
            android:theme="@style/Theme.Yorvana">
            <intent-filter>
                <action android:name="com.yorvana.benchmark.SEED_VAULT" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>
    </application>

</manifest>

package com.yorvana.macrobenchmark

internal object BenchmarkFixtures {
    const val PACKAGE_NAME = "com.yorvana"
    const val SEED_ACTION = "com.yorvana.benchmark.SEED_VAULT"
    const val EXTRA_FIXTURE = "fixture"
    const val LARGE_20_VEHICLES = "large_20v"
    const val LARGE_200_RECORDS = "large_200r"
    const val RECORD_VEHICLE_ID = "benchmark-vehicle-records"

    fun vehicleItem(ordinal: Int): String = "VehicleItem_benchmark-vehicle-${ordinal.toString().padStart(3, '0')}"

    fun recordItem(ordinal: Int): String = "RecordItem_benchmark-record-${ordinal.toString().padStart(3, '0')}"
}

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-permission android:name="android.permission.DUMP" />
    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />

    <application android:label="Yorvana Macrobenchmarks" />

</manifest>

package com.yorvana.macrobenchmark

import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.FrameTimingMetric
import androidx.benchmark.macro.MacrobenchmarkRule
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingMetric
import androidx.benchmark.macro.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Direction
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class YorvanaBenchmark {
    @get:Rule
    val benchmarkRule = MacrobenchmarkRule()

    @Test
    fun M01_coldStart() =
        benchmarkRule.measureRepeated(
            packageName = BenchmarkFixtures.PACKAGE_NAME,
            metrics = listOf(StartupTimingMetric()),
            compilationMode = CompilationMode.Partial(),
            iterations = ITERATIONS,
            startupMode = StartupMode.COLD,
            setupBlock = {
                pressHome()
            },
        ) {
            startActivityAndWait()
            device.waitForObject(By.res(BenchmarkTags.VEHICLE_LIST))
        }

    @Test
    fun M02_vehicleListScroll() =
        benchmarkRule.measureRepeated(
            packageName = BenchmarkFixtures.PACKAGE_NAME,
            metrics = listOf(FrameTimingMetric()),
            compilationMode = CompilationMode.Partial(),
            iterations = ITERATIONS,
            setupBlock = {
                seedVault(BenchmarkFixtures.LARGE_20_VEHICLES)
            },
        ) {
            val list = device.waitForObject(By.res(BenchmarkTags.VEHICLE_LIST))
            repeat(SCROLL_REPEAT_COUNT) {
                list.fling(Direction.DOWN)
                device.waitForIdle()
                list.fling(Direction.UP)
                device.waitForIdle()
            }
        }

    @Test
    fun M03_recordListScroll() =
        benchmarkRule.measureRepeated(
            packageName = BenchmarkFixtures.PACKAGE_NAME,
            metrics = listOf(FrameTimingMetric()),
            compilationMode = CompilationMode.Partial(),
            iterations = ITERATIONS,
            setupBlock = {
                seedVault(BenchmarkFixtures.LARGE_200_RECORDS)
                openRecordVehicle()
            },
        ) {
            val list = device.waitForObject(By.res(BenchmarkTags.RECORD_LIST))
            repeat(SCROLL_REPEAT_COUNT) {
                list.fling(Direction.DOWN)
                device.waitForIdle()
                list.fling(Direction.UP)
                device.waitForIdle()
            }
        }

    @Test
    fun M04_navigationGarageDetailRecord() =
        benchmarkRule.measureRepeated(
            packageName = BenchmarkFixtures.PACKAGE_NAME,
            metrics = listOf(FrameTimingMetric()),
            compilationMode = CompilationMode.Partial(),
            iterations = ITERATIONS,
            setupBlock = {
                seedVault(BenchmarkFixtures.LARGE_200_RECORDS)
            },
        ) {
            device.waitForObject(By.res(BenchmarkFixtures.vehicleItem(1))).click()
            device.waitForObject(By.res(BenchmarkTags.RECORD_LIST))
            device.waitForObject(By.res(BenchmarkFixtures.recordItem(1))).click()
            device.waitForIdle()
            device.pressBack()
            device.waitForObject(By.res(BenchmarkTags.RECORD_LIST))
            device.pressBack()
            device.waitForObject(By.res(BenchmarkTags.VEHICLE_LIST))
        }

    private fun MacrobenchmarkRule.Scope.seedVault(fixture: String) {
        device.pressHome()
        device.executeShellCommand(
            "am start -W -a ${BenchmarkFixtures.SEED_ACTION} " +
                "--es ${BenchmarkFixtures.EXTRA_FIXTURE} $fixture ${BenchmarkFixtures.PACKAGE_NAME}",
        )
        device.waitForObject(By.res(BenchmarkTags.VEHICLE_LIST))
    }

    private fun MacrobenchmarkRule.Scope.openRecordVehicle() {
        device.waitForObject(By.res(BenchmarkFixtures.vehicleItem(1))).click()
        device.waitForObject(By.res(BenchmarkTags.RECORD_LIST))
    }

    private fun UiDevice.waitForObject(selector: BySelectorCompat) =
        wait(Until.findObject(selector.selector), TIMEOUT_MS) ?: error("Timed out waiting for ${selector.label}")

    private fun By.res(resourceName: String): BySelectorCompat =
        BySelectorCompat(selector = androidx.test.uiautomator.By.res(resourceName), label = resourceName)

    private data class BySelectorCompat(
        val selector: androidx.test.uiautomator.BySelector,
        val label: String,
    )

    companion object {
        private const val ITERATIONS = 5
        private const val SCROLL_REPEAT_COUNT = 2
        private const val TIMEOUT_MS = 30_000L
    }
}

plugins {
    alias(libs.plugins.android.test)
    alias(libs.plugins.kotlin.android)
}

android {
    namespace = "com.yorvana.macrobenchmark"
    compileSdk = 36
    targetProjectPath = ":app"

    defaultConfig {
        minSdk = 26
        targetSdk = 36
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "EMULATOR"
    }

    buildTypes {
        create("benchmark") {
            isDebuggable = false
            matchingFallbacks += listOf("release")
        }
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    testOptions {
        managedDevices {
            allDevices {
                create<com.android.build.api.dsl.ManagedVirtualDevice>("pixel2api33") {
                    device = "Pixel 2"
                    apiLevel = 33
                    systemImageSource = "aosp"
                    testedAbi = "x86_64"
                }
            }
        }
    }
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
    compilerOptions {
        jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
    }
}

dependencies {
    implementation(libs.androidx.benchmark.macro)
    implementation(libs.androidx.test.ext)
    implementation(libs.uiautomator)
}

# Macrobenchmark Baselines

These baselines are calibrated from five runs on the dedicated Pixel 2 API 33
Gradle Managed Device with `swiftshader_indirect`.

The first committed values are placeholders for the CI artifact contract. Replace
them with measured values after running:

```bash
./gradlew :macrobenchmark:pixel2api33BenchmarkAndroidTest \
  -Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect
```

Absolute NFR targets are advisory on swiftshader. Relative regressions greater
than 20% against the calibrated values are the hard gate.

package com.yorvana.benchmark

import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import com.yorvana.MainActivity
import com.yorvana.YorvanaApplication
import com.yorvana.data.model.ServiceRecord
import com.yorvana.data.model.Vehicle
import com.yorvana.data.preferences.CURRENT_CONSENT_VERSION
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import java.io.File

class BenchmarkSeedActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val fixture = intent.getStringExtra(EXTRA_FIXTURE) ?: FIXTURE_20_VEHICLES
        val vaultRoot = File(filesDir, "benchmark_vault").apply {
            deleteRecursively()
            mkdirs()
        }
        when (fixture) {
            FIXTURE_200_RECORDS -> seedRecordsVault(vaultRoot)
            else -> seedVehiclesVault(vaultRoot)
        }

        val app = application as YorvanaApplication
        val vaultUri = Uri.fromFile(vaultRoot).toString()
        runBlocking {
            app.preferences.setCrashReportingConsentPromptShownVersion(CURRENT_CONSENT_VERSION)
            app.preferences.setVaultUri(vaultUri)
            app.applyVaultUri(vaultUri)
        }
        getSharedPreferences("yorvana_navigation", MODE_PRIVATE)
            .edit()
            .putString("last_route", "vehicle-list")
            .apply()

        startActivity(
            Intent(this, MainActivity::class.java).apply {
                addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
            },
        )
        finish()
    }

    private fun seedVehiclesVault(root: File) {
        repeat(VEHICLE_COUNT) { index ->
            val ordinal = index + 1
            writeVehicle(
                root = root,
                vehicle =
                    Vehicle(
                        id = vehicleId(ordinal),
                        nickname = "Benchmark Vehicle $ordinal",
                        make = "Yorvana",
                        model = "Runner",
                        year = 2020 + index % 5,
                        createdAt = CREATED_AT,
                        lastServiceDate = "2026-01-01",
                        recordCount = 0,
                    ),
            )
        }
    }

    private fun seedRecordsVault(root: File) {
        val vehicle =
            Vehicle(
                id = RECORD_VEHICLE_ID,
                nickname = "Benchmark Record Vehicle",
                make = "Yorvana",
                model = "Ledger",
                year = 2024,
                createdAt = CREATED_AT,
                lastServiceDate = "2026-01-01",
                recordCount = RECORD_COUNT,
            )
        writeVehicle(root, vehicle)
        repeat(RECORD_COUNT) { index ->
            val ordinal = index + 1
            val record =
                ServiceRecord(
                    id = recordId(ordinal),
                    vehicleId = RECORD_VEHICLE_ID,
                    date = "2026-${((index / 28) % 12) + 1}".padMonth() + "-${(index % 28) + 1}".padDay(),
                    odometer = 10_000 + ordinal * 250,
                    category = "oil-change",
                    performer = "Benchmark Shop",
                    cost = 50.0 + ordinal,
                    currency = "USD",
                    createdAt = CREATED_AT,
                    updatedAt = CREATED_AT,
                )
            writeRecord(root, record)
        }
    }

    private fun writeVehicle(
        root: File,
        vehicle: Vehicle,
    ) {
        File(root, "vehicles/${vehicle.id}").mkdirs()
        File(root, "vehicles/${vehicle.id}/vehicle.json").writeText(json.encodeToString(Vehicle.serializer(), vehicle))
    }

    private fun writeRecord(
        root: File,
        record: ServiceRecord,
    ) {
        File(root, "vehicles/${record.vehicleId}/records").mkdirs()
        File(root, "vehicles/${record.vehicleId}/records/${record.id}.json")
            .writeText(json.encodeToString(ServiceRecord.serializer(), record))
    }

    private fun String.padMonth(): String = split("-").let { "${it[0]}-${it[1].padStart(2, '0')}" }

    private fun Int.padDay(): String = toString().padStart(2, '0')

    companion object {
        const val ACTION_SEED = "com.yorvana.benchmark.SEED_VAULT"
        const val EXTRA_FIXTURE = "fixture"
        const val FIXTURE_20_VEHICLES = "large_20v"
        const val FIXTURE_200_RECORDS = "large_200r"
        const val RECORD_VEHICLE_ID = "benchmark-vehicle-records"
        private const val VEHICLE_COUNT = 20
        private const val RECORD_COUNT = 200
        private const val CREATED_AT = "2026-01-01T00:00:00Z"
        private val json = Json { encodeDefaults = true }

        fun vehicleId(ordinal: Int): String = "benchmark-vehicle-${ordinal.toString().padStart(3, '0')}"

        fun recordId(ordinal: Int): String = "benchmark-record-${ordinal.toString().padStart(3, '0')}"
    }
}


Output:
Exit code: 0
Wall time: 11.9 seconds
Output:
Success. Updated the following files:
A /home/Messier82/projects/service-book/app/src/benchmark/AndroidManifest.xml
A /home/Messier82/projects/service-book/app/src/benchmark/java/com/yorvana/benchmark/BenchmarkSeedActivity.kt
A /home/Messier82/projects/service-book/macrobenchmark/build.gradle.kts
A /home/Messier82/projects/service-book/macrobenchmark/src/main/AndroidManifest.xml
A /home/Messier82/projects/service-book/macrobenchmark/src/main/java/com/yorvana/macrobenchmark/BenchmarkFixtures.kt
A /home/Messier82/projects/service-book/macrobenchmark/src/main/java/com/yorvana/macrobenchmark/BenchmarkTags.kt
A /home/Messier82/projects/service-book/macrobenchmark/src/main/java/com/yorvana/macrobenchmark/YorvanaBenchmark.kt
A /home/Messier82/projects/service-book/macrobenchmark/baselines/README.md
A /home/Messier82/projects/service-book/macrobenchmark/baselines/baselines.json
A /home/Messier82/projects/service-book/macrobenchmark/README.md


I spotted a couple of Kotlin API issues in the first pass, mainly around the Macrobenchmark scope and UiAutomator selector helper. I’m cleaning those up before running Gradle so the first verification pass is meaningful.


Input:
*** Begin Patch
*** Update File: /home/Messier82/projects/service-book/macrobenchmark/src/main/java/com/yorvana/macrobenchmark/YorvanaBenchmark.kt
@@
 import androidx.benchmark.macro.FrameTimingMetric
 import androidx.benchmark.macro.MacrobenchmarkRule
+import androidx.benchmark.macro.MacrobenchmarkScope
 import androidx.benchmark.macro.StartupMode
 import androidx.benchmark.macro.StartupTimingMetric
 import androidx.benchmark.macro.junit4.measureRepeated
@@
     private fun MacrobenchmarkRule.Scope.seedVault(fixture: String) {
+        device.pressHome()
+        device.executeShellCommand(
+            "am start -W -a ${BenchmarkFixtures.SEED_ACTION} " +
+                "--es ${BenchmarkFixtures.EXTRA_FIXTURE} $fixture ${BenchmarkFixtures.PACKAGE_NAME}",
+        )
+        device.waitForObject(BenchmarkTags.VEHICLE_LIST)
+    }
+
+    private fun MacrobenchmarkScope.seedVault(fixture: String) {
         device.pressHome()
         device.executeShellCommand(
             "am start -W -a ${BenchmarkFixtures.SEED_ACTION} " +
                 "--es ${BenchmarkFixtures.EXTRA_FIXTURE} $fixture ${BenchmarkFixtures.PACKAGE_NAME}",
         )
-        device.waitForObject(By.res(BenchmarkTags.VEHICLE_LIST))
+        device.waitForObject(BenchmarkTags.VEHICLE_LIST)
     }

-    private fun MacrobenchmarkRule.Scope.openRecordVehicle() {
-        device.waitForObject(By.res(BenchmarkFixtures.vehicleItem(1))).click()
-        device.waitForObject(By.res(BenchmarkTags.RECORD_LIST))
+    private fun MacrobenchmarkScope.openRecordVehicle() {
+        device.waitForObject(BenchmarkFixtures.vehicleItem(1)).click()
+        device.waitForObject(BenchmarkTags.RECORD_LIST)
     }

-    private fun UiDevice.waitForObject(selector: BySelectorCompat) =
-        wait(Until.findObject(selector.selector), TIMEOUT_MS) ?: error("Timed out waiting for ${selector.label}")
-
-    private fun By.res(resourceName: String): BySelectorCompat =
-        BySelectorCompat(selector = androidx.test.uiautomator.By.res(resourceName), label = resourceName)
-
-    private data class BySelectorCompat(
-        val selector: androidx.test.uiautomator.BySelector,
-        val label: String,
-    )
+    private fun UiDevice.waitForObject(resourceName: String) =
+        wait(Until.findObject(By.res(resourceName)), TIMEOUT_MS) ?: error("Timed out waiting for $resourceName")
*** End Patch


Success. Updated the following files:
M /home/Messier82/projects/service-book/macrobenchmark/src/main/java/com/yorvana/macrobenchmark/YorvanaBenchmark.kt

@@ -5,2 +5,3 @@
 import androidx.benchmark.macro.MacrobenchmarkRule
+import androidx.benchmark.macro.MacrobenchmarkScope
 import androidx.benchmark.macro.StartupMode
@@ -106,20 +107,21 @@
         )
-        device.waitForObject(By.res(BenchmarkTags.VEHICLE_LIST))
+        device.waitForObject(BenchmarkTags.VEHICLE_LIST)
     }

-    private fun MacrobenchmarkRule.Scope.openRecordVehicle() {
-        device.waitForObject(By.res(BenchmarkFixtures.vehicleItem(1))).click()
-        device.waitForObject(By.res(BenchmarkTags.RECORD_LIST))
+    private fun MacrobenchmarkScope.seedVault(fixture: String) {
+        device.pressHome()
+        device.executeShellCommand(
+            "am start -W -a ${BenchmarkFixtures.SEED_ACTION} " +
+                "--es ${BenchmarkFixtures.EXTRA_FIXTURE} $fixture ${BenchmarkFixtures.PACKAGE_NAME}",
+        )
+        device.waitForObject(BenchmarkTags.VEHICLE_LIST)
     }

-    private fun UiDevice.waitForObject(selector: BySelectorCompat) =
-        wait(Until.findObject(selector.selector), TIMEOUT_MS) ?: error("Timed out waiting for ${selector.label}")
+    private fun MacrobenchmarkScope.openRecordVehicle() {
+        device.waitForObject(BenchmarkFixtures.vehicleItem(1)).click()
+        device.waitForObject(BenchmarkTags.RECORD_LIST)
+    }

-    private fun By.res(resourceName: String): BySelectorCompat =
-        BySelectorCompat(selector = androidx.test.uiautomator.By.res(resourceName), label = resourceName)
-
-    private data class BySelectorCompat(
-        val selector: androidx.test.uiautomator.BySelector,
-        val label: String,
-    )
+    private fun UiDevice.waitForObject(resourceName: String) =
+        wait(Until.findObject(By.res(resourceName)), TIMEOUT_MS) ?: error("Timed out waiting for $resourceName")


Output:
Exit code: 0
Wall time: 0.9 seconds
Output:
Success. Updated the following files:
M /home/Messier82/projects/service-book/macrobenchmark/src/main/java/com/yorvana/macrobenchmark/YorvanaBenchmark.kt


sed -n '1,240p' macrobenchmark/src/main/java/com/yorvana/macrobenchmark/YorvanaBenchmark.kt


Output:
Chunk ID: e468b4
Wall time: 0.1576 seconds
Process exited with code 0
Original token count: 1234
Output:
package com.yorvana.macrobenchmark

import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.FrameTimingMetric
import androidx.benchmark.macro.MacrobenchmarkRule
import androidx.benchmark.macro.MacrobenchmarkScope
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingMetric
import androidx.benchmark.macro.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Direction
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class YorvanaBenchmark {
    @get:Rule
    val benchmarkRule = MacrobenchmarkRule()

    @Test
    fun M01_coldStart() =
        benchmarkRule.measureRepeated(
            packageName = BenchmarkFixtures.PACKAGE_NAME,
            metrics = listOf(StartupTimingMetric()),
            compilationMode = CompilationMode.Partial(),
            iterations = ITERATIONS,
            startupMode = StartupMode.COLD,
            setupBlock = {
                pressHome()
            },
        ) {
            startActivityAndWait()
            device.waitForObject(By.res(BenchmarkTags.VEHICLE_LIST))
        }

    @Test
    fun M02_vehicleListScroll() =
        benchmarkRule.measureRepeated(
            packageName = BenchmarkFixtures.PACKAGE_NAME,
            metrics = listOf(FrameTimingMetric()),
            compilationMode = CompilationMode.Partial(),
            iterations = ITERATIONS,
            setupBlock = {
                seedVault(BenchmarkFixtures.LARGE_20_VEHICLES)
            },
        ) {
            val list = device.waitForObject(By.res(BenchmarkTags.VEHICLE_LIST))
            repeat(SCROLL_REPEAT_COUNT) {
                list.fling(Direction.DOWN)
                device.waitForIdle()
                list.fling(Direction.UP)
                device.waitForIdle()
            }
        }

    @Test
    fun M03_recordListScroll() =
        benchmarkRule.measureRepeated(
            packageName = BenchmarkFixtures.PACKAGE_NAME,
            metrics = listOf(FrameTimingMetric()),
            compilationMode = CompilationMode.Partial(),
            iterations = ITERATIONS,
            setupBlock = {
                seedVault(BenchmarkFixtures.LARGE_200_RECORDS)
                openRecordVehicle()
            },
        ) {
            val list = device.waitForObject(By.res(BenchmarkTags.RECORD_LIST))
            repeat(SCROLL_REPEAT_COUNT) {
                list.fling(Direction.DOWN)
                device.waitForIdle()
                list.fling(Direction.UP)
                device.waitForIdle()
            }
        }

    @Test
    fun M04_navigationGarageDetailRecord() =
        benchmarkRule.measureRepeated(
            packageName = BenchmarkFixtures.PACKAGE_NAME,
            metrics = listOf(FrameTimingMetric()),
            compilationMode = CompilationMode.Partial(),
            iterations = ITERATIONS,
            setupBlock = {
                seedVault(BenchmarkFixtures.LARGE_200_RECORDS)
            },
        ) {
            device.waitForObject(By.res(BenchmarkFixtures.vehicleItem(1))).click()
            device.waitForObject(By.res(BenchmarkTags.RECORD_LIST))
            device.waitForObject(By.res(BenchmarkFixtures.recordItem(1))).click()
            device.waitForIdle()
            device.pressBack()
            device.waitForObject(By.res(BenchmarkTags.RECORD_LIST))
            device.pressBack()
            device.waitForObject(By.res(BenchmarkTags.VEHICLE_LIST))
        }

    private fun MacrobenchmarkRule.Scope.seedVault(fixture: String) {
        device.pressHome()
        device.executeShellCommand(
            "am start -W -a ${BenchmarkFixtures.SEED_ACTION} " +
                "--es ${BenchmarkFixtures.EXTRA_FIXTURE} $fixture ${BenchmarkFixtures.PACKAGE_NAME}",
        )
        device.waitForObject(BenchmarkTags.VEHICLE_LIST)
    }

    private fun MacrobenchmarkScope.seedVault(fixture: String) {
        device.pressHome()
        device.executeShellCommand(
            "am start -W -a ${BenchmarkFixtures.SEED_ACTION} " +
                "--es ${BenchmarkFixtures.EXTRA_FIXTURE} $fixture ${BenchmarkFixtures.PACKAGE_NAME}",
        )
        device.waitForObject(BenchmarkTags.VEHICLE_LIST)
    }

    private fun MacrobenchmarkScope.openRecordVehicle() {
        device.waitForObject(BenchmarkFixtures.vehicleItem(1)).click()
        device.waitForObject(BenchmarkTags.RECORD_LIST)
    }

    private fun UiDevice.waitForObject(resourceName: String) =
        wait(Until.findObject(By.res(resourceName)), TIMEOUT_MS) ?: error("Timed out waiting for $resourceName")

    companion object {
        private const val ITERATIONS = 5
        private const val SCROLL_REPEAT_COUNT = 2
        private const val TIMEOUT_MS = 30_000L
    }
}


Input:
*** Begin Patch
*** Update File: /home/Messier82/projects/service-book/macrobenchmark/src/main/java/com/yorvana/macrobenchmark/YorvanaBenchmark.kt
@@
             startActivityAndWait()
-            device.waitForObject(By.res(BenchmarkTags.VEHICLE_LIST))
+            device.waitForObject(BenchmarkTags.VEHICLE_LIST)
         }
@@
-            val list = device.waitForObject(By.res(BenchmarkTags.VEHICLE_LIST))
+            val list = device.waitForObject(BenchmarkTags.VEHICLE_LIST)
@@
-            val list = device.waitForObject(By.res(BenchmarkTags.RECORD_LIST))
+            val list = device.waitForObject(BenchmarkTags.RECORD_LIST)
@@
-            device.waitForObject(By.res(BenchmarkFixtures.vehicleItem(1))).click()
-            device.waitForObject(By.res(BenchmarkTags.RECORD_LIST))
-            device.waitForObject(By.res(BenchmarkFixtures.recordItem(1))).click()
+            device.waitForObject(BenchmarkFixtures.vehicleItem(1)).click()
+            device.waitForObject(BenchmarkTags.RECORD_LIST)
+            device.waitForObject(BenchmarkFixtures.recordItem(1)).click()
             device.waitForIdle()
             device.pressBack()
-            device.waitForObject(By.res(BenchmarkTags.RECORD_LIST))
+            device.waitForObject(BenchmarkTags.RECORD_LIST)
             device.pressBack()
-            device.waitForObject(By.res(BenchmarkTags.VEHICLE_LIST))
+            device.waitForObject(BenchmarkTags.VEHICLE_LIST)
         }

-    private fun MacrobenchmarkRule.Scope.seedVault(fixture: String) {
-        device.pressHome()
-        device.executeShellCommand(
-            "am start -W -a ${BenchmarkFixtures.SEED_ACTION} " +
-                "--es ${BenchmarkFixtures.EXTRA_FIXTURE} $fixture ${BenchmarkFixtures.PACKAGE_NAME}",
-        )
-        device.waitForObject(BenchmarkTags.VEHICLE_LIST)
-    }
-
*** End Patch


@@ -36,3 +36,3 @@
             startActivityAndWait()
-            device.waitForObject(By.res(BenchmarkTags.VEHICLE_LIST))
+            device.waitForObject(BenchmarkTags.VEHICLE_LIST)
         }
@@ -50,3 +50,3 @@
         ) {
-            val list = device.waitForObject(By.res(BenchmarkTags.VEHICLE_LIST))
+            val list = device.waitForObject(BenchmarkTags.VEHICLE_LIST)
             repeat(SCROLL_REPEAT_COUNT) {
@@ -71,3 +71,3 @@
         ) {
-            val list = device.waitForObject(By.res(BenchmarkTags.RECORD_LIST))
+            val list = device.waitForObject(BenchmarkTags.RECORD_LIST)
             repeat(SCROLL_REPEAT_COUNT) {
@@ -91,20 +91,11 @@
         ) {
-            device.waitForObject(By.res(BenchmarkFixtures.vehicleItem(1))).click()
-            device.waitForObject(By.res(BenchmarkTags.RECORD_LIST))
-            device.waitForObject(By.res(BenchmarkFixtures.recordItem(1))).click()
+            device.waitForObject(BenchmarkFixtures.vehicleItem(1)).click()
+            device.waitForObject(BenchmarkTags.RECORD_LIST)
+            device.waitForObject(BenchmarkFixtures.recordItem(1)).click()
             device.waitForIdle()
             device.pressBack()
-            device.waitForObject(By.res(BenchmarkTags.RECORD_LIST))
+            device.waitForObject(BenchmarkTags.RECORD_LIST)
             device.pressBack()
-            device.waitForObject(By.res(BenchmarkTags.VEHICLE_LIST))
+            device.waitForObject(BenchmarkTags.VEHICLE_LIST)
         }
-
-    private fun MacrobenchmarkRule.Scope.seedVault(fixture: String) {
-        device.pressHome()
-        device.executeShellCommand(
-            "am start -W -a ${BenchmarkFixtures.SEED_ACTION} " +
-                "--es ${BenchmarkFixtures.EXTRA_FIXTURE} $fixture ${BenchmarkFixtures.PACKAGE_NAME}",
-        )
-        device.waitForObject(BenchmarkTags.VEHICLE_LIST)
-    }


Output:
Exit code: 0
Wall time: 0.8 seconds
Output:
Success. Updated the following files:
M /home/Messier82/projects/service-book/macrobenchmark/src/main/java/com/yorvana/macrobenchmark/YorvanaBenchmark.kt


The new source files are in place. I’m running Gradle discovery/compile now; this usually shakes out AGP DSL details for new Android test modules before we spend time on device execution.


Output:
Chunk ID: 037f0a
Wall time: 1.0011 seconds
Process running with session ID 65620
Original token count: 13
Output:
Picked up JAVA_TOOL_OPTIONS: -Xlog:os+container=off


Output:
Chunk ID: f31926
Wall time: 5.0015 seconds
Process running with session ID 65620
Original token count: 0
Output:


Output:
Chunk ID: 67166c
Wall time: 5.0006 seconds
Process running with session ID 65620
Original token count: 32
Output:

> Configure project :app
WARNING: The option setting 'android.testoptions.manageddevices.emulator.gpu=host' is experimental.


Output:
Chunk ID: f9b2e4
Wall time: 0.0000 seconds
Process exited with code 1
Original token count: 383
Output:

[Incubating] Problems report is available at: file:///home/Messier82/projects/service-book/build/reports/problems/problems-report.html

FAILURE: Build failed with an exception.

* Where:
Build file '/home/Messier82/projects/service-book/macrobenchmark/build.gradle.kts' line: 1

* What went wrong:
An exception occurred applying plugin request [id: 'org.jetbrains.kotlin.android', version: '2.3.21']
> Failed to apply plugin 'org.jetbrains.kotlin.android'.
   > ⛔ Failed to apply plugin 'org.jetbrains.kotlin.android'
     The 'org.jetbrains.kotlin.android' plugin is no longer required for Kotlin support since AGP 9.0.
     Solution: Remove the 'org.jetbrains.kotlin.android' plugin from this project's build file: macrobenchmark/build.gradle.kts.
     See https://kotl.in/gradle/agp-built-in-kotlin for more details.
      > java.lang.Throwable (no error message)

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights from a Build Scan (powered by Develocity).
> Get more help at https://help.gradle.org.

Deprecated Gradle features were used in this build, making it incompatible with Gradle 10.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/9.5.1/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD FAILED in 17s


AGP 9 has built-in Kotlin support here, so applying `org.jetbrains.kotlin.android` is rejected. I’m removing that plugin from the new module/root catalog changes and keeping the module aligned with the existing app build style.


Input:
*** Begin Patch
*** Update File: /home/Messier82/projects/service-book/gradle/libs.versions.toml
@@
 android-application = { id = "com.android.application", version.ref = "agp" }
 android-test = { id = "com.android.test", version.ref = "agp" }
-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
 kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
*** Update File: /home/Messier82/projects/service-book/build.gradle.kts
@@
     alias(libs.plugins.android.application) apply false
     alias(libs.plugins.android.test) apply false
-    alias(libs.plugins.kotlin.android) apply false
     alias(libs.plugins.kotlin.serialization) apply false
*** Update File: /home/Messier82/projects/service-book/macrobenchmark/build.gradle.kts
@@
 plugins {
     alias(libs.plugins.android.test)
-    alias(libs.plugins.kotlin.android)
 }
*** End Patch


Success. Updated the following files:
M /home/Messier82/projects/service-book/gradle/libs.versions.toml
M /home/Messier82/projects/service-book/build.gradle.kts
M /home/Messier82/projects/service-book/macrobenchmark/build.gradle.kts

@@ -3,3 +3,2 @@
     alias(libs.plugins.android.test) apply false
-    alias(libs.plugins.kotlin.android) apply false
     alias(libs.plugins.kotlin.serialization) apply false

@@ -67,3 +67,2 @@
 android-test = { id = "com.android.test", version.ref = "agp" }
-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
 kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

@@ -2,3 +2,2 @@
     alias(libs.plugins.android.test)
-    alias(libs.plugins.kotlin.android)
 }


Output:
Exit code: 0
Wall time: 2.6 seconds
Output:
Success. Updated the following files:
M /home/Messier82/projects/service-book/gradle/libs.versions.toml
M /home/Messier82/projects/service-book/build.gradle.kts
M /home/Messier82/projects/service-book/macrobenchmark/build.gradle.kts


Output:
Chunk ID: 911195
Wall time: 1.0002 seconds
Process running with session ID 8318
Original token count: 13
Output:
Picked up JAVA_TOOL_OPTIONS: -Xlog:os+container=off