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:
parent
9a0222b4a2
commit
4eadc479eb
@ -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
|
||||
|
||||
@ -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) {
|
||||
// 折叠状态:下拉恢复到月视图
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
},
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 月历网格
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user