mcpField
T
testing-compose-in-release-mode

testing-compose-in-release-mode

github.com/skydovesテスト・品質520131 回インストール19 週間前に更新
リポジトリ

testing-compose-in-release-mode について

Android Compose 開発者向けに、Debug の数値に頼らず、シンボル付き Release ビルドで性能を測るためのスキルです。コンパイラーレポート、Macrobenchmark、再コンポジションの追跡、デバイス選定を扱います。

主な機能

  1. 01R8 を有効にした Release 測定ビルドの設定
  2. 02Release の Compose コンパイラーレポートの確認
  3. 03Macrobenchmark による起動・フレーム測定
  4. 04@TraceRecomposition による再コンポジション数の検証
  5. 05デバイスやビルド条件など測定情報の記録

活用シーン

  1. 01性能バグ報告の測定値を検証
  2. 02CI 性能ゲートの設計
  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 プラグインにより、すべての定数がゲッターに変換されます。デバッグ時の再コンポジション回数、起動時間、コンパイラーレポートは、ユーザーが実際に体験するものとは異なります。このスキルでは、パフォーマンスに関する主張をする前に、正確な測定のためのシンボル付きリリースビルドを設定する方法を Claude に説明します。

このスキルを使用するタイミング

  • 開発者が、デバッグビルド、デバッグに対する Layout Inspector、または CompilationMode.None から得たパフォーマンスの数値(「起動に 1.4 秒かかる」「このコンポーザブルはスクロールのたびに 50 回再コンポジションされる」など)を報告した場合。
  • 開発者がパフォーマンスのバグを登録しようとしている場合、リグレッションレポートを作成しようとしている場合、または PR にベンチマークの数値を投稿しようとしている場合。
  • CI のパフォーマンスゲート(Macrobenchmark、ベースラインプロファイルの差分、フレームタイミングの予算)を設計している場合。
  • 開発者が「ベンチマークの数値が Play Store で見るものよりずっと悪いのはなぜですか?」と尋ねた場合。
  • Compose Compiler のレポートを読み取ろうとしている場合 — 必ずリリースの出力ディレクトリから取得する必要があります。そうでなければ、すべての結論が疑わしくなります。

このスキルを使用しないタイミング

  • パフォーマンスが対象になる前の機能開発 — 機能を構築している間は、デバッグ時の反復速度で問題ありません。
  • 動作テスト(正確性、統合、画面のインストルメンテーションテスト) — これはパフォーマンステストとは別のものであり、デバッグで行います。
  • R8 自体のビルド設定 — ../../build/configuring-r8-for-compose/SKILL.md を参照してください。
  • ベンチマーク実行からのベースラインプロファイルの生成 — ../generating-baseline-profiles/SKILL.md を参照してください。
  • Compose Compiler レポートの個々の行の読み取り — ../../stability/diagnosing-compose-stability/SKILL.md を参照してください。

前提条件

  • Compose UI を使用する Android プロジェクトが動作していること。
  • リリース署名設定。測定にはデバッグ署名を使ったリリースモードのビルドでも構いません(リリースタイプで signingConfig signingConfigs.debug を使用してください)が、署名されていないリリースビルドはインストールできません。
  • org.jetbrains.kotlin.plugin.compose Gradle プラグインを使用する Kotlin 2.0 以降。
  • 実行に使用する実機。エミュレーターや Cuttlefish の数値は、実機での数値と互換性がありません。
  • Macrobenchmark とベースラインプロファイルの知識があると役立ちますが、必須ではありません。

デバッグ時の測定が不正確になる 4 つの仕組み

開発者が「でも、この数値は実際のものに感じます」と反論したときは、以下の点を説明してください。

  1. Compose ランタイムのインタープリター実行。 Compose UI はプラットフォームの一部ではなく、通常の依存関係です。デバッグでは、ランタイムの多くがインタープリター実行され、アプリ起動後の最初の数秒にわたって JIT によるウォームアップが発生します。リリースでは、R8 がラムダのグループ化、sourceInformation() 呼び出しの除去、ComposerImpl の脱仮想化、コンポーザブル引数の定数畳み込みを行います。
  2. Live Literals。 Live Literals コンパイラープラグイン(Android Studio のライブ編集のために、デバッグではデフォルトで有効)は、0.dp、"Hello"、Color.Red など、すべてのリテラルをゲッターでラップします。再コンポーザーはこれらを動的な値として扱うため、ソースコードで定数しか使っていない場合でも、レポートではコンポーザブルが不安定なパラメーターを受け取っていると判定されます。
  3. R8 なし/フルモードなし。 デバッグで R8 を有効にしたとしても、設定は弱いものになります。R8 のフルモードがなければ、ラムダの割り当ては残り、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. リリースバリアントで R8 を設定します。 これが基盤です。設定しなければ、残りのワークフローは意味を成しません。keep ルールの詳細については、../../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 が有効になっていないことを確認します。 Live Literals は新しい Compose Compiler Gradle DSL(Kotlin 2.0 以降)には存在しません。featureFlags 列挙型が公開するのは IntrinsicRemember、OptimizeNonSkippingGroups、PausableComposition、StrongSkipping のみです。従来の Compose Compiler 1.5.x 拡張では、プロパティ名は liveLiterals で、非推奨とマークされていました。測定にはリリースバリアントをビルドしてください。Live Literals はリリースではデフォルトでオフです。別の「benchmark」ビルドタイプを使う場合(推奨:署名の制約を除いたリリース)、リリースのデフォルト設定を引き継ぎます。

  • 3. リリースの出力ディレクトリから 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) を使用します。 ベンチマークモジュール自体は独自のバリアントのままです。対象アプリはリリースである必要があります。../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 を使ってリリースで確認してください。 Layout Inspector の再コンポジション数表示は、デバッグ専用の簡易機能です。信頼できるカウントを得るには、skydoves の compose-stability-analyzer にある @TraceRecomposition を、シンボル付きリリースビルドに対して使用してください。../tracing-recompositions-at-runtime/SKILL.md を相互参照してください。

  • 6. 計測を行わずにランタイムをプロファイリングする場合は、デバッグシンボル付きリリースビルドを使用します。 リリースバリアントの**ネイティブ(NDK)**フレームを simpleperf と Android Studio Profiler で解決できるよう、次を追加してください。

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

ndk { debugSymbolLevel = "FULL" } はNDK/ネイティブシンボルのパッケージ化のみを制御します。Kotlin/Java フレームの読みやすさには影響しません。Kotlin フレームの読みやすさは mapping.txt によって決まり、isMinifyEnabled = true の場合、R8 は常にこれを生成します。ネイティブのプロファイリングと R8 retrace(../../build/configuring-r8-for-compose/SKILL.md を参照)を組み合わせ、難読化された Kotlin スタックをソース行に対応付けてください。

  • 7. デバイスを選びます。 ベンチマーク実行時の優先順位は次のとおりです。

    1. 低価格帯の実機(例:Pixel 4a、パートナーのミッドレンジ端末)。実際のユーザーが体感するのはこの環境です。
    2. Cuttlefish。予測可能ですが、GPU/熱の挙動を代表するものではありません。
    3. エミュレーター。最後の手段としてのみ使用し、フレームタイミングの予算測定には決して使用しないでください。
  • 8. 数値を報告する際は、バリアント、デバイス、R8 の状態も併記します。 「release / R8 on / Pixel 6 / cold start」のような情報がないパフォーマンス数値はレビューできません。これをバグ報告テンプレートに含めてください。

パターン

パターン: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) { /* ... */ }
パターン:リリースバリアントを測定する — 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 を有効にしたリリースバリアントで、実機を使って Compose のパフォーマンスを測定してください。デバッグ、CompilationMode.None、またはエミュレーターのみでの実行による数値は、せいぜい診断用です。
  • 必須: Compose Compiler のレポートは、リリースの出力ディレクトリ(build/compose_compiler/<module>_release-*)から読み取ってください。デバッグレポートは Live Literals によって破損しています。
  • 必須ではありません: 同じ文中に「デバッグビルドのため、概算として扱ってください」という明示的な注意書きがない限り、デバッグビルドのパフォーマンス数値を報告しないでください。
  • 必須ではありません: どの測定でも、Live Literals の定数としての扱いを信用しないでください。リリースで測定してください。新しい(Kotlin 2.0 以降の)Compose Compiler DSL では、リリースで Live Literals はデフォルトでオフになっており、切り替え可能な機能フラグとしてのプロパティはなくなっています。
  • 必須: 起動時間またはフレームタイミングの数値を報告する際は、バリアント、デバイス、コンパイルモード、反復回数を併記してください。
  • 必須ではありません: Layout Inspector の再コンポーズ回数(サンプリングによる概算値で、デバッグのみ)を、@TraceRecomposition の回数(決定論的で、実行時に計測される)と同一視しないでください。結論を出す際は後者を使用してください。
  • 推奨: ベンチマークの実行には、実機のローエンド端末 > Cuttlefish > エミュレーターの順で優先してください。ローエンド端末の熱特性と GPU の動作範囲によって、実際のカクつきが明らかになります。
  • 推奨: リリースバリアントで ndk { debugSymbolLevel = "FULL" } を維持してください。再ビルドしなくても、simpleperf、Android Studio Profiler、クラッシュのスタックトレースの復元を利用できます。

検証

  • 測定対象のバリアントが debug ではなく、release(またはリリースから派生したビルドタイプ)である。
  • そのバリアントで 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) を使用し、実行対象としてインストールされているアプリがリリースバリアントである。
  • 報告する数値に、バリアント、デバイス、コンパイルモード、反復回数が含まれている。
  • 再コンポーズに関する主張の根拠が Layout Inspector の場合、その主張をリリースビルドで @TraceRecomposition(または Macrobenchmark の FrameTimingMetric)を使って再確認している。

参考資料

コメント