mcpField
G
generating-baseline-profiles

generating-baseline-profiles

github.com/skydoves開発520140 回インストール19 週間前に更新
リポジトリ

generating-baseline-profiles について

Android 開発者向けの Baseline Profile 生成・測定スキルです。起動とスクロールの操作経路をプロファイルし、APK への同梱と Macrobenchmark による性能確認を案内します。

主な機能

  1. 01Baseline Profile Generator モジュールの設定
  2. 02起動とスクロール経路のプロファイル生成
  3. 03プロファイルの APK への同梱
  4. 04Macrobenchmark による起動とフレーム時間の測定
  5. 05ReportDrawn による完全表示時間の測定

活用シーン

  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

詳細

ベースラインプロファイルの生成 — AOT コンパイルのヒントリストを同梱し、効果を実証する

ベースラインプロファイルは、AOT コンパイルのヒントリストを APK に同梱します。これにより、ART は初回実行時の JIT に頼らず、インストール時にホットな Compose コードパスを事前コンパイルします。報告されている効果は、プロファイルを作成した操作で、コールドスタートアップが約 30%高速化し、初回スクロールが 40%滑らかになることです。Compose はプラットフォームからバンドルされずに提供されるため、すべての Compose UI アプリがこの恩恵を受けます。システムイメージによって Compose がすでに AOT コンパイルされている Android のバージョンはありません。

このスキルは、パフォーマンス改善作業全体の計測の基盤です。プロファイルの生成には androidx.benchmark:benchmark-macro-junit4 の BaselineProfileRule を使用し、計測には同じアーティファクトの MacrobenchmarkRule を使用します。生成と計測は、別々の @Test ファイルで行います。これらは、AGP 8.2 以降で New Module → Baseline Profile Generator を選択すると作成される、独立した :baselineprofile モジュールに配置します。

このスキルを使用する場合

  • コールドスタートアップが遅く、開発者が初回起動時にホットパスを AOT コンパイルしたい場合。
  • 安定性の修正後も、実機での LazyColumn / LazyVerticalGrid の初回スクロールがカクつく場合。
  • リリースビルドを準備しており、開発者がリリース前にパフォーマンスのベースラインを必要としている場合。
  • 安定性または強力なスキップの修正が、コールドスタートアップやフレームタイミングの数値を実際に改善したか検証する場合。Macrobenchmark が基準となります。
  • ユーザーが「baseline profile」、「macrobenchmark」、「TTFD」、「time-to-fully-drawn」、「StartupTimingMetric」、「FrameTimingMetric」、「CompilationMode」、「aosp_cf_x86_64_phone-userdebug」、または「ReportDrawn」に言及した場合。

このスキルを使用しない場合

  • まだ初期のプロトタイピング段階で、リリース可能なユーザージャーニーがない場合。ベースラインプロファイルはホットパスを記録するため、画面がまだ頻繁に変更されている段階では記録するホットパスがありません。
  • 開発者がコンポーザブルごとの再コンポジション回数を知りたい場合。それには ../../recomposition/debugging-recompositions/SKILL.md(Layout Inspector によるデバッグ)、または ../tracing-recompositions-at-runtime/SKILL.md(リリース版の @TraceRecomposition)を使用します。
  • 開発者が安定性のリグレッションに対する CI ゲートを設けたい場合。それには ../../stability/enforcing-stability-in-ci/SKILL.md を使用します。ベースラインプロファイルは実行時を計測し、stabilityCheck はコンパイル時のスキップ可能性を計測します。
  • デバッグビルドから数値を収集している場合。デバッグビルドは Live Literals を有効にしてインタープリター実行されるため、代表性のない数値になります。../testing-compose-in-release-mode/SKILL.md を参照してください。

前提条件

  • AGP 8.2 以降。これにより New Module → 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: 名前空間のプレフィックスは必須です。これがないと、マニフェストのマージに失敗します。
  • 起動以外に、少なくとも 1 つのスクロール操作を特定していること。フィードリスト、ページ分割されたグリッド、詳細画面を開く操作などです。起動だけをプロファイリングしても、初回スクロールは未コンパイルのままです。
  • 起動の大部分が初期コンポジションではなく、回避可能な再コンポジションによって占められている場合に備え、../../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 によって、以下がスキャフォールディングされます。

  • androidx.baselineprofile Gradle プラグインが適用された新しい :baselineprofile モジュール。
  • 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")) }

<profileable> を含む Gradle とマニフェストの設定全体については、references/macrobenchmark-harness.md を参照してください。

2. 起動とスクロールの両方を含むジェネレーターを作成する

ジェネレーターは、BaselineProfileRule.collect を使用する @Test です。起動に加え、少なくとも 1 回のスクロールを必ず含めてください。起動だけのプロファイルでは、最初のスクロールがコールド状態のままになります。

@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()
    }
}

By.res("feed") が解決されるように、Compose のソースで Modifier.testTag("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 にプロファイルが含まれていることを確認する

Build → Analyze APK でリリース版の .apk / .aab を開き、次の内容を確認します。

assets/dexopt/baseline.prof
assets/dexopt/baseline.profm

これらのファイルがない場合、プロファイルは生成されていますが、パッケージには含まれていません。通常は、アプリモジュールの 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 比較を行うには、compilationMode = CompilationMode.None を指定した同様の @Test を追加し、中央値を比較します。プロファイルによって数値が改善したことを証明する方法はこれだけです。

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. 正確な TTFD 計測のために ReportDrawn を設定する

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 } — suspend ブロックの完了後に呼び出します(典型的なケースは、明示的な awaitFirstFrame() です)。
8. 実機で実行し、中央値を比較して反復する

接続した実機、または Cuttlefish の aosp_cf_x86_64_phone-userdebug で ./gradlew :baselineprofile:connectedReleaseAndroidTest を実行します。平均値ではなく、反復実行の中央値を報告します。Macrobenchmark は両方をログに出力しますが、平均値は熱によるスロットリングが発生した 1 回の実行に影響されやすいためです。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)

必須ルール

  • 必須: 起動と、少なくとも 1 つのスクロール操作の両方で Baseline Profile を生成します。起動のみのプロファイルでは、最初のスクロールがコールド状態のままになります。
  • 必須: 実際のローエンド実機または Cuttlefish の aosp_cf_x86_64_phone-userdebug で計測します。ハイエンド端末ではパフォーマンスの差が見えにくく、デフォルトの 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")) の配線が不足しています。
  • 必須: 平均値ではなく、反復実行の中央値を報告します。平均値は、熱によるスロットリングが発生した 1 回の実行に影響されやすいためです。
  • 推奨: 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 で、コールドスタートアップと少なくとも 1 回のスクロールフリングがカバーされている。
  • 計測用の @Test で CompilationMode.Partial(BaselineProfileMode.Require) が使われている。
  • 画面が意味のある状態まで完成したときのみ描画されるコンポーザブルから、ReportDrawn/ReportDrawnWhen/ReportDrawnAfter が呼び出されている。
  • Macrobenchmark の結果で、起動は 10 回以上、スクロールは 5 回以上の反復の中央値が、CompilationMode.None に対して測定可能な差を示している。
  • 計測は、標準の API エミュレーターではなく、ローエンド実機または aosp_cf_x86_64_phone-userdebug で実行されている。
  • レポート内で、デバッグビルドの数値が根拠として使われていない。

参考資料

生成されたプロファイルによって、リリースビルドで特定のコンポーザブルの再コンポーズ回数が実際に変化したことを確認するには、../tracing-recompositions-at-runtime/SKILL.md を参照してください。デバッグビルドの数値が測定の根拠にならない理由については、../testing-compose-in-release-mode/SKILL.md を参照してください。リリース測定で前提となるビルド時の R8 設定については、../../build/configuring-r8-for-compose/SKILL.md を参照してください。プロファイル生成を繰り返す間に安定性のリグレッションが起きるのを防ぐには、../../stability/enforcing-stability-in-ci/SKILL.md を参照してください。

コメント