perf: 升级 Macrobenchmark 至 1.4.1,适配新 API 并重新生成 Baseline Profile

- benchmarkMacro 1.3.4 → 1.4.1
- BaselineProfileGenerator: 使用 Direction 枚举替代坐标 swipe;
  用 executeShellCommand 绕过 startActivityAndWait(模拟器 software renderer
  不支持 gfxinfo framestats,会导致 amStartAndWait 超时)
- updateBaselineProfile task: 适配 1.4+ 的 startup-prof.txt 文件名格式,
  使用 layout.buildDirectory 预先计算路径(configuration cache 兼容)
- 重新生成 baseline-prof.txt(全量 AOT 覆盖)
- benchmark build type 设为 isDebuggable=true(配合模拟器运行)
- 将 Baseline Profile 使用文档从 README 迁移至 DEVELOPMENT.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-21 11:37:37 +08:00
parent 2771e3733b
commit 98f3b71c4f
7 changed files with 17515 additions and 263 deletions

View File

@ -117,6 +117,7 @@ CalendarMonthView (顶层屏幕)
项目使用 `composeTraceBeginSection` / `composeTraceEndSection` 在关键代码段插入 trace markerAndroid 上会被记录到系统 trace 中。iOS 为空操作。
已有的 trace section
- `MonthView:Compose` / `YearView:Compose` — 顶层重组耗时
- `YearView→MonthView` / `MonthView→YearView` — 年视图切换动画
- `YearGridView:$year` / `generateMiniMonthDays:$year-$month` — 年网格渲染
@ -125,6 +126,7 @@ CalendarMonthView (顶层屏幕)
### 分析折叠器卡顿的方法
1. **录制 trace**Android Studio → Profiler → CPU → 选择 "Trace Java Methods" 或命令行:
```bash
adb shell perfetto -c - --txt \<<EOF
buffers: { size_kb: 65536 }
@ -247,6 +249,47 @@ CalendarMonthView (顶层屏幕)
### 已知排查结论2026-05-19
对折叠器 trace 的分析显示:
- **重组本身很快**VM progress → Compose 约 500μs不是卡顿来源。
- **触摸事件采样间隔不均匀**是主要问题。某些拖拽序列中出现 30-50ms 的触摸事件间隔,偶尔有 >100ms 的断流。这属于系统/模拟器层的事件分发问题,而非 Compose 代码问题。
- 若在真机上复现,建议检查是否有 CPU 抢占或手指短暂离屏。
## Baseline Profile
```bash
# 编译 Android debug APK
./gradlew :app:assembleDebug
# 安装到设备
./gradlew :app:installDebug
# 编译 release APK含 Baseline Profiles
./gradlew :app:assembleRelease
./gradlew :app:installBenchmark
```
Baseline Profile 自动生成器。
运行方式(一键生成 + 自动复制到 :core
```
./gradlew :macrobenchmark:updateBaselineProfile
```
仅运行基准测试(不自动复制):
```
./gradlew :macrobenchmark:connectedBenchmarkAndroidTest
```
手动复制路径:
`macrobenchmark/build/outputs/connected_android_test_additional_output/`
测试覆盖全部用户交互路径,实现全量 AOT
1. 冷启动 → 首帧渲染
2. FAB 展开 → 年视图 → 月视图
3. 日期选择 → 周视图折叠/展开
4. 关于页 → 开源许可页
5. 返回主界面

View File

@ -20,43 +20,3 @@
- iOS 入口为 `MainViewController.kt`,Xcode 工程位于 `iosApp/`
线条小狗表情包来自 https://www.douban.com/group/topic/264788645/?_i=9181692phrDzjR,9241256phrDzjR
## 快速开始
```bash
# 编译 Android debug APK
./gradlew :app:assembleDebug
# 安装到设备
./gradlew :app:installDebug
# 编译 release APK含 Baseline Profiles
./gradlew :app:assembleRelease
./gradlew :app:installBenchmark
```
Baseline Profile 自动生成器。
运行方式(一键生成 + 自动复制到 :core
```
./gradlew :macrobenchmark:updateBaselineProfile
```
仅运行基准测试(不自动复制):
```
./gradlew :macrobenchmark:connectedBenchmarkAndroidTest
```
手动复制路径:
`macrobenchmark/build/outputs/connected_android_test_additional_output/`
测试覆盖全部用户交互路径,实现全量 AOT
1. 冷启动 → 首帧渲染
2. FAB 展开 → 年视图 → 月视图
3. 日期选择 → 周视图折叠/展开
4. 关于页 → 开源许可页
5. 返回主界面

View File

@ -50,7 +50,7 @@ android {
initWith(buildTypes.getByName("release"))
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks += listOf("release")
isDebuggable = false
isDebuggable = true
}
}

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,7 @@ kotlinx-datetime = "0.8.0"
tyme4kt = "1.4.5"
sketch = "4.4.0"
profileinstaller = "1.4.0"
benchmarkMacro = "1.3.4"
benchmarkMacro = "1.4.1"
androidx-uiautomator = "2.3.0"
[libraries]

View File

@ -43,28 +43,31 @@ dependencies {
val updateBaselineProfile by tasks.registering {
group = "benchmark"
description = "运行 connectedBenchmarkAndroidTest 并将生成的 baseline-prof.txt 复制到 :core 模块"
description = "运行 connectedBenchmarkAndroidTest 并将生成的 startup-prof.txt 复制到 :core 模块"
// 依赖基准测试 task需要先连接设备/模拟器)
dependsOn("connectedBenchmarkAndroidTest")
// 预先计算目标路径,避免在 doLast 中引用 project 对象configuration cache 兼容)
val targetPath = rootProject.projectDir.resolve("core/src/main/baseline-prof.txt").absolutePath
val buildDirPath = layout.buildDirectory.get().asFile.absolutePath
doLast {
// 寻找生成的 profile 文件(路径格式因设备/Gradle 版本略有不同)
// 寻找生成的 profile 文件(benchmark 1.4+ 文件名格式:{Class}_{Method}-startup-prof.txt
val sourcePaths = listOf(
// AGP 8.x+ 标准输出路径
"build/outputs/connected_android_test_additional_output/benchmark/",
"build/outputs/connected_android_test_additional_output/",
"$buildDirPath/outputs/connected_android_test_additional_output/benchmark/",
"$buildDirPath/outputs/connected_android_test_additional_output/",
)
val targetFile = rootProject.projectDir.resolve("core/src/main/baseline-prof.txt")
val targetFile = File(targetPath)
var copied = false
for (path in sourcePaths) {
val dir = file(path)
val dir = File(path)
if (!dir.exists()) continue
// 优先匹配不带时间戳的 startup-prof.txtbenchmark 1.4+ 格式)
val profileFile = dir.walkTopDown()
.firstOrNull { it.name == "baseline-prof.txt" }
.firstOrNull { it.name.endsWith("-startup-prof.txt") && !it.name.contains(Regex("-\\d{4}-\\d{2}-\\d{2}-")) }
?: continue
profileFile.copyTo(targetFile, overwrite = true)
@ -76,7 +79,7 @@ val updateBaselineProfile by tasks.registering {
if (!copied) {
throw GradleException(
"未找到生成的 baseline-prof.txt。\n" +
"未找到生成的 *-startup-prof.txt。\n" +
"请确认:\n" +
" 1. 设备/模拟器已连接 (adb devices)\n" +
" 2. 应用已安装在 benchmark 构建类型下\n" +

View File

@ -3,6 +3,7 @@ package plus.rua.project.baseline
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.Rule
import org.junit.Test
@ -44,10 +45,12 @@ class BaselineProfileGenerator {
includeInStartupProfile = true,
profileBlock = {
// 1. 冷启动:从 launcher 启动应用
// 注:使用 shell command 绕过 startActivityAndWait因为模拟器的 software
// renderer 不支持 gfxinfo framestats会导致 amStartAndWait 超时。
pressHome()
startActivityAndWait()
// 2. 等待首帧渲染完成
device.executeShellCommand(
"am start -W -n plus.rua.project/.MainActivity"
)
device.waitForIdle()
// 3. 模拟用户交互:展开 FAB 菜单
@ -67,9 +70,9 @@ class BaselineProfileGenerator {
// 5. 在年视图中滑动到不同年份(覆盖动画和分页路径)
val yearGrid = device.findObject(By.res("plus.rua.project:id/year_grid"))
if (yearGrid != null) {
yearGrid.swipe(300, 600, 300, 1200, 10)
yearGrid.swipe(Direction.UP, 0.5f)
device.waitForIdle()
yearGrid.swipe(300, 1200, 300, 600, 10)
yearGrid.swipe(Direction.DOWN, 0.5f)
device.waitForIdle()
}
@ -92,17 +95,17 @@ class BaselineProfileGenerator {
val calendarArea = device.findObject(By.res("plus.rua.project:id/calendar_pager"))
?: device.findObject(By.textContains("2026"))
if (calendarArea != null) {
calendarArea.swipe(300, 600, 300, 1200, 10)
calendarArea.swipe(Direction.UP, 0.5f)
device.waitForIdle()
calendarArea.swipe(300, 1200, 300, 600, 10)
calendarArea.swipe(Direction.DOWN, 0.5f)
device.waitForIdle()
}
// 9. 左右滑动切换月份(覆盖 CalendarPager 翻页)
if (calendarArea != null) {
calendarArea.swipe(600, 800, 100, 800, 10)
calendarArea.swipe(Direction.LEFT, 0.5f)
device.waitForIdle()
calendarArea.swipe(100, 800, 600, 800, 10)
calendarArea.swipe(Direction.RIGHT, 0.5f)
device.waitForIdle()
}