关于 generating-baseline-profiles
面向 Android 开发者的 Baseline Profile 生成与测量技能,指导为启动和滚动路径生成 AOT 配置,并用 Macrobenchmark 验证性能变化。
主要功能
- 01创建 Baseline Profile Generator 模块
- 02为启动和滚动路径生成配置
- 03将配置打包进 APK
- 04用 Macrobenchmark 测量启动与帧耗时
- 05通过 ReportDrawn 测量完整显示时间
适用场景
- 01改善应用冷启动
- 02减少首次滚动卡顿
- 03验证性能修复是否改变指标
- 04为发布构建建立性能基线
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ skydoves/ compose-performance-skills --skill generating-baseline-profiles安装后在 Claude Code 中输入/generating-baseline-profiles确认已加载。
附带文件references/macrobenchmark-harness.md
详细介绍
生成 Baseline Profile — 随 APK 发布 AOT 编译提示列表,并验证它确实带来了提升
Baseline Profile 会在 APK 中携带 AOT 编译提示列表,以便 ART 在安装时预编译热点 Compose 代码路径,而不是依赖首次运行时的 JIT 编译。据报告,在经过性能分析的使用流程中,冷启动速度约提升 30%,首次滚动流畅度提升 40%。Compose 与平台分开发布,因此所有 Compose UI 应用都能受益——没有任何 Android 版本会在系统映像中预先 AOT 编译 Compose。
这项技能是后续性能工作的测量主干。Profile 使用 androidx.benchmark:benchmark-macro-junit4 中的 BaselineProfileRule 生成,并使用同一构件中的 MacrobenchmarkRule 测量。生成和测量分别放在两个独立的 @Test 文件中,位于单独的 :baselineprofile 模块内;AGP 8.2+ 可通过新建模块 → Baseline Profile Generator创建该模块。
何时使用这项技能
- 冷启动速度慢,开发者希望首次启动时就能使用 AOT 编译的热点路径。
- 即使修复了稳定性问题,真实设备上的
LazyColumn/LazyVerticalGrid首次滚动仍有卡顿。 - 准备发布版本,开发者需要在发布前建立性能基线。
- 验证稳定性或强跳过修复是否确实改善了冷启动或帧耗时数据——Macrobenchmark 是衡量实际效果的基准。
- 用户提到“baseline profile”“macrobenchmark”“TTFD”“time-to-fully-drawn”“StartupTimingMetric”“FrameTimingMetric”“CompilationMode”“
aosp_cf_x86_64_phone-userdebug”或“ReportDrawn”。
何时不使用这项技能
- 仍处于早期原型阶段,尚无可用于发布的用户流程——Baseline Profile 会记录热点路径,而界面仍在频繁变动时还没有可记录的热点路径。
- 开发者想查看每个可组合项的重组次数——应使用
../../recomposition/debugging-recompositions/SKILL.md(用于调试的 Layout Inspector)或../tracing-recompositions-at-runtime/SKILL.md(发布版本中的@TraceRecomposition)。 - 开发者希望在 CI 中设置稳定性回归门禁——应使用
../../stability/enforcing-stability-in-ci/SKILL.md。Baseline Profile 衡量运行时表现;stabilityCheck衡量编译时是否可跳过。 - 数据来自调试版本——调试版本通过 Live Literals 以解释方式运行,测得的数据不具代表性。请参阅
../testing-compose-in-release-mode/SKILL.md。
前置条件
- AGP 8.2+,以使用新建模块 → Baseline Profile Generator模板(该模板会创建
androidx.baselineprofileGradle 插件和:baselineprofile模块)。 - 一台低端实体测试设备或 Cuttlefish 模拟器(
aosp_cf_x86_64_phone-userdebug)。高端 Pixel 设备会掩盖性能提升;使用默认 API 映像的模拟器也不具代表性。 - 已配置发布变体(
isMinifyEnabled = true、isShrinkResources = true、proguard-android-optimize.txt)。调试版本无法用于测量。如果尚未配置 R8,请参阅../../build/configuring-r8-for-compose/SKILL.md。 - 在应用
AndroidManifest.xml的<application>下添加<profileable android:shell="true"/>,以便 Macrobenchmark 进程连接 simpleperf。必须包含android:命名空间前缀,否则清单合并会失败。 - 除启动流程外,至少确定一个滚动流程——例如信息流列表、分页网格或打开详情页。只对启动过程进行性能分析,不会编译首次滚动路径。
- 如果启动耗时主要来自可避免的重组,而不是初始组合,请熟悉
../../stability/diagnosing-compose-stability/SKILL.md和../../recomposition/debugging-recompositions/SKILL.md。
工作流
1. 添加 Baseline Profile Generator 模块
在 Android Studio 中:File → New → New Module → Baseline Profile Generator。选择目标应用模块。AGP 会生成:
- 一个新的
:baselineprofile模块,并应用androidx.baselineprofileGradle 插件。 - 一个包含
BaselineProfileRule@Test的BaselineProfileGenerator.kt骨架。 - 一个包含
MacrobenchmarkRule@Test的StartupBenchmarks.kt骨架。 - 同时在 app 模块中应用
androidx.baselineprofile插件,以配置好./gradlew :app:generateBaselineProfile。
如果不使用模板而是手动编辑,请在 :baselineprofile/build.gradle.kts 中添加:
plugins {
id("com.android.test")
id("org.jetbrains.kotlin.android")
id("androidx.baselineprofile")
}
android { targetProjectPath = ":app" }
dependencies {
implementation("androidx.benchmark:benchmark-macro-junit4:1.3.0")
implementation("androidx.test.ext:junit:1.2.1")
implementation("androidx.test.uiautomator:uiautomator:2.3.0")
implementation("androidx.test.espresso:espresso-core:3.6.1")
}
并在 :app/build.gradle.kts 中添加:
plugins { id("androidx.baselineprofile") }
dependencies { baselineProfile(project(":baselineprofile")) }
有关完整的 Gradle 和 manifest 设置(包括 <profileable>),请参阅 references/macrobenchmark-harness.md。
2. 编写同时覆盖启动和滚动的生成器
生成器是一个使用 BaselineProfileRule.collect 的 @Test。必须覆盖启动和至少一次滚动——仅覆盖启动的配置文件会导致首次滚动时仍未预热。
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule val rule = BaselineProfileRule()
@Test
fun startupAndScroll() = rule.collect(packageName = "com.example") {
startActivityAndWait()
val feed = device.findObject(By.res("feed"))
feed.setGestureMargin(device.displayWidth / 5)
feed.fling(Direction.DOWN)
feed.fling(Direction.DOWN)
device.findObject(By.res("feed_item_0"))?.click()
device.wait(Until.hasObject(By.res("detail")), 3_000)
device.pressBack()
}
}
在 Compose 源码中使用 Modifier.testTag("feed"),以便 By.res("feed") 能够解析。将手势边距设为 displayWidth / 5,可让快速滑动避开系统手势区域。
3. 生成配置文件
./gradlew :app:generateBaselineProfile
对于特定变体:./gradlew :app:generateReleaseBaselineProfile。
输出文件位于:
app/src/<variant>/generated/baselineProfiles/baseline-prof.txt
(例如:app/src/release/generated/baselineProfiles/baseline-prof.txt。在较旧的 AGP 目录结构中,文件可能位于 app/src/main/generated/baselineProfiles/baseline-prof.txt——AGP 会写入特定变体的路径;如果所有变体共用一个配置文件,则发布时将其合并到 main。)
4. 验证配置文件已打包进 APK
在 release .apk / .aab 上执行 Build → Analyze APK,并确认:
assets/dexopt/baseline.prof
assets/dexopt/baseline.profm
如果缺少这些文件,则说明配置文件已生成,但未打包——通常是因为没有在 app 模块的 dependencies 中添加 baselineProfile(project(":baselineprofile"))。如果 APK 中没有这些文件,ART 就无法进行 AOT 编译,性能收益为零。
5. 使用 Macrobenchmark 测量——启动
在同一个 :baselineprofile 模块中,使用 MacrobenchmarkRule 编写一个独立的 @Test。编译模式必须为 CompilationMode.Partial(BaselineProfileMode.Require),这样如果缺少配置文件,测试会明确失败,而不是悄悄测量未应用配置文件的构建。
@RunWith(AndroidJUnit4::class)
class StartupBenchmarks {
@get:Rule val rule = MacrobenchmarkRule()
@Test
fun startupCompilationBaselineProfiles() = rule.measureRepeated(
packageName = "com.example",
metrics = listOf(StartupTimingMetric()),
iterations = 10,
startupMode = StartupMode.COLD,
compilationMode = CompilationMode.Partial(BaselineProfileMode.Require),
) {
pressHome()
startActivityAndWait()
}
}
如需进行 A/B 对比,请添加一个并列的 @Test,设置 compilationMode = CompilationMode.None,然后比较中位数。只有这样才能证明配置文件确实改变了数值。
6. 使用 Macrobenchmark 测量——滚动
@Test
fun feedScrollPerformance() = rule.measureRepeated(
packageName = "com.example",
metrics = listOf(FrameTimingMetric()),
iterations = 5,
compilationMode = CompilationMode.Partial(BaselineProfileMode.Require),
) {
startActivityAndWait()
val feed = device.findObject(By.res("feed"))
feed.setGestureMargin(device.displayWidth / 5)
feed.fling(Direction.DOWN)
feed.fling(Direction.DOWN)
}
FrameTimingMetric 会报告 frameDurationCpuMs 百分位数(P50 / P90 / P95 / P99)以及 frameOverrunMs(负值 = 按时完成,正值 = 错过截止时间)。最重要的指标是 P95 frameOverrunMs——滚动过程中高延迟尾部中错过截止时间的最坏帧。
7. 接入 ReportDrawn,以准确测量 TTFD
默认情况下,StartupTimingMetric 会报告 timeToInitialDisplay。要测量 timeToFullDisplay(TTFD)——真正符合用户感知的数值——应用必须在首个有意义的屏幕状态渲染后调用 ReportDrawn。如果不调用,TTFD 会回退到 timeToInitialDisplay,导致测量值偏低。
import androidx.activity.compose.ReportDrawn
import androidx.activity.compose.ReportDrawnWhen
import androidx.activity.compose.ReportDrawnAfter
@Composable
fun Feed(state: FeedState) {
ReportDrawnWhen { state.items.isNotEmpty() }
LazyColumn { items(state.items, key = { it.id }) { SnackRow(it) } }
}
ReportDrawn— 在组合时立即触发(适用于首帧就是有意义的帧的情况)。ReportDrawnWhen { predicate }— 当谓词首次变为 true 时触发(典型情况:状态已完成填充)。ReportDrawnAfter { suspend block }— 在挂起代码块完成后触发(典型情况:显式调用awaitFirstFrame())。
8. 在真实设备上运行、比较中位数并迭代
在已连接的实体设备或 aosp_cf_x86_64_phone-userdebug Cuttlefish 上运行 ./gradlew :baselineprofile:connectedReleaseAndroidTest。报告各次迭代的中位数,而非平均值——Macrobenchmark 会同时记录两者,而单次因温控降频而变慢的运行会显著影响平均值。IDE 会显示结果表;JSON 文件位于 :baselineprofile/build/outputs/connected_android_test_additional_output/。
有关在 CI 中解析 JSON,请参阅 references/macrobenchmark-harness.md。
模式
模式:为启动和滚动生成配置文件,绝不能只生成启动配置文件
// WRONG
@Test fun startup() = rule.collect(packageName = "com.example") {
startActivityAndWait()
}
// WRONG because: only the startup code paths get AOT-compiled. First-scroll, list-item
// composition, and detail-screen entry stay interpreted on first run, which is exactly
// where the user perceives jank. Profile every hot user journey, not just launch.
// RIGHT
@Test fun startupAndScroll() = rule.collect(packageName = "com.example") {
startActivityAndWait()
val feed = device.findObject(By.res("feed"))
feed.setGestureMargin(device.displayWidth / 5)
feed.fling(Direction.DOWN)
feed.fling(Direction.DOWN)
}
模式:使用 BaselineProfileMode.Require 断言配置文件已应用
// WRONG
compilationMode = CompilationMode.Partial(BaselineProfileMode.UseIfAvailable)
// WRONG because: UseIfAvailable silently falls back to no profile if assets/dexopt/baseline.prof
// is missing. The Macrobench then measures an unprofiled build and the developer thinks
// the profile is working when it is not. Use Require to fail loudly.
// RIGHT
compilationMode = CompilationMode.Partial(BaselineProfileMode.Require)
模式:缺少 ReportDrawn 时,TTFD 会被低估
// WRONG
@Composable
fun Feed(state: FeedState) {
LazyColumn { items(state.items) { SnackRow(it) } }
}
// WRONG because: TTFD is reported when the first frame draws. An empty LazyColumn draws
// a frame too — TTFD then matches timeToInitialDisplay and undercounts the time the user
// actually waited for content. Add ReportDrawnWhen { state.items.isNotEmpty() }.
// RIGHT
@Composable
fun Feed(state: FeedState) {
ReportDrawnWhen { state.items.isNotEmpty() }
LazyColumn { items(state.items, key = { it.id }) { SnackRow(it) } }
}
模式:绝不要在调试构建中测量 Compose 性能
# WRONG
"./gradlew :app:installDebug && adb shell am start … && record startup with systrace"
# WRONG because: debug builds run Compose interpreted, with Live Literals turning constants
# into getters that defeat compile-time folding. Cold-start numbers are inflated 2–4×;
# scroll FrameTiming numbers are dominated by the interpreter overhead, not by the code.
# Measurement only counts on release + R8 + real device. Cross-link
# ../testing-compose-in-release-mode/SKILL.md.
# RIGHT
"./gradlew :baselineprofile:connectedReleaseAndroidTest" on a physical device, with
release variant minified, baseline profile generated, BaselineProfileMode.Require asserting it.
模式:进行足够多次迭代并报告中位数,而非平均值
# WRONG
"Mean cold startup with profile: 412 ms (5 iterations)."
# WRONG because: a single thermal-throttled iteration drags the mean. Macrobenchmark
# reports min / median / max; the median is the resilient summary. 5 iterations is also
# thin for startup variance — use ≥10 for StartupTimingMetric, ≥5 for FrameTimingMetric.
# RIGHT
"Median cold startup, 10 iterations: BaselineProfile 318 ms vs None 462 ms (–31%).
P95 frameOverrunMs scrolling 30 items: BaselineProfile –6 ms vs None +9 ms."
模式:设置手势边缘留白,避免系统拦截快速滑动手势
// WRONG
val feed = device.findObject(By.res("feed"))
feed.fling(Direction.DOWN)
// WRONG because: on gesture-nav devices, a fling that starts inside the system gesture area
// is intercepted as a back-swipe or recents-swipe and the scroll never happens.
// Macrobenchmark then measures an idle screen and reports artificially-good numbers.
// RIGHT
val feed = device.findObject(By.res("feed"))
feed.setGestureMargin(device.displayWidth / 5)
feed.fling(Direction.DOWN)
强制规则
- 必须为启动和至少一个滚动流程生成 Baseline Profile。仅生成启动配置文件会导致首次滚动仍然缓慢。
- 必须在真实的低端实体设备或
aosp_cf_x86_64_phone-userdebugCuttlefish 上进行测量。高端设备会掩盖性能差异;默认 API 模拟器映像不具代表性。 - 必须在测量测试中使用
CompilationMode.Partial(BaselineProfileMode.Require),确保缺失或过期的配置文件能明确导致测试失败。BaselineProfileMode.UseIfAvailable会静默地测量未应用配置文件的构建。 - 必须使用
androidx.activity.compose中的ReportDrawn/ReportDrawnWhen/ReportDrawnAfter标记有意义的首次绘制时刻。否则,timeToFullDisplay会被低估。 - 绝不能在调试构建中测量 Compose 性能——调试运行会使用 Live Literals 解释执行,所得数据不具代表性。请交叉参阅
../testing-compose-in-release-mode/SKILL.md。 - 必须验证配置文件已打包进 APK:运行
./gradlew :app:assembleRelease后,assets/dexopt/baseline.prof必须存在。如果不存在,说明 app 模块缺少baselineProfile(project(":baselineprofile"))配置。 - 必须报告各次迭代的中位数,而非平均值。单次因温控降频而变慢的运行会显著影响平均值。
- 优先建议:
StartupTimingMetric至少进行 10 次迭代,FrameTimingMetric至少进行 5 次迭代。 - **优先建议:**保留一个固定使用
CompilationMode.None的 A/B 对照测试,以便每个 PR 都能证明配置文件仍然有效。 - **优先建议:**在 Compose 源码中添加
Modifier.testTag("feed")(或流程使用的其他 ID),而不要依赖文本匹配器——文本会随本地化而变化,测试标签不会。
验证
- 存在
:baselineprofile模块,并已应用androidx.baselineprofile插件。 -
:app/build.gradle.kts的依赖项中包含baselineProfile(project(":baselineprofile"))。 - 运行
./gradlew :app:generateBaselineProfile后,会在app/src/<variant>/generated/baselineProfiles/(或app/src/main/generated/baselineProfiles/)下生成baseline-prof.txt。 - 在发布构件上选择 Build → Analyze APK,可看到
assets/dexopt/baseline.prof和assets/dexopt/baseline.profm。 - 生成器
@Test涵盖冷启动和至少一次滚动快速滑动。 - 测量用的
@Test使用CompilationMode.Partial(BaselineProfileMode.Require)。 - 从仅在屏幕有意义地完成渲染时才显示内容的可组合项中调用
ReportDrawn/ReportDrawnWhen/ReportDrawnAfter。 - Macrobench 结果——启动至少 10 次迭代、滚动至少 5 次迭代的中位数——与
CompilationMode.None相比显示出可测量的差异。 - 测量在低端实体设备或
aosp_cf_x86_64_phone-userdebug上进行,而不是在原生 API 模拟器上进行。 - 报告中未将调试构建的数据用作任何证据。
参考资料
- Baseline Profiles 概览 — https://developer.android.com/topic/performance/baselineprofiles/overview
- 在 Compose 中使用 Baseline Profiles — https://developer.android.com/develop/ui/compose/performance/baseline-profiles
- 使用 Macrobenchmark 对 Baseline Profiles 进行基准测试 — https://developer.android.com/topic/performance/baselineprofiles/measure-baselineprofile
- Macrobenchmark 库 — https://developer.android.com/topic/performance/benchmarking/macrobenchmark-overview
androidx.activity.compose.ReportDrawn参考 — https://developer.android.com/reference/kotlin/androidx/activity/compose/package-summary- Ben Trengrove,《为什么应该始终在 release 模式下测试 Compose 性能》— https://medium.com/androiddevelopers/why-should-you-always-test-compose-performance-in-release-4168dd0f2c71
- skydoves,《使用 Baseline Profiles 提升 Android 应用性能》(Stream 博客)— https://getstream.io/blog/android-baseline-profile/
- Compose 性能概览 — https://developer.android.com/develop/ui/compose/performance
references/macrobenchmark-harness.md— 完整的 Gradle + manifest 测试框架设置、指标目录(StartupTimingMetric、FrameTimingMetric、TraceSectionMetric、MemoryUsageMetric、PowerMetric),以及如何解析 JSON 输出以供 CI 仪表板使用。
如需确认生成的配置文件是否确实改变了 release 模式下某个特定可组合项的重组次数,请参阅 ../tracing-recompositions-at-runtime/SKILL.md。如需了解为什么 debug 数值不能作为测量依据,请参阅 ../testing-compose-in-release-mode/SKILL.md。如需了解 release 测量所依赖的构建时 R8 设置,请参阅 ../../build/configuring-r8-for-compose/SKILL.md。如需了解如何防止不同配置文件生成轮次之间出现稳定性回归,请参阅 ../../stability/enforcing-stability-in-ci/SKILL.md。