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
@ -18,46 +18,20 @@
|
|||||||
输出文件保存在 `logs/` 目录:
|
输出文件保存在 `logs/` 目录:
|
||||||
|
|
||||||
| 文件 | 说明 |
|
| 文件 | 说明 |
|
||||||
|------|------|
|
| ------------------------ | ----------------------------------------------- |
|
||||||
| `trace_*.perfetto-trace` | Perfetto trace,在 https://ui.perfetto.dev 打开 |
|
| `trace_*.perfetto-trace` | Perfetto trace,在 https://ui.perfetto.dev 打开 |
|
||||||
| `framestats_*.txt` | GPU 帧统计 |
|
| `framestats_*.txt` | GPU 帧统计 |
|
||||||
| `meminfo_*.txt` | 内存快照 |
|
| `meminfo_*.txt` | 内存快照 |
|
||||||
| `report_*.md` | 追踪报告摘要 |
|
| `report_*.md` | 追踪报告摘要 |
|
||||||
|
|
||||||
trace 中包含自定义标记:
|
trace 中包含自定义标记:
|
||||||
|
|
||||||
- `MonthView:Compose` — 月视图重组
|
- `MonthView:Compose` — 月视图重组
|
||||||
- `YearView:Compose` — 年视图重组
|
- `YearView:Compose` — 年视图重组
|
||||||
- `YearGridView:*` — 年视图网格组合(首帧耗时关键指标)
|
- `YearGridView:*` — 年视图网格组合(首帧耗时关键指标)
|
||||||
- `generateMiniMonthDays:*` — 月份网格计算
|
- `generateMiniMonthDays:*` — 月份网格计算
|
||||||
- `VM:collapseProgress` — 折叠动画
|
- `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
|
## Baseline Profile
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
@ -84,6 +85,7 @@ fun BottomCard(
|
|||||||
Surface(
|
Surface(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.testTag("bottom_card")
|
||||||
.pointerInput(isCollapsed) {
|
.pointerInput(isCollapsed) {
|
||||||
if (isCollapsed) {
|
if (isCollapsed) {
|
||||||
// 折叠状态:下拉恢复到月视图
|
// 折叠状态:下拉恢复到月视图
|
||||||
|
|||||||
@ -61,6 +61,9 @@ import androidx.compose.runtime.snapshotFlow
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
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.draw.clipToBounds
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@ -177,6 +180,7 @@ fun CalendarMonthView(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.background)
|
.background(MaterialTheme.colorScheme.background)
|
||||||
.statusBarsPadding()
|
.statusBarsPadding()
|
||||||
|
.semantics { testTagsAsResourceId = true }
|
||||||
.onSizeChanged { size ->
|
.onSizeChanged { size ->
|
||||||
screenWidthPx = size.width
|
screenWidthPx = size.width
|
||||||
}
|
}
|
||||||
@ -377,7 +381,8 @@ fun CalendarMonthView(
|
|||||||
onClick = { isMenuExpanded = !isMenuExpanded },
|
onClick = { isMenuExpanded = !isMenuExpanded },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomStart)
|
.align(Alignment.BottomStart)
|
||||||
.padding(start = 24.dp, bottom = 32.dp),
|
.padding(start = 24.dp, bottom = 32.dp)
|
||||||
|
.testTag("fab_menu"),
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import plus.rua.project.util.logd
|
import plus.rua.project.util.logd
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
@ -91,7 +92,7 @@ fun CalendarPager(
|
|||||||
state = pagerState,
|
state = pagerState,
|
||||||
beyondViewportPageCount = 0,
|
beyondViewportPageCount = 0,
|
||||||
flingBehavior = PagerDefaults.flingBehavior(state = pagerState),
|
flingBehavior = PagerDefaults.flingBehavior(state = pagerState),
|
||||||
modifier = modifier
|
modifier = modifier.testTag("calendar_pager")
|
||||||
) { page ->
|
) { page ->
|
||||||
val pageOffset = abs(currentPageOffsetFraction)
|
val pageOffset = abs(currentPageOffsetFraction)
|
||||||
val isCurrentPage = page == currentPage
|
val isCurrentPage = page == currentPage
|
||||||
|
|||||||
@ -45,6 +45,9 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.geometry.Offset
|
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.Color
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
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) }
|
var datePickerTarget by remember { mutableStateOf<DatePickerTarget?>(null) }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
modifier = modifier.semantics { testTagsAsResourceId = true },
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text("日期检查器") },
|
title = { Text("日期检查器") },
|
||||||
@ -113,6 +117,7 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
|||||||
rows = rows + ExpiryRow(nextId, null)
|
rows = rows + ExpiryRow(nextId, null)
|
||||||
nextId++
|
nextId++
|
||||||
},
|
},
|
||||||
|
modifier = Modifier.testTag("date_checker_fab"),
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
@ -120,7 +125,6 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
|||||||
PlusIcon(color = MaterialTheme.colorScheme.onPrimaryContainer)
|
PlusIcon(color = MaterialTheme.colorScheme.onPrimaryContainer)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = modifier
|
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -261,7 +265,10 @@ private fun ProductionDateField(
|
|||||||
imeAction = ImeAction.Done
|
imeAction = ImeAction.Done
|
||||||
),
|
),
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
IconButton(onClick = onShowDatePicker) {
|
IconButton(
|
||||||
|
onClick = onShowDatePicker,
|
||||||
|
modifier = Modifier.testTag("date_picker_button")
|
||||||
|
) {
|
||||||
CalendarIcon(color = MaterialTheme.colorScheme.onSurfaceVariant)
|
CalendarIcon(color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -243,7 +243,11 @@ private fun DayCellImpl(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.semantics {
|
.semantics {
|
||||||
@Suppress("DEPRECATION")
|
@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)
|
.clip(CircleShape)
|
||||||
.drawBehind {
|
.drawBehind {
|
||||||
|
|||||||
@ -22,6 +22,9 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
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
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,6 +42,7 @@ fun ToolsScreen(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
modifier = modifier.semantics { testTagsAsResourceId = true },
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text("工具") },
|
title = { Text("工具") },
|
||||||
@ -66,7 +70,6 @@ fun ToolsScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = modifier
|
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -76,7 +79,8 @@ fun ToolsScreen(
|
|||||||
) {
|
) {
|
||||||
ToolItem(
|
ToolItem(
|
||||||
title = "日期检查器",
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@ -155,7 +156,9 @@ fun YearGridView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.testTag("year_grid"),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
// 4×3 月历网格
|
// 4×3 月历网格
|
||||||
|
|||||||
@ -91,25 +91,80 @@ class BaselineProfileGenerator {
|
|||||||
device.waitForIdle()
|
device.waitForIdle()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. 上下滑动触发月视图↔周视图切换(覆盖 BottomCard 拖拽 + collapse 动画)
|
// 8. 拖拽 BottomCard 触发月视图↔周视图折叠/展开
|
||||||
val calendarArea = device.findObject(By.res("plus.rua.project:id/calendar_pager"))
|
val bottomCard = device.findObject(By.res("plus.rua.project:id/bottom_card"))
|
||||||
?: device.findObject(By.textContains("2026"))
|
if (bottomCard != null) {
|
||||||
if (calendarArea != null) {
|
val bounds = bottomCard.visibleBounds
|
||||||
calendarArea.swipe(Direction.UP, 0.5f)
|
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()
|
device.waitForIdle()
|
||||||
calendarArea.swipe(Direction.DOWN, 0.5f)
|
// 向下拖拽 → 展开到月视图
|
||||||
|
device.drag(centerX, centerY - dragDistance, centerX, centerY, 20)
|
||||||
device.waitForIdle()
|
device.waitForIdle()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. 左右滑动切换月份(覆盖 CalendarPager 翻页)
|
// 9. 展开 FAB 并进入工具页面
|
||||||
if (calendarArea != null) {
|
val fabMenu = device.findObject(By.res("plus.rua.project:id/fab_menu"))
|
||||||
calendarArea.swipe(Direction.LEFT, 0.5f)
|
if (fabMenu != null) {
|
||||||
|
fabMenu.click()
|
||||||
device.waitForIdle()
|
device.waitForIdle()
|
||||||
calendarArea.swipe(Direction.RIGHT, 0.5f)
|
}
|
||||||
|
val toolsButton = device.findObject(By.text("工具"))
|
||||||
|
if (toolsButton != null) {
|
||||||
|
toolsButton.click()
|
||||||
device.waitForIdle()
|
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("关于"))
|
val aboutButton = device.findObject(By.text("关于"))
|
||||||
if (aboutButton != null) {
|
if (aboutButton != null) {
|
||||||
aboutButton.click()
|
aboutButton.click()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user