Jetpack Benchmark 库的 Macrobenchmark 稳定性,CI 环境下跑得过吗
Jetpack Benchmark 库的 Macrobenchmark 稳定性,CI 环境下跑得过吗
Macrobenchmark 这个库从 AndroidX Benchmark 1.1.0 被扶正以来,Google 官方文档的口径就一直是“把性能测试左移”、“在 CI 中建立性能基准线”。但凡是真动手把 measureRepeated 块塞到 MacrobenchmarkRule 里、然后试图让 GitHub Actions 每晚跑一遍的开发者,大概率都碰过同一种绝望:同一套 startup benchmark,本地 Pixel 6 连着跑三轮,启动时间方差控制在 8% 以内;一模一样的二进制丢到 Firebase Test Lab 的物理设备队列里,方差能飙到 35% 甚至 50%。这时候你盯着 Grafana 上那条像心电图一样的折线,根本分不清是昨天那笔 Kotlin 升级搞垮了性能,还是云手机刚从上一个游戏团队的烫手测试里缓过来。Macrobenchmark 不是不能跑 CI,是它的整个设计前提跟云端共享设备、未 root、温控严苛的环境天生相克。Google 生态团队自己在 I/O 上演示用的都是 rooted Pixel 实验室集群,却把“建议锁频”这句话轻描淡写地埋在文档第十页,这多少有点何不食肉糜。
`lockClocks` 的门槛:CI 设备连入场券都没有
Macrobenchmark 底层对测量稳定性的假设非常硬。它期望设备在测试期间 CPU/GPU 频率恒定、不触发 thermal throttling、后台没有流氓进程抢占调度器。为了达到这个理想状态,库内部会尝试通过 adb shell cmd activity memory-factor critical 或者 root 权限下的 lockClocks 脚本来钉死频率。Jetpack Benchmark 1.2.0 的官方文档里白纸黑字写着:“最佳实践是使用已 root 的设备进行基准测试。” 这句话翻译成现实就是:如果你的 CI 跑在 Firebase Test Lab、AWS Device Farm 或者 GitHub Actions 提供的共享 macOS runner 上,你连门票都没买到。
未 root 的设备上,Macrobenchmark 的 fallback 策略可怜得很。它只能调低屏幕亮度、关掉动画、尽量推应用进缓存,然后祈祷系统调度器给点面子。Android 12(API 31)之后,Google 进一步收紧了 WRITE_SECURE_SETTINGS 和 root shell 命令的可用性,很多云设备厂商连 adb shell settings put global sys_vm_stats 0 这类操作都要弹安全警告。后果就是,CI 上跑出来的 trace 文件里经常能看到 CPU 频率在测试中途从 1.8GHz 跳到 1.2GHz,而 MacrobenchmarkScope 提供的 isThermalThrottled 检查在未 root 设备上根本就是盲猜——它读不到 /sys/class/thermal/thermal_zone*/temp,只能依赖系统上报的 POWER_THROTTLING 信号,后者在部分 OEM 定制 ROM 上根本不存在。你拿到一份“启动耗时 420ms”的报告,其实里头有 80ms 是 CPU 降频等来的,这数据你敢当 regression gate 用?
`CompilationMode` 在云端的时间黑洞
Macrobenchmark 默认会操纵应用的 AOT 编译状态。你写 measureRepeated 测冷启动,库底层会拿着 pm compile -f speed-profile 或者 --full 去折腾 target package。这个设计在本地很好理解:确保每次测试都是相同的 ART 编译状态,排除 profile-guided compilation 的干扰。但在 CI 里,它就是个不定时炸弹。
Firebase Test Lab 的物理设备实例是复用的,虽然每次测试会清数据,但 pm compile 触发的 dex2oat 进程在后台烧 CPU 的时间完全不可控。曾经有个很典型的场景:某个团队在 FTL 上跑 CompilationMode.Full() 的 startup benchmark,本地 40 秒能结束的测试,在 FTL arm64 队列里硬生生拖了 6 分半。不是因为 app 变重了,而是 FTL 那台共享设备当时的 I/O 调度被隔壁进程的 dex2oat 占满,Macrobenchmark 的 compilation phase 在等待 ART 完成全量编译。更坑的是,Gradle Managed Device(GMD)的默认 timeoutSeconds 在 AGP 8.2 里对这类测试并不友好,benchmark 进程被 system server 杀掉的日志在 FTL 上又看不着,你只能看到 JUnit runner 抛一个模糊的 ProcessCrashException,然后 CI 标注失败