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 中包含自定义标记: trace 中包含自定义标记:
- `MonthView:Compose` — 月视图重组 - `MonthView:Compose` — 月视图重组
- `CalendarPagerArea` — 日历分页器区域
- `CalendarPager:Page:*` — 月视图单页重组
- `CalendarMonthPage:*` — 月页面数据计算(含折叠动画准备)
- `WeekPager:Page` — 周视图单页重组
- `YearView:Compose` — 年视图重组 - `YearView:Compose` — 年视图重组
- `YearGridView:*` — 年视图网格组合(首帧耗时关键指标) - `YearGridView:*` — 年视图网格组合(首帧耗时关键指标)
- `generateMiniMonthDays:*` — 月份网格计算 - `generateMiniMonthDays:*` — 月份网格计算
- `VM:collapseProgress` — 折叠动画 - `MonthView→YearView` / `YearView→MonthView` — 视图切换
- `YearView:SelectMonth` — 年视图选月
- `getMonthDays:*` — ViewModel 月份网格计算
- `VM:collapseProgress:*` — 折叠动画拖拽onDrag/onDragEnd/onExpandDrag/onExpandDragEnd
## Baseline Profile ## 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 SPLplus/rua/project/ui/WeekdayHeaderKt;->WeekdayHeader(Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
Lplus/rua/project/ui/WeekdayHeaderKt$$ExternalSyntheticLambda0; Lplus/rua/project/ui/WeekdayHeaderKt$$ExternalSyntheticLambda0;
SPLplus/rua/project/ui/WeekdayHeaderKt$$ExternalSyntheticLambda0;-><init>()V 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] 区间 * @param delta 拖拽增量已归一化到 [0,1] 区间
*/ */
fun onDrag(delta: Float) { fun onDrag(delta: Float) {
composeTraceBeginSection("VM:collapseProgress:onDrag")
_collapseProgress.value = (_collapseProgress.value + delta).coerceIn(0f, 1f) _collapseProgress.value = (_collapseProgress.value + delta).coerceIn(0f, 1f)
composeTraceEndSection()
} }
/** /**
@ -289,6 +291,7 @@ class CalendarViewModel(
* 拖拽超过阈值时自动折叠到周视图否则回弹到月视图 * 拖拽超过阈值时自动折叠到周视图否则回弹到月视图
*/ */
fun onDragEnd() { fun onDragEnd() {
composeTraceBeginSection("VM:collapseProgress:onDragEnd")
val progress = _collapseProgress.value val progress = _collapseProgress.value
if (progress > COLLAPSE_THRESHOLD) { if (progress > COLLAPSE_THRESHOLD) {
_isCollapsed.value = true _isCollapsed.value = true
@ -297,6 +300,7 @@ class CalendarViewModel(
_isCollapsed.value = false _isCollapsed.value = false
_collapseProgress.value = 0f _collapseProgress.value = 0f
} }
composeTraceEndSection()
} }
/** /**
@ -305,9 +309,11 @@ class CalendarViewModel(
* @param delta 拖拽增量已归一化到 [0,1] 区间 * @param delta 拖拽增量已归一化到 [0,1] 区间
*/ */
fun onExpandDrag(delta: Float) { fun onExpandDrag(delta: Float) {
composeTraceBeginSection("VM:collapseProgress:onExpandDrag")
val old = _collapseProgress.value val old = _collapseProgress.value
_collapseProgress.value = (_collapseProgress.value + delta).coerceIn(0f, 1f) _collapseProgress.value = (_collapseProgress.value + delta).coerceIn(0f, 1f)
logd(TAG_VM, "onExpandDrag: delta=$delta old=$old new=${_collapseProgress.value}") logd(TAG_VM, "onExpandDrag: delta=$delta old=$old new=${_collapseProgress.value}")
composeTraceEndSection()
} }
/** /**
@ -316,6 +322,7 @@ class CalendarViewModel(
* 下拉超过阈值时自动展开到月视图否则回弹到周视图 * 下拉超过阈值时自动展开到月视图否则回弹到周视图
*/ */
fun onExpandDragEnd() { fun onExpandDragEnd() {
composeTraceBeginSection("VM:collapseProgress:onExpandDragEnd")
val progress = _collapseProgress.value val progress = _collapseProgress.value
val result = if (progress < (1 - COLLAPSE_THRESHOLD)) { val result = if (progress < (1 - COLLAPSE_THRESHOLD)) {
_isCollapsed.value = false _isCollapsed.value = false
@ -327,6 +334,7 @@ class CalendarViewModel(
"COLLAPSED (bounce back)" "COLLAPSED (bounce back)"
} }
logd(TAG_VM, "onExpandDragEnd: progress=$progress threshold=${1 - COLLAPSE_THRESHOLD} result=$result") 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 val WEBP_FILES = (1..152).map { "${it.toString().padStart(3, '0')}.webp" }
private const val REPEAT_COUNT = 2
/** /**
* 显示动画 WebP 图片切换日期时随机选择一个 * 显示动画 WebP 图片切换日期时随机选择一个
* *
* 动画播放 3 1 + [REPEAT_COUNT]后停止避免持续解码导致的帧丢失 * 动画无限循环播放
* *
* @param modifier 应用于图片的 Modifier * @param modifier 应用于图片的 Modifier
* @param contentDescription 无障碍描述 * @param contentDescription 无障碍描述
@ -55,7 +53,7 @@ fun AnimatedGif(
} }
val state = rememberAsyncImageState( val state = rememberAsyncImageState(
options = remember { ImageOptions { repeatCount(REPEAT_COUNT) } } options = remember { ImageOptions { repeatCount(-1) } }
) )
AsyncImage( AsyncImage(

View File

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

View File

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

View File

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