From 281abcf66b13013b5656d05f7f3402ecf6be3476 Mon Sep 17 00:00:00 2001 From: xfy Date: Wed, 27 May 2026 16:06:05 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E6=B7=BB=E5=8A=A0=E6=80=A7=E8=83=BD?= =?UTF-8?q?=E8=BF=BD=E8=B8=AA=E6=A0=87=E8=AE=B0=E5=B9=B6=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E5=9F=BA=E7=BA=BF=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DEVELOPMENT.md | 9 +- core/src/main/baseline-prof.txt | 15 +- .../plus/rua/project/CalendarViewModel.kt | 8 + .../kotlin/plus/rua/project/ui/AnimatedGif.kt | 6 +- .../plus/rua/project/ui/CalendarMonthPage.kt | 4 + .../plus/rua/project/ui/CalendarPager.kt | 4 + .../baseline/BaselineProfileGenerator.kt | 207 +++++++++--------- 7 files changed, 146 insertions(+), 107 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 4cd8ece..0e8b44b 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -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 diff --git a/core/src/main/baseline-prof.txt b/core/src/main/baseline-prof.txt index c5c20d3..89c7a81 100644 --- a/core/src/main/baseline-prof.txt +++ b/core/src/main/baseline-prof.txt @@ -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;->()V -SPLplus/rua/project/ui/WeekdayHeaderKt$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;)Ljava/lang/Object; \ No newline at end of file +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; diff --git a/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt b/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt index aa4b649..50aa9ae 100644 --- a/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt +++ b/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt @@ -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() } /** diff --git a/core/src/main/kotlin/plus/rua/project/ui/AnimatedGif.kt b/core/src/main/kotlin/plus/rua/project/ui/AnimatedGif.kt index e102446..9a6bb4f 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/AnimatedGif.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/AnimatedGif.kt @@ -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( diff --git a/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthPage.kt b/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthPage.kt index 810bae8..6bd0bea 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthPage.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthPage.kt @@ -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( diff --git a/core/src/main/kotlin/plus/rua/project/ui/CalendarPager.kt b/core/src/main/kotlin/plus/rua/project/ui/CalendarPager.kt index 21168c5..d9cd2c8 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/CalendarPager.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/CalendarPager.kt @@ -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() } } \ No newline at end of file diff --git a/macrobenchmark/src/main/java/plus/rua/project/baseline/BaselineProfileGenerator.kt b/macrobenchmark/src/main/java/plus/rua/project/baseline/BaselineProfileGenerator.kt index dbd3138..2561884 100644 --- a/macrobenchmark/src/main/java/plus/rua/project/baseline/BaselineProfileGenerator.kt +++ b/macrobenchmark/src/main/java/plus/rua/project/baseline/BaselineProfileGenerator.kt @@ -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 生成完成,所有路径已覆盖") } ) }