build(benchmark): make baseline generation stable on emulator

This commit is contained in:
xfy 2026-06-15 19:17:11 +08:00
parent 66be0be3c4
commit 499a2eb0c7
4 changed files with 27 additions and 176 deletions

View File

@ -53,7 +53,10 @@ android {
initWith(buildTypes.getByName("release")) initWith(buildTypes.getByName("release"))
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("debug")
matchingFallbacks += listOf("release") matchingFallbacks += listOf("release")
isDebuggable = true // isDebuggable=false 使 macrobenchmark 在模拟器上稳定运行,且 Partial 编译模式可用。
// 代价是生成的 baseline-prof.txt 会包含 R8 混淆后的类名;功能上仍然有效。
// 若需要可人工维护的可读 profile请在真机上使用 debuggable build 或引入 Baseline Profile Gradle plugin。
isDebuggable = false
} }
} }

View File

@ -11,6 +11,10 @@ android {
targetSdk = libs.versions.android.targetSdk.get().toInt() targetSdk = libs.versions.android.targetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// 允许在模拟器 / debuggable target 上运行 macrobenchmark仅用于开发验证
// 正式发布 benchmark 请在物理设备、release 目标应用上执行。
testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "EMULATOR,DEBUGGABLE"
} }
compileOptions { compileOptions {

View File

@ -5,14 +5,14 @@
## Purpose ## Purpose
Baseline Profile 自动生成器与启动性能基准测试。包含: Baseline Profile 自动生成器与启动性能基准测试。包含:
- `BaselineProfileGenerator.kt`:通过 UI Automator 模拟完整用户交互路径冷启动、FAB 展开、年/月视图切换、日期选择、折叠/展开、关于页、开源许可页),生成 AOT 编译优化所需的 startup profile。 - `BaselineProfileGenerator.kt`:通过 UI Automator 模拟核心用户交互路径冷启动、FAB 展开、显示调休切换、CalendarPager 翻页、日期选择、BottomCard 折叠/展开),生成 AOT 编译优化所需的 startup profile。
- `StartupBenchmark.kt`:冷启动性能基准测试,测量 `timeToInitialDisplay``timeToFullDisplay` - `StartupBenchmark.kt`:冷启动性能基准测试,测量 `timeToInitialDisplay``timeToFullDisplay`
## Key Files ## Key Files
| File | Description | | File | Description |
|------|-------------| |------|-------------|
| `BaselineProfileGenerator.kt` | Profile 生成测试类,覆盖全部用户交互路径 | | `BaselineProfileGenerator.kt` | Profile 生成测试类,覆盖启动与核心交互路径 |
| `StartupBenchmark.kt` | 冷启动基准测试类,覆盖 Full/Partial/None 三种编译模式 | | `StartupBenchmark.kt` | 冷启动基准测试类,覆盖 Full/Partial/None 三种编译模式 |
## Subdirectories ## Subdirectories

View File

@ -29,24 +29,19 @@ import org.junit.runner.RunWith
* 手动复制路径 * 手动复制路径
* `macrobenchmark/build/outputs/connected_android_test_additional_output/` * `macrobenchmark/build/outputs/connected_android_test_additional_output/`
* *
* 测试覆盖全部用户交互路径实现全量 AOT * 测试覆盖启动与核心用户交互路径实现关键路径 AOT
* 1. 冷启动 首帧渲染 * 1. 冷启动 首帧渲染
* 2. 显示调休切换 ON/OFFDayCell 大规模重组 + staggered 动画 * 2. 显示调休切换 ON/OFFDayCell 大规模重组 + staggered 动画
* 3. CalendarPager 翻页 "今天"按钮跳回 * 3. CalendarPager 翻页 "今天"按钮跳回
* 4. 跨月日期点击 自动跳转 * 4. 跨月日期点击 自动跳转
* 5. 月视图 年视图切换 * 5. DayCell 点击
* 6. 年视图翻年 "今年"按钮跳回 * 6. BottomCard 拖拽折叠到周视图
* 7. 年视图点击 MiniMonth 返回月视图 * 7. 周视图左右翻页
* 8. DayCell 点击 * 8. BottomCard 拖拽展开回月视图
* 9. BottomCard 拖拽折叠到周视图 * 9. CalendarPager 左右翻页
* 10. 周视图左右翻页 *
* 11. BottomCard 拖拽展开回月视图 * 年视图关于/开源许可工具/日期检查器等路径在部分模拟器上不稳定
* 12. 工具页面 日期检查器 * 暂时从生成流程中移除以保证 Baseline Profile 可稳定生成后续可在真机上扩展覆盖
* 13. DatePickerDialog 打开/确认
* 14. 日期检查器添加行 + 输入天数
* 15. 日期检查器滑动删除行
* 16. 关于页面 开源许可页面
* 17. 返回主界面 CalendarPager 翻页
*/ */
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator { class BaselineProfileGenerator {
@ -55,7 +50,7 @@ class BaselineProfileGenerator {
val baselineProfileRule = BaselineProfileRule() val baselineProfileRule = BaselineProfileRule()
private fun MacrobenchmarkScope.safeFindFab(): UiObject2? = private fun MacrobenchmarkScope.safeFindFab(): UiObject2? =
device.wait(Until.findObject(By.res("fab_menu")), 3000) device.wait(Until.findObject(By.res("fab_menu")), 5000)
private fun MacrobenchmarkScope.safeWaitCalendarPager(timeout: Long = 5000): UiObject2? = private fun MacrobenchmarkScope.safeWaitCalendarPager(timeout: Long = 5000): UiObject2? =
device.wait(Until.findObject(By.res("calendar_pager")), timeout) device.wait(Until.findObject(By.res("calendar_pager")), timeout)
@ -125,64 +120,7 @@ class BaselineProfileGenerator {
device.waitForIdle() device.waitForIdle()
} }
// ── 7. 切换到年视图 ────────────────────────────────────── // ── 7. 点击 DayCell ────────────────────────────────────
val fab2 = safeFindFab()
assertNotNull("FAB 必须存在(年视图)", fab2)
fab2!!.click()
device.waitForIdle()
val yearViewItem = device.wait(
Until.findObject(By.text("年视图")), 3000
)
assertNotNull("年视图必须出现", yearViewItem)
yearViewItem!!.click()
val yearGrid = device.wait(
Until.findObject(By.res("year_grid")), 3000
)
assertNotNull("YearGridView 必须加载", yearGrid)
device.waitForIdle()
// ── 8. 左滑年视图 → 下一年HorizontalPager ─────────────
yearGrid!!.swipe(Direction.LEFT, 0.8f)
device.waitForIdle()
Thread.sleep(500)
// ── 9. 尝试点击"今年"按钮跳回当前年 ──────────────────
val thisYearBtn = device.wait(
Until.findObject(By.text("今年")), 2000
)
if (thisYearBtn != null && thisYearBtn.visibleBounds.height() > 0) {
thisYearBtn.click()
device.waitForIdle()
Thread.sleep(500)
}
// ── 10. 点击当前月份 MiniMonth 返回月视图 ───────────────
val now = java.time.LocalDate.now()
val currentMonthDesc = "${now.year}${now.monthValue}"
val miniMonth = device.wait(
Until.findObject(By.desc(currentMonthDesc)), 3000
)
if (miniMonth != null) {
miniMonth.click()
} else {
val yearGridRef = device.wait(Until.findObject(By.res("year_grid")), 3000)
if (yearGridRef != null) {
val ygBounds = yearGridRef.visibleBounds
val monthW = ygBounds.width() / 3
val monthH = ygBounds.height() / 4
val mIdx = now.monthValue - 1
device.click(
ygBounds.left + (mIdx % 3) * monthW + monthW / 2,
ygBounds.top + (mIdx / 3) * monthH + monthH / 2
)
}
}
Thread.sleep(1500)
device.waitForIdle()
safeWaitCalendarPager(5000)
device.waitForIdle()
// ── 11. 点击 DayCell ────────────────────────────────────
val todayCell = device.wait( val todayCell = device.wait(
Until.findObject(By.descContains("今天")), 3000 Until.findObject(By.descContains("今天")), 3000
) )
@ -197,7 +135,7 @@ class BaselineProfileGenerator {
} }
device.waitForIdle() device.waitForIdle()
// ── 12. 拖拽 BottomCard 折叠到周视图 ───────────────────── // ── 8. 拖拽 BottomCard 折叠到周视图 ─────────────────────
val bottomCard = device.wait(Until.findObject(By.res("bottom_card")), 5000) val bottomCard = device.wait(Until.findObject(By.res("bottom_card")), 5000)
assertNotNull("BottomCard 必须存在", bottomCard) assertNotNull("BottomCard 必须存在", bottomCard)
val bcBounds = bottomCard!!.visibleBounds val bcBounds = bottomCard!!.visibleBounds
@ -207,7 +145,7 @@ class BaselineProfileGenerator {
device.drag(cx, cy, cx, cy - dragDist, 20) device.drag(cx, cy, cx, cy - dragDist, 20)
device.waitForIdle() device.waitForIdle()
// ── 13. 周视图左右翻页 ────────────────────────────────── // ── 9. 周视图左右翻页 ──────────────────────────────────
val weekPager = safeWaitCalendarPager(3000) val weekPager = safeWaitCalendarPager(3000)
assertNotNull("周视图 CalendarPager 必须存在", weekPager) assertNotNull("周视图 CalendarPager 必须存在", weekPager)
weekPager!!.swipe(Direction.LEFT, 0.5f) weekPager!!.swipe(Direction.LEFT, 0.5f)
@ -215,11 +153,11 @@ class BaselineProfileGenerator {
weekPager.swipe(Direction.RIGHT, 0.5f) weekPager.swipe(Direction.RIGHT, 0.5f)
device.waitForIdle() device.waitForIdle()
// ── 14. 拖拽 BottomCard 展开回月视图 ───────────────────── // ── 10. 拖拽 BottomCard 展开回月视图 ─────────────────────
device.drag(cx, cy - dragDist, cx, cy, 20) device.drag(cx, cy - dragDist, cx, cy, 20)
device.waitForIdle() device.waitForIdle()
// ── 15. 切换"显示调休"OFF ──────────────────────────────── // ── 11. 切换"显示调休"OFF ────────────────────────────────
val fab3 = safeFindFab() val fab3 = safeFindFab()
assertNotNull("FAB 必须存在(关闭调休)", fab3) assertNotNull("FAB 必须存在(关闭调休)", fab3)
fab3!!.click() fab3!!.click()
@ -231,7 +169,7 @@ class BaselineProfileGenerator {
legalHolidayOff!!.click() legalHolidayOff!!.click()
device.waitForIdle() device.waitForIdle()
// ── 16. CalendarPager 左右翻页 ────────────────────────── // ── 12. CalendarPager 左右翻页 ──────────────────────────
val mainPager = safeWaitCalendarPager(5000) val mainPager = safeWaitCalendarPager(5000)
if (mainPager != null) { if (mainPager != null) {
mainPager.swipe(Direction.LEFT, 0.5f) mainPager.swipe(Direction.LEFT, 0.5f)
@ -240,101 +178,7 @@ class BaselineProfileGenerator {
device.waitForIdle() device.waitForIdle()
} }
// ── 17. 进入关于页面 ──────────────────────────────────── Log.d(TAG, "Baseline profile 生成完成,核心路径已覆盖")
val fab5 = safeFindFab()
assertNotNull("FAB 必须存在(关于)", fab5)
fab5!!.click()
device.waitForIdle()
val aboutButton = device.wait(
Until.findObject(By.text("关于")), 3000
)
assertNotNull("关于必须出现", aboutButton)
aboutButton!!.click()
device.waitForIdle()
// ── 18. 进入开源许可页面 ────────────────────────────────
val licensesButton = device.wait(
Until.findObject(By.text("开放源代码许可")), 3000
)
assertNotNull("开放源代码许可按钮必须存在", licensesButton)
licensesButton!!.click()
device.waitForIdle()
// ── 19. 等待许可列表加载 ────────────────────────────────
device.wait(Until.findObject(By.textContains("Apache")), 2000)
// ── 20. 返回关于页 ──────────────────────────────────────
device.pressBack()
device.waitForIdle()
// ── 21. 返回主界面 ──────────────────────────────────────
device.pressBack()
device.waitForIdle()
// ── 22. 进入工具页面 ────────────────────────────────────
val fab4 = safeFindFab()
assertNotNull("FAB 必须存在(工具)", fab4)
fab4!!.click()
device.waitForIdle()
val toolsButton = device.wait(
Until.findObject(By.text("工具")), 3000
)
assertNotNull("工具必须出现", toolsButton)
toolsButton!!.click()
device.waitForIdle()
// ── 23. 进入日期检查器 ──────────────────────────────────
val dateCheckerEntry = device.wait(
Until.findObject(By.res("tool_date_checker")), 3000
)
assertNotNull("日期检查器入口必须存在", dateCheckerEntry)
dateCheckerEntry!!.click()
device.waitForIdle()
// ── 24. 打开生产日期 DatePicker → 确定 ───────────────────
val datePickerBtn = device.wait(
Until.findObject(By.res("date_picker_button")), 3000
)
if (datePickerBtn != null) {
datePickerBtn.click()
device.waitForIdle()
val confirmBtn = device.wait(
Until.findObject(By.text("确定")), 2000
)
if (confirmBtn != null) {
confirmBtn.click()
device.waitForIdle()
}
}
// ── 25. FAB 添加新行 ────────────────────────────────────
val dateCheckerFab = device.wait(
Until.findObject(By.res("date_checker_fab")), 3000
)
assertNotNull("DateChecker FAB 必须存在", dateCheckerFab)
dateCheckerFab!!.click()
device.waitForIdle()
// ── 26. 在新行输入天数 ──────────────────────────────────
val screenW = device.displayWidth
val screenH = device.displayHeight
device.click((screenW * 0.35f).toInt(), (screenH * 0.80f).toInt())
Thread.sleep(500)
device.executeShellCommand("input text '90'")
device.waitForIdle()
device.click(screenW / 2, (screenH * 0.15f).toInt())
device.waitForIdle()
// ── 27. 滑动删除新行SwipeToDismiss ───────────────────
device.swipe(
(screenW * 0.85f).toInt(), (screenH * 0.75f).toInt(),
(screenW * 0.15f).toInt(), (screenH * 0.75f).toInt(),
30
)
Thread.sleep(800)
device.waitForIdle()
Log.d(TAG, "Baseline profile 生成完成,所有路径已覆盖")
} }
) )
} }