perf: 添加性能追踪标记并改进基线配置文件生成器

This commit is contained in:
xfy 2026-05-27 16:06:05 +08:00
parent 4de00e35dc
commit 281abcf66b
7 changed files with 146 additions and 107 deletions

View File

@ -27,10 +27,17 @@
trace 中包含自定义标记:
- `MonthView:Compose` — 月视图重组
- `CalendarPagerArea` — 日历分页器区域
- `CalendarPager:Page:*` — 月视图单页重组
- `CalendarMonthPage:*` — 月页面数据计算(含折叠动画准备)
- `WeekPager:Page` — 周视图单页重组
- `YearView:Compose` — 年视图重组
- `YearGridView:*` — 年视图网格组合(首帧耗时关键指标)
- `generateMiniMonthDays:*` — 月份网格计算
- `VM:collapseProgress` — 折叠动画
- `MonthView→YearView` / `YearView→MonthView` — 视图切换
- `YearView:SelectMonth` — 年视图选月
- `getMonthDays:*` — ViewModel 月份网格计算
- `VM:collapseProgress:*` — 折叠动画拖拽onDrag/onDragEnd/onExpandDrag/onExpandDragEnd
## Baseline Profile

View File

@ -18617,4 +18617,17 @@ SPLplus/rua/project/ui/WeekdayHeaderKt;->WeekdayHeader$lambda$0$0(Landroidx/comp
SPLplus/rua/project/ui/WeekdayHeaderKt;->WeekdayHeader(Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
Lplus/rua/project/ui/WeekdayHeaderKt$$ExternalSyntheticLambda0;
SPLplus/rua/project/ui/WeekdayHeaderKt$$ExternalSyntheticLambda0;-><init>()V
SPLplus/rua/project/ui/WeekdayHeaderKt$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
SPLplus/rua/project/ui/WeekdayHeaderKt$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
Lplus/rua/project/ui/YearGridViewKt;
HPLplus/rua/project/ui/YearGridViewKt;->YearGridView(IILkotlinx/datetime/LocalDate;Lkotlin/jvm/functions/Function1;Landroidx/compose/animation/SharedTransitionScope;Landroidx/compose/animation/AnimatedVisibilityScope;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
HPLplus/rua/project/ui/YearGridViewKt;->generateMiniMonthDays(II)Ljava/util/List;
Lplus/rua/project/ui/YearGridViewKt;->MiniMonth(IIZLkotlinx/datetime/LocalDate;Ljava/util/List;Lplus/rua/project/ui/MiniMonthColors;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
Lplus/rua/project/ui/YearGridViewKt;->YearHeader(IILkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
Lplus/rua/project/ui/MiniMonthColors;
HPLplus/rua/project/CalendarViewModel;->toggleYearView()V
HPLplus/rua/project/CalendarViewModel;->selectMonthFromYearView(I)V
HPLplus/rua/project/CalendarViewModel;->onDrag(F)V
HPLplus/rua/project/CalendarViewModel;->onDragEnd()V
HPLplus/rua/project/CalendarViewModel;->onExpandDrag(F)V
HPLplus/rua/project/CalendarViewModel;->onExpandDragEnd()V
HPLplus/rua/project/CalendarViewModel;->getMonthDays(II)Ljava/util/List;

View File

@ -280,7 +280,9 @@ class CalendarViewModel(
* @param delta 拖拽增量已归一化到 [0,1] 区间
*/
fun onDrag(delta: Float) {
composeTraceBeginSection("VM:collapseProgress:onDrag")
_collapseProgress.value = (_collapseProgress.value + delta).coerceIn(0f, 1f)
composeTraceEndSection()
}
/**
@ -289,6 +291,7 @@ class CalendarViewModel(
* 拖拽超过阈值时自动折叠到周视图否则回弹到月视图
*/
fun onDragEnd() {
composeTraceBeginSection("VM:collapseProgress:onDragEnd")
val progress = _collapseProgress.value
if (progress > COLLAPSE_THRESHOLD) {
_isCollapsed.value = true
@ -297,6 +300,7 @@ class CalendarViewModel(
_isCollapsed.value = false
_collapseProgress.value = 0f
}
composeTraceEndSection()
}
/**
@ -305,9 +309,11 @@ class CalendarViewModel(
* @param delta 拖拽增量已归一化到 [0,1] 区间
*/
fun onExpandDrag(delta: Float) {
composeTraceBeginSection("VM:collapseProgress:onExpandDrag")
val old = _collapseProgress.value
_collapseProgress.value = (_collapseProgress.value + delta).coerceIn(0f, 1f)
logd(TAG_VM, "onExpandDrag: delta=$delta old=$old new=${_collapseProgress.value}")
composeTraceEndSection()
}
/**
@ -316,6 +322,7 @@ class CalendarViewModel(
* 下拉超过阈值时自动展开到月视图否则回弹到周视图
*/
fun onExpandDragEnd() {
composeTraceBeginSection("VM:collapseProgress:onExpandDragEnd")
val progress = _collapseProgress.value
val result = if (progress < (1 - COLLAPSE_THRESHOLD)) {
_isCollapsed.value = false
@ -327,6 +334,7 @@ class CalendarViewModel(
"COLLAPSED (bounce back)"
}
logd(TAG_VM, "onExpandDragEnd: progress=$progress threshold=${1 - COLLAPSE_THRESHOLD} result=$result")
composeTraceEndSection()
}
/**

View File

@ -21,12 +21,10 @@ import plus.rua.project.getWebpUri
*/
private val WEBP_FILES = (1..152).map { "${it.toString().padStart(3, '0')}.webp" }
private const val REPEAT_COUNT = 2
/**
* 显示动画 WebP 图片切换日期时随机选择一个
*
* 动画播放 3 1 + [REPEAT_COUNT]后停止避免持续解码导致的帧丢失
* 动画无限循环播放
*
* @param modifier 应用于图片的 Modifier
* @param contentDescription 无障碍描述
@ -55,7 +53,7 @@ fun AnimatedGif(
}
val state = rememberAsyncImageState(
options = remember { ImageOptions { repeatCount(REPEAT_COUNT) } }
options = remember { ImageOptions { repeatCount(-1) } }
)
AsyncImage(

View File

@ -21,6 +21,8 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import plus.rua.project.composeTraceBeginSection
import plus.rua.project.composeTraceEndSection
import plus.rua.project.util.logd
import kotlinx.datetime.DatePeriod
import kotlinx.datetime.LocalDate
@ -68,6 +70,7 @@ fun CalendarMonthPage(
onRowHeightMeasured: ((Int) -> Unit)? = null,
modifier: Modifier = Modifier
) {
composeTraceBeginSection("CalendarMonthPage:$year-$month")
val days = remember(year, month) {
generateMonthDays(year, month)
}
@ -129,6 +132,7 @@ fun CalendarMonthPage(
else Modifier
)
) {
composeTraceEndSection()
weeks.forEachIndexed { weekIndex, week ->
key(weekIndex) {
WeekRow(

View File

@ -15,6 +15,8 @@ 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.composeTraceBeginSection
import plus.rua.project.composeTraceEndSection
import plus.rua.project.util.logd
import androidx.compose.ui.draw.alpha
import kotlinx.coroutines.flow.drop
@ -105,6 +107,7 @@ fun CalendarPager(
if (isCurrentPage) {
logd("AnimLog", "[CalendarPager] Compose page=$page ($year-$month) alpha=$alpha pageOffset=$pageOffset")
}
composeTraceBeginSection("CalendarPager:Page:$year-$month")
CalendarMonthPage(
year = year,
month = month,
@ -137,5 +140,6 @@ fun CalendarPager(
onRowHeightMeasured = onRowHeightMeasured,
modifier = Modifier.alpha(alpha)
)
composeTraceEndSection()
}
}

View File

@ -1,10 +1,13 @@
package plus.rua.project.baseline
import android.util.Log
import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Direction
import androidx.test.uiautomator.Until
import org.junit.Assert.assertNotNull
import org.junit.Assert.fail
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@ -44,95 +47,95 @@ class BaselineProfileGenerator {
packageName = "plus.rua.project",
includeInStartupProfile = true,
profileBlock = {
val TAG = "BaselineProfile"
// 1. 冷启动:从 launcher 启动应用
// 注:使用 shell command 绕过 startActivityAndWait因为模拟器的 software
// renderer 不支持 gfxinfo framestats会导致 amStartAndWait 超时。
pressHome()
device.executeShellCommand(
"am start -W -n plus.rua.project/.MainActivity"
)
device.waitForIdle()
// 3. 模拟用户交互:展开 FAB 菜单
// 2. 展开 FAB 菜单,等待菜单项出现
val fab = device.findObject(By.res("plus.rua.project:id/fab_menu"))
if (fab != null) {
fab.click()
device.waitForIdle()
}
assertNotNull("FAB 按钮必须存在", fab)
fab!!.click()
val yearViewItem = device.wait(Until.findObject(By.text("年视图")), 3000)
Log.d(TAG, "FAB 菜单展开: yearViewItem=${yearViewItem != null}")
// 4. 切换到年视图(覆盖 YearGridView、YearHeader、MiniMonth 路径)
val yearViewButton = device.findObject(By.text("年视图"))
if (yearViewButton != null) {
yearViewButton.click()
device.waitForIdle()
}
// 3. 切换到年视图(覆盖 YearGridView、YearHeader、MiniMonth 路径)
assertNotNull("年视图菜单项必须出现", yearViewItem)
yearViewItem!!.click()
val yearGrid = device.wait(Until.findObject(By.res("plus.rua.project:id/year_grid")), 3000)
Log.d(TAG, "年视图加载: yearGrid=${yearGrid != null}")
assertNotNull("YearGridView 必须加载", yearGrid)
device.waitForIdle()
// 5. 在年视图中滑动到不同年份(覆盖动画和分页路径)
val yearGrid = device.findObject(By.res("plus.rua.project:id/year_grid"))
if (yearGrid != null) {
yearGrid.swipe(Direction.UP, 0.5f)
device.waitForIdle()
yearGrid.swipe(Direction.DOWN, 0.5f)
device.waitForIdle()
}
// 4. 在年视图中滑动到不同年份(覆盖动画和分页路径)
yearGrid!!.swipe(Direction.UP, 0.5f)
device.waitForIdle()
yearGrid.swipe(Direction.DOWN, 0.5f)
device.waitForIdle()
// 6. 切换回月视图
val monthViewButton = device.findObject(By.text("月视图"))
if (monthViewButton != null) {
monthViewButton.click()
device.waitForIdle()
}
// 5. 展开 FAB 并切换回月视图
val fabForMonth = device.findObject(By.res("plus.rua.project:id/fab_menu"))
assertNotNull("FAB 按钮必须存在(返回月视图)", fabForMonth)
fabForMonth!!.click()
val monthViewItem = device.wait(Until.findObject(By.text("月视图")), 3000)
Log.d(TAG, "FAB 菜单展开: monthViewItem=${monthViewItem != null}")
assertNotNull("月视图菜单项必须出现", monthViewItem)
monthViewItem!!.click()
device.waitForIdle()
// 7. 点击某一天(覆盖 DayCell 点击路径 + 底部卡片展开)
// 6. 点击某一天(覆盖 DayCell 点击路径 + 底部卡片展开)
val todayCell = device.findObject(By.descContains("今天"))
?: device.findObject(By.text("21"))
if (todayCell != null) {
todayCell.click()
device.waitForIdle()
}
assertNotNull("DayCell 必须可点击", todayCell)
todayCell!!.click()
device.waitForIdle()
// 8. 拖拽 BottomCard 触发月视图↔周视图折叠/展开
// 7. 拖拽 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()
// 向下拖拽 → 展开到月视图
device.drag(centerX, centerY - dragDistance, centerX, centerY, 20)
device.waitForIdle()
}
assertNotNull("BottomCard 必须存在", bottomCard)
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()
// 向下拖拽 → 展开到月视图
device.drag(centerX, centerY - dragDistance, centerX, centerY, 20)
device.waitForIdle()
// 9. 展开 FAB 并进入工具页面
val fabMenu = device.findObject(By.res("plus.rua.project:id/fab_menu"))
if (fabMenu != null) {
fabMenu.click()
device.waitForIdle()
}
val toolsButton = device.findObject(By.text("工具"))
if (toolsButton != null) {
toolsButton.click()
device.waitForIdle()
}
// 8. 展开 FAB 并进入工具页面
val fabForTools = device.findObject(By.res("plus.rua.project:id/fab_menu"))
assertNotNull("FAB 按钮必须存在(工具页)", fabForTools)
fabForTools!!.click()
val toolsButton = device.wait(Until.findObject(By.text("工具")), 3000)
Log.d(TAG, "FAB 菜单展开: toolsButton=${toolsButton != null}")
assertNotNull("工具菜单项必须出现", toolsButton)
toolsButton!!.click()
device.waitForIdle()
// 10. 进入日期检查器(覆盖 DateCheckerScreen
val dateCheckerEntry = device.findObject(By.res("plus.rua.project:id/tool_date_checker"))
if (dateCheckerEntry != null) {
dateCheckerEntry.click()
device.waitForIdle()
}
// 9. 进入日期检查器(覆盖 DateCheckerScreen
val dateCheckerEntry = device.wait(
Until.findObject(By.res("plus.rua.project:id/tool_date_checker")), 3000
)
assertNotNull("日期检查器入口必须存在", dateCheckerEntry)
dateCheckerEntry!!.click()
device.waitForIdle()
// 11. 点击日历图标打开 DatePickerDialog覆盖 DatePicker
val datePickerBtn = device.findObject(By.res("plus.rua.project:id/date_picker_button"))
// 10. 点击日历图标打开 DatePickerDialog覆盖 DatePicker
val datePickerBtn = device.wait(
Until.findObject(By.res("plus.rua.project:id/date_picker_button")), 3000
)
if (datePickerBtn != null) {
datePickerBtn.click()
device.waitForIdle()
}
// 12. 等待 DatePickerDialog 并点击确定
// 11. 等待 DatePickerDialog 并点击确定
device.wait(Until.findObject(By.text("确定")), 2000)
val confirmBtn = device.findObject(By.text("确定"))
if (confirmBtn != null) {
@ -140,54 +143,56 @@ class BaselineProfileGenerator {
device.waitForIdle()
}
// 13. 点击 FAB 添加新行(覆盖 FAB + LazyColumn items 重组)
// 12. 点击 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()
device.waitForIdle()
}
// 11. 进入开源许可页面(覆盖 LicensesScreen
val licensesButton = device.findObject(By.text("开源许可"))
if (licensesButton != null) {
licensesButton.click()
device.waitForIdle()
}
// 12. 等待许可列表加载
device.wait(Until.findObject(By.textContains("Apache")), 2000)
// 13. 返回关于页
// 13. 返回工具页
device.pressBack()
device.waitForIdle()
// 14. 返回主界面
device.pressBack()
device.waitForIdle()
// 15. 左右滑动切换月份(覆盖 CalendarPager 翻页)
val calendarPager = device.findObject(By.res("plus.rua.project:id/calendar_pager"))
assertNotNull("CalendarPager 必须存在", calendarPager)
calendarPager!!.swipe(Direction.LEFT, 0.5f)
device.waitForIdle()
calendarPager.swipe(Direction.RIGHT, 0.5f)
device.waitForIdle()
// 16. 进入关于页面(覆盖 AboutScreen + AnimatedGif
val fabForAbout = device.findObject(By.res("plus.rua.project:id/fab_menu"))
assertNotNull("FAB 按钮必须存在(关于页)", fabForAbout)
fabForAbout!!.click()
val aboutButton = device.wait(Until.findObject(By.text("关于")), 3000)
assertNotNull("关于菜单项必须出现", aboutButton)
aboutButton!!.click()
device.waitForIdle()
// 17. 进入开源许可页面(覆盖 LicensesScreen
val licensesButton = device.wait(Until.findObject(By.text("开源许可")), 3000)
assertNotNull("开源许可按钮必须存在", licensesButton)
licensesButton!!.click()
device.waitForIdle()
// 18. 等待许可列表加载
device.wait(Until.findObject(By.textContains("Apache")), 2000)
// 19. 返回关于页
device.pressBack()
device.waitForIdle()
// 20. 返回主界面
device.pressBack()
device.waitForIdle()
Log.d(TAG, "Baseline profile 生成完成,所有路径已覆盖")
}
)
}