feat: 添加 testTag 基础设施,扩展 Baseline Profile 覆盖路径

为 CalendarPager、BottomCard、FAB、工具页、日期检查器等关键 UI 元素添加 testTag,
启用 testTagsAsResourceId 支持 UI Automator 通过 resource-id 定位。
BaselineProfileGenerator 使用 testTag 选择器重写,覆盖工具页、日期检查器、
DatePicker 等更多导航路径。清理 DEVELOPMENT.md 移除已过时的性能瓶颈描述。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-27 10:03:16 +08:00
parent 9a0222b4a2
commit 4eadc479eb
9 changed files with 106 additions and 51 deletions

View File

@ -17,47 +17,21 @@
输出文件保存在 `logs/` 目录:
| 文件 | 说明 |
|------|------|
| 文件 | 说明 |
| ------------------------ | ----------------------------------------------- |
| `trace_*.perfetto-trace` | Perfetto trace在 https://ui.perfetto.dev 打开 |
| `framestats_*.txt` | GPU 帧统计 |
| `meminfo_*.txt` | 内存快照 |
| `report_*.md` | 追踪报告摘要 |
| `framestats_*.txt` | GPU 帧统计 |
| `meminfo_*.txt` | 内存快照 |
| `report_*.md` | 追踪报告摘要 |
trace 中包含自定义标记:
- `MonthView:Compose` — 月视图重组
- `YearView:Compose` — 年视图重组
- `YearGridView:*` — 年视图网格组合(首帧耗时关键指标)
- `generateMiniMonthDays:*` — 月份网格计算
- `VM:collapseProgress` — 折叠动画
### 已知性能瓶颈
#### AnimatedGif 持续解码
`AnimatedGif` 中的 250×250 WebP 动画在 `repeatCount` 未限制时会以 11-14 FPS 无限循环解码,
持续消耗 CPU/GPU。已通过 `ImageOptions { repeatCount(2) }` 限制播放 3 次后停止。
#### YearGridView 首帧 168ms
切换到年视图时,`YearGridView` 首次组合需要:
- 创建 12 个 MiniMonth composable`sharedElement` + `clickable` + `Canvas` 等 modifier 节点)
- 124 次文本测量93 日期 + 24 标题 + 7 星期)
- `SharedTransitionLayout` 注册 12 个共享元素
文本测量已通过 `produceState` 延迟到第二帧执行,首帧 Canvas 渲染为空。
剩余 ~140ms 是 12 个 composable 节点创建 + layout 的 Compose 运行时开销,
需要通过 **Baseline Profile** 预编译相关类来优化。
确保 `macrobenchmark``BaselineProfileGenerator` 覆盖以下路径:
- 冷启动 → FAB → 年视图(触发 YearGridView 首次组合)
- 年视图 → 点击任意月份返回月视图(触发 sharedElement 转场)
#### CalendarPager 预加载
`beyondViewportPageCount` 已设为 0避免翻页时预加载相邻月页导致的帧丢失。
如需恢复预加载,注意 `compose:lazy:prefetch` 可能产生 400-700ms 卡顿。
## Baseline Profile
```bash

View File

@ -23,6 +23,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.contentDescription
@ -84,6 +85,7 @@ fun BottomCard(
Surface(
modifier = modifier
.fillMaxWidth()
.testTag("bottom_card")
.pointerInput(isCollapsed) {
if (isCollapsed) {
// 折叠状态:下拉恢复到月视图

View File

@ -61,6 +61,9 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
@ -177,6 +180,7 @@ fun CalendarMonthView(
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.statusBarsPadding()
.semantics { testTagsAsResourceId = true }
.onSizeChanged { size ->
screenWidthPx = size.width
}
@ -377,7 +381,8 @@ fun CalendarMonthView(
onClick = { isMenuExpanded = !isMenuExpanded },
modifier = Modifier
.align(Alignment.BottomStart)
.padding(start = 24.dp, bottom = 32.dp),
.padding(start = 24.dp, bottom = 32.dp)
.testTag("fab_menu"),
shape = CircleShape,
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer

View File

@ -14,6 +14,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import plus.rua.project.util.logd
import androidx.compose.ui.draw.alpha
import kotlinx.coroutines.flow.drop
@ -91,7 +92,7 @@ fun CalendarPager(
state = pagerState,
beyondViewportPageCount = 0,
flingBehavior = PagerDefaults.flingBehavior(state = pagerState),
modifier = modifier
modifier = modifier.testTag("calendar_pager")
) { page ->
val pageOffset = abs(currentPageOffsetFraction)
val isCurrentPage = page == currentPage

View File

@ -45,6 +45,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.input.ImeAction
@ -97,6 +100,7 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
var datePickerTarget by remember { mutableStateOf<DatePickerTarget?>(null) }
Scaffold(
modifier = modifier.semantics { testTagsAsResourceId = true },
topBar = {
TopAppBar(
title = { Text("日期检查器") },
@ -113,6 +117,7 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
rows = rows + ExpiryRow(nextId, null)
nextId++
},
modifier = Modifier.testTag("date_checker_fab"),
shape = CircleShape,
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
@ -120,7 +125,6 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
PlusIcon(color = MaterialTheme.colorScheme.onPrimaryContainer)
}
},
modifier = modifier
) { innerPadding ->
Column(
modifier = Modifier
@ -261,7 +265,10 @@ private fun ProductionDateField(
imeAction = ImeAction.Done
),
trailingIcon = {
IconButton(onClick = onShowDatePicker) {
IconButton(
onClick = onShowDatePicker,
modifier = Modifier.testTag("date_picker_button")
) {
CalendarIcon(color = MaterialTheme.colorScheme.onSurfaceVariant)
}
},

View File

@ -243,7 +243,11 @@ private fun DayCellImpl(
.fillMaxSize()
.semantics {
@Suppress("DEPRECATION")
contentDescription = "${date.year}${date.monthNumber}${date.day}"
contentDescription = if (isToday) {
"今天 ${date.year}${date.monthNumber}${date.day}"
} else {
"${date.year}${date.monthNumber}${date.day}"
}
}
.clip(CircleShape)
.drawBehind {

View File

@ -22,6 +22,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.unit.dp
/**
@ -39,6 +42,7 @@ fun ToolsScreen(
modifier: Modifier = Modifier
) {
Scaffold(
modifier = modifier.semantics { testTagsAsResourceId = true },
topBar = {
TopAppBar(
title = { Text("工具") },
@ -66,7 +70,6 @@ fun ToolsScreen(
}
)
},
modifier = modifier
) { innerPadding ->
Column(
modifier = Modifier
@ -76,7 +79,8 @@ fun ToolsScreen(
) {
ToolItem(
title = "日期检查器",
onClick = onNavigateToDateChecker
onClick = onNavigateToDateChecker,
modifier = Modifier.testTag("tool_date_checker")
)
}
}

View File

@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.platform.testTag
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@ -155,7 +156,9 @@ fun YearGridView(
}
Column(
modifier = modifier.fillMaxSize(),
modifier = modifier
.fillMaxSize()
.testTag("year_grid"),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 4×3 月历网格

View File

@ -91,25 +91,80 @@ class BaselineProfileGenerator {
device.waitForIdle()
}
// 8. 上下滑动触发月视图↔周视图切换(覆盖 BottomCard 拖拽 + collapse 动画)
val calendarArea = device.findObject(By.res("plus.rua.project:id/calendar_pager"))
?: device.findObject(By.textContains("2026"))
if (calendarArea != null) {
calendarArea.swipe(Direction.UP, 0.5f)
// 8. 拖拽 BottomCard 触发月视图↔周视图折叠/展开
val bottomCard = device.findObject(By.res("plus.rua.project:id/bottom_card"))
if (bottomCard != null) {
val bounds = bottomCard.visibleBounds
val centerX = bounds.centerX()
val centerY = bounds.centerY()
val dragDistance = (bounds.height() * 0.4).toInt()
// 向上拖拽 → 折叠到周视图
device.drag(centerX, centerY, centerX, centerY - dragDistance, 20)
device.waitForIdle()
calendarArea.swipe(Direction.DOWN, 0.5f)
// 向下拖拽 → 展开到月视图
device.drag(centerX, centerY - dragDistance, centerX, centerY, 20)
device.waitForIdle()
}
// 9. 左右滑动切换月份(覆盖 CalendarPager 翻页)
if (calendarArea != null) {
calendarArea.swipe(Direction.LEFT, 0.5f)
// 9. 展开 FAB 并进入工具页面
val fabMenu = device.findObject(By.res("plus.rua.project:id/fab_menu"))
if (fabMenu != null) {
fabMenu.click()
device.waitForIdle()
calendarArea.swipe(Direction.RIGHT, 0.5f)
}
val toolsButton = device.findObject(By.text("工具"))
if (toolsButton != null) {
toolsButton.click()
device.waitForIdle()
}
// 10. 进入关于页面(覆盖 AboutScreen + AnimatedGif
// 10. 进入日期检查器(覆盖 DateCheckerScreen
val dateCheckerEntry = device.findObject(By.res("plus.rua.project:id/tool_date_checker"))
if (dateCheckerEntry != null) {
dateCheckerEntry.click()
device.waitForIdle()
}
// 11. 点击日历图标打开 DatePickerDialog覆盖 DatePicker
val datePickerBtn = device.findObject(By.res("plus.rua.project:id/date_picker_button"))
if (datePickerBtn != null) {
datePickerBtn.click()
device.waitForIdle()
}
// 12. 等待 DatePickerDialog 并点击确定
device.wait(Until.findObject(By.text("确定")), 2000)
val confirmBtn = device.findObject(By.text("确定"))
if (confirmBtn != null) {
confirmBtn.click()
device.waitForIdle()
}
// 13. 点击 FAB 添加新行(覆盖 FAB + LazyColumn items 重组)
val dateCheckerFab = device.findObject(By.res("plus.rua.project:id/date_checker_fab"))
if (dateCheckerFab != null) {
dateCheckerFab.click()
device.waitForIdle()
}
// 14. 返回工具页
device.pressBack()
device.waitForIdle()
// 15. 返回主界面
device.pressBack()
device.waitForIdle()
// 16. 左右滑动切换月份(覆盖 CalendarPager 翻页)
val calendarPager = device.findObject(By.res("plus.rua.project:id/calendar_pager"))
if (calendarPager != null) {
calendarPager.swipe(Direction.LEFT, 0.5f)
device.waitForIdle()
calendarPager.swipe(Direction.RIGHT, 0.5f)
device.waitForIdle()
}
// 17. 进入关于页面(覆盖 AboutScreen + AnimatedGif
val aboutButton = device.findObject(By.text("关于"))
if (aboutButton != null) {
aboutButton.click()