mcpField
G
generating-baseline-profiles

generating-baseline-profiles

github.com/skydoves开发520140 次安装19 周前更新
代码仓库

关于 generating-baseline-profiles

面向 Android 开发者的 Baseline Profile 生成与测量技能,指导为启动和滚动路径生成 AOT 配置,并用 Macrobenchmark 验证性能变化。

主要功能

  1. 01创建 Baseline Profile Generator 模块
  2. 02为启动和滚动路径生成配置
  3. 03将配置打包进 APK
  4. 04用 Macrobenchmark 测量启动与帧耗时
  5. 05通过 ReportDrawn 测量完整显示时间

适用场景

  1. 01改善应用冷启动
  2. 02减少首次滚动卡顿
  3. 03验证性能修复是否改变指标
  4. 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.baselineprofile Gradle 插件和 :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.baselineprofile Gradle 插件。
  • 一个包含 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-userdebug Cuttlefish 上进行测量。高端设备会掩盖性能差异;默认 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 模拟器上进行。
  • 报告中未将调试构建的数据用作任何证据。

参考资料

如需确认生成的配置文件是否确实改变了 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。

评论

支持 Markdown