mcpField
T
testing-compose-in-release-mode

testing-compose-in-release-mode

github.com/skydoves测试与质量520131 次安装19 周前更新
代码仓库

关于 testing-compose-in-release-mode

面向 Android Compose 开发者的技能指南,帮助在带符号的 Release 构建中测量性能,避免依据 Debug 数据作出结论。涵盖编译器报告、Macrobenchmark、重组追踪和设备选择。

主要功能

  1. 01配置启用 R8 的 Release 测量构建
  2. 02读取 Release Compose 编译器报告
  3. 03使用 Macrobenchmark 测量启动与帧性能
  4. 04以 @TraceRecomposition 核实重组次数
  5. 05记录设备与构建配置等测量背景

适用场景

  1. 01为性能缺陷报告验证测量数据
  2. 02为 CI 性能门禁设计基准测试
  3. 03比较代码变更前后的启动与帧时间

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/skydoves/compose-performance-skills --skill testing-compose-in-release-mode

安装后在 Claude Code 中输入/testing-compose-in-release-mode确认已加载。

详细介绍

在发布模式下测试 Compose——调试构建会掩盖真实性能

Compose 以未打包的形式提供——其 UI 运行时从应用代码加载,在调试模式下以解释方式运行,而 Live Literals 插件会将每个常量都转换为 getter。调试模式下的重组次数、启动时间和编译器报告,并不代表用户的实际体验。本技能将教 Claude 如何设置带符号的发布构建,以便在提出任何性能结论之前进行真实测量。

何时使用此技能

  • 开发者报告了一个性能数据(“启动需要 1.4s”“这个可组合项每次滚动会重组 50 次”),但该数据来自调试构建、针对调试构建使用的 Layout Inspector,或 CompilationMode.None。
  • 开发者准备提交性能 Bug、发布回归报告,或在 PR 中发布基准测试数据。
  • 正在设计 CI 性能门禁(Macrobenchmark、基线配置文件差异、帧时长预算)。
  • 开发者询问“为什么我的基准测试数据比在 Play Store 上看到的差这么多?”。
  • 即将查看 Compose Compiler 报告——这些报告必须来自发布构建输出目录,否则所有结论都值得怀疑。

何时不要使用此技能

  • 性能尚未纳入考量的功能开发——构建功能期间,调试模式下的迭代速度足够快。
  • 行为测试(正确性、集成测试、屏幕的插桩测试)——这与性能测试不同,应继续在调试模式下进行。
  • 配置 R8 本身——请参阅 ../../build/configuring-r8-for-compose/SKILL.md。
  • 根据基准测试运行生成 Baseline Profile——请参阅 ../generating-baseline-profiles/SKILL.md。
  • 阅读 Compose Compiler 报告中的具体行——请参阅 ../../stability/diagnosing-compose-stability/SKILL.md。

先决条件

  • 一个包含 Compose UI 的可运行 Android 项目。
  • 发布签名配置。用于测量的发布模式构建可以使用调试签名(在发布类型中使用 signingConfig signingConfigs.debug),但未签名的发布构建无法安装。
  • Kotlin 2.0+,并使用 org.jetbrains.kotlin.plugin.compose Gradle 插件。
  • 一台用于运行应用的实体设备。模拟器和 Cuttlefish 的数据不能与设备上的数据互换。
  • 熟悉 Macrobenchmark 和 Baseline Profiles 会有所帮助,但并非必需。

为什么调试模式会掩盖真实性能——四种机制

如果开发者反驳说“但我的数据看起来很真实”,请向其说明以下几点:

  1. 以解释方式运行的 Compose 运行时。 Compose UI 是常规依赖项,而非平台的一部分。在调试模式下,大部分运行时会以解释方式执行,并在应用启动后的最初几秒内进行 JIT 预热。R8 在发布模式下会执行 lambda 分组、移除 sourceInformation() 调用、对 ComposerImpl 进行去虚拟化,并对可组合项参数进行常量折叠。
  2. Live Literals。 Live Literals 编译器插件(默认在 Android 调试模式下启用,供 Android Studio 实时编辑使用)会将每个字面量——0.dp、"Hello"、Color.Red——都包装在 getter 中。重组器会将这些值视为动态值,因此即使源代码只使用常量,报告也会将可组合项标记为具有不稳定参数。
  3. 没有 R8/未启用 full mode。 即使在调试模式下启用了 R8,其配置也会较为宽松。未启用 R8 full mode 时,lambda 分配仍会保留,sourceInformation 字符串也不会移除,每帧的开销预算会高于发布版本。
  4. Layout Inspector 的近似数据。 Layout Inspector 的重组次数只是近似值——它按时间间隔采样,因此会漏掉中间的工作。Live Literals 也可能使这些次数虚高。仅限调试模式的计数仅供诊断参考,并非权威数据。

引用的测量结果:从调试模式切换到启用 R8 的发布模式后,启动性能约提升 75%,帧渲染性能约提升 60%。来源:Ben Trengrove,《Why should you always test Compose performance in release》(Android Developers Medium)。

工作流

  • 1. 为 release 变体配置 R8。 这是基础;没有它,其余工作流都无从谈起。完整的保留规则说明见 ../../build/configuring-r8-for-compose/SKILL.md。最低配置:
android {
    buildTypes {
        release {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro",
            )
            // Use debug signing if there is no release keystore yet — the build still installs.
            signingConfig = signingConfigs.getByName("debug")
        }
    }
}
  • 2. 确认用于测量的变体未启用 Live Literals。 新版 Compose Compiler Gradle DSL(Kotlin 2.0+)中已不再提供 Live Literals——featureFlags 枚举仅公开 IntrinsicRemember、OptimizeNonSkippingGroups、PausableComposition 和 StrongSkipping。在旧版 Compose Compiler 1.5.x 扩展中,该属性名为 liveLiterals,并已标记为弃用。测量时,请构建 release 变体——Live Literals 默认在 release 中关闭。如果使用单独的“benchmark”构建类型(推荐,即去除签名限制的 release),它会继承 release 的默认设置。

  • 3. 从 release 输出目录读取 Compose Compiler 报告。 务必如此。如何解读报告,请参阅 ../../stability/diagnosing-compose-stability/SKILL.md。

./gradlew :app:assembleRelease -PcomposeCompilerReports=true
ls app/build/compose_compiler/
# app_release-classes.txt, app_release-composables.txt, app_release-composables.csv, app_release-module.json
  • 4. 对于 Macrobenchmark,请使用 CompilationMode.Partial(BaselineProfileMode.Require),并将被测应用的 release 变体作为目标。 benchmark 模块本身仍使用自己的变体;目标应用必须是 release。另请参阅 ../generating-baseline-profiles/SKILL.md。
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
    @get:Rule val rule = MacrobenchmarkRule()

    @Test fun startupRelease() = rule.measureRepeated(
        packageName = "com.example",
        metrics = listOf(StartupTimingMetric()),
        iterations = 10,
        startupMode = StartupMode.COLD,
        compilationMode = CompilationMode.Partial(BaselineProfileMode.Require),
    ) {
        pressHome()
        startActivityAndWait()
    }
}
  • 5. Layout Inspector 的计数只是近似值——请使用 @TraceRecomposition 在 release 中确认。 Layout Inspector 的重组计数仅是 debug 模式下的便捷功能。如需获得权威计数,请在 release-with-symbols 构建中使用 skydoves 的 compose-stability-analyzer 中的 @TraceRecomposition。另请参阅 ../tracing-recompositions-at-runtime/SKILL.md。

  • 6. 如需在不进行插桩的情况下分析运行时性能,请使用 release-with-debug-symbols 构建。 添加以下内容,以便 simpleperf 和 Android Studio Profiler 能在 release 变体中解析 native(NDK) 栈帧:

android {
    buildTypes {
        release {
            isMinifyEnabled = true
            // NDK / native symbol level only — controls .so debug symbol packaging.
            ndk { debugSymbolLevel = "FULL" }
        }
    }
}

ndk { debugSymbolLevel = "FULL" } 仅控制 NDK / native 符号打包;不会影响 Kotlin / Java 栈帧的可读性。Kotlin 栈帧的可读性来自 mapping.txt,当 isMinifyEnabled = true 时,R8 始终会生成该文件。将 native 性能分析与 R8 retrace 配合使用(参见 ../../build/configuring-r8-for-compose/SKILL.md),即可将混淆后的 Kotlin 堆栈映射回源代码行。

  • 7. 选择设备。 benchmark 运行的优先顺序:

    1. 实体低端设备(例如 Pixel 4a、中端合作伙伴设备)——这最能反映真实用户的感受。
    2. Cuttlefish——可预测,但无法代表 GPU / 热行为。
    3. Emulator——仅作为最后的选择,绝不可用于帧耗时预算。
  • 8. 报告任何数据时,同时注明变体、设备和 R8 状态。 不注明“release / R8 已开启 / Pixel 6 / 冷启动”的性能数据无法审核。请将这些信息纳入 bug 报告模板。

模式

模式:从 debug 中读取编译器报告
# WRONG
./gradlew :app:assembleDebug
cat app/build/compose_compiler/debug/app-composables.txt
# WRONG because: debug enables Live Literals which turns every constant into a getter, so reports show false-positive unstable params and inflate non-skippable counts.
# RIGHT
./gradlew :app:assembleRelease -PcomposeCompilerReports=true
cat app/build/compose_compiler/app_release-composables.txt
模式:以 debug 目标运行 Macrobenchmark
// WRONG
rule.measureRepeated(
    packageName = "com.example",
    metrics = listOf(StartupTimingMetric()),
    iterations = 5,
    startupMode = StartupMode.COLD,
    compilationMode = CompilationMode.None,
)
// WRONG because: CompilationMode.None disables AOT compilation and the target app may also be the debug variant — interpreted Compose stack plus JIT warmup makes startup numbers 3-4x worse than what users see, and any regression diff is dominated by JIT noise.
// RIGHT
rule.measureRepeated(
    packageName = "com.example",
    metrics = listOf(StartupTimingMetric()),
    iterations = 10,
    startupMode = StartupMode.COLD,
    compilationMode = CompilationMode.Partial(BaselineProfileMode.Require),
)
// And the targetPackage points to the release variant of the app under test.
模式:相信 debug Layout Inspector 的计数
// WRONG
// "Layout Inspector says ProductCard recomposes 50 times per scroll, so the bug is real
// and we need to mark every parameter @Stable."
// WRONG because: Layout Inspector counts are sampled and approximate; Live Literals can inflate them; the count may double or halve in release. Confirm with @TraceRecomposition or Macrobenchmark FrameTimingMetric in a release build before chasing a fix.
// RIGHT
// Step 1: Reproduce in release build with @TraceRecomposition on the suspect composable.
// Step 2: If the release-build trace still shows the count, then diagnose with
//         ../../stability/diagnosing-compose-stability/SKILL.md against the release reports.
// Step 3: Fix, then re-measure with FrameTimingMetric in Macrobenchmark.
@TraceRecomposition(traceStates = true)
@Composable
fun ProductCard(product: Product) { /* ... */ }
模式:测量 release 变体——Live Literals 默认在其中关闭
// WRONG
// "I ran my benchmark against the debug variant — Live Literals was on, so my recomposition
//  counts and frame timings were inflated, and I cannot trust the report."
// WRONG because: Live Literals is on in debug to support Android Studio's live edit; it wraps
// constants in getters that the recomposer treats as dynamic. Stability reports and recomposition
// counts from a debug build are not measurement evidence.
// RIGHT — measure release; Live Literals is off there by default in the new (Kotlin 2.0+)
// Compose Compiler Gradle DSL. The featureFlags enum exposes IntrinsicRemember,
// OptimizeNonSkippingGroups, PausableComposition, and StrongSkipping — there is no LiveLiterals
// flag to set. Just build the release variant and read the release reports:
composeCompiler {
    reportsDestination = layout.buildDirectory.dir("compose_compiler")
    metricsDestination = layout.buildDirectory.dir("compose_compiler")
}
模式:引用性能数据却不注明出处
// WRONG
// "Startup is 1420 ms after my change."
// WRONG because: no variant, no device, no compilation mode, no iteration count — unreviewable. The same code measures 460 ms in release with R8 + a baseline profile on a Pixel 6.
// RIGHT
// "Startup TimeToInitialDisplay p50 = 460 ms (release, R8 on, baseline profile required,
//  Pixel 6 stock, cold start, 10 iterations, Macrobenchmark)."

强制规则

  • 必须在真实物理设备上使用启用 R8 的 release 变体测量 Compose 性能。debug、CompilationMode.None 或仅在模拟器上运行得到的数据,最多只能用于诊断。
  • 必须从 release 输出目录(build/compose_compiler/<module>_release-*)读取 Compose Compiler 报告。debug 报告会被 Live Literals 破坏。
  • 不得报告 debug 构建的性能数据,除非在同一句中明确注明“debug 构建,请将数据视为近似值”。
  • 不得在任何测量中信任 Live Literals 对常量的处理方式。请测量 release——在新版(Kotlin 2.0+)Compose Compiler DSL 中,Live Literals 在 release 中默认关闭,而且它已不再是可切换的功能标志。
  • 必须在报告任何启动或帧时序数据时,同时注明变体、设备、编译模式和迭代次数。
  • 不得将 Layout Inspector 的重组计数(采样所得、近似值、仅限 debug)等同于 @TraceRecomposition 计数(确定性、运行时插桩)。任何结论都应使用后者。
  • **优先选择:**物理低端设备 > Cuttlefish > 模拟器来运行基准测试。低端设备的温度和 GPU 负载范围能够暴露真实的卡顿。
  • **优先选择:**在 release 变体中保留 ndk { debugSymbolLevel = "FULL" },这样无需重新构建,simpleperf、Android Studio Profiler 和崩溃回溯就能正常工作。

验证

  • 正在测量的变体是 release(或基于 release 派生的构建类型),而不是 debug。
  • 该变体设置了 isMinifyEnabled = true,并包含 proguard-android-optimize.txt。
  • 测量变体是 release(在 Kotlin 2.0+ Compose Compiler DSL 中,Live Literals 在该变体中默认关闭——没有可切换的 LiveLiterals 功能标志)。
  • Compose Compiler 报告文件名带有 _release 后缀(例如 app_release-composables.txt)。
  • Macrobenchmark 使用 CompilationMode.Partial(BaselineProfileMode.Require),且运行时安装的目标应用是 release 变体。
  • 报告的数据包含变体、设备、编译模式和迭代次数。
  • 如果重组相关结论来自 Layout Inspector,则已通过 release 构建中的 @TraceRecomposition(或 Macrobenchmark FrameTimingMetric)重新确认该结论。

参考资料

评论

支持 Markdown