ベースライン プロファイルを使用してアプリのパフォーマンスを改善する

1. 始める前に

この Codelab では、ベースライン プロファイルを生成してアプリのパフォーマンスを最適化する方法と、ベースライン プロファイルの使用がもたらすパフォーマンス上のメリットを検証する方法を学びます。

前提条件

この Codelab は、Macrobenchmark ライブラリでアプリのパフォーマンスを測定する方法を説明している Macrobenchmark を使用してアプリのパフォーマンスを調査する Codelab を土台としています。

必要なもの

演習内容

  • パフォーマンスを最適化するためにベースライン プロファイルを生成する
  • Macrobenchmark ライブラリを使用してパフォーマンスの向上を確認する

学習内容

  • ベースライン プロファイルを生成する
  • ベースライン プロファイルによるパフォーマンスの向上を分析する

2. 設定方法

最初に、コマンドラインで次のコマンドを使用して、GitHub リポジトリのクローンを作成します。

$ git clone --branch baselineprofiles-main  https://github.com/googlecodelabs/android-performance.git
$ cd android-macrobenchmark-codelab

または、次の 2 つの zip ファイルをダウンロードします。

Android Studio でプロジェクトを開く

  1. [Welcome to Android Studio] ウィンドウで、c01826594f360d94.png [Open an Existing Project] を選択します。
  2. [Download Location]/android-performance-codelabs/benchmarking フォルダを選択します(ヒント: build.gradle が格納されている benchmarking ディレクトリを選択してください)。
  3. Android Studio にプロジェクトがインポートされたら、app モジュールを実行して、ベンチマーク対象のサンプルアプリをビルドできることを確認します。

3. ベースライン プロファイルとは

ベースライン プロファイルは、APK に含まれているクラスとメソッドのリストであり、アプリのインストール時に、マシンコードへのクリティカル パスをプリコンパイルするために Android ランタイム(ART)によって使用されます。プロファイルに基づく最適化(PGO)の一種であり、アプリの起動を最適化し、ジャンクを減らし、エンドユーザーにとってのパフォーマンスを向上させることを可能にします。

ベースライン プロファイルの仕組み

プロファイル ルールは、assets/dexopt/baseline.prof 内で APK にバイナリ形式でコンパイルされ、APK とともに(Google Play を通じて)ユーザーに直接配布されます。

アプリのインストール時に、ART はプロファイル内のメソッドを事前(AOT)コンパイルします。これにより、メソッドの実行速度が向上します。アプリの起動時またはフレームのレンダリング時に使用されるメソッドをプロファイルに含めると、起動時間の短縮やジャンクの減少により、ユーザー エクスペリエンスが向上します。

4. ベンチマーク モジュールを更新する

アプリ デベロッパーは、Jetpack Macrobenchmark ライブラリを使用して、ベースライン プロファイルを自動的に生成できます。ベースライン プロファイルを生成する際は、アプリのベンチマーク用に作成したのと同じモジュールを利用して変更を加えることができます。

ベースライン プロファイルの難読化を無効にする

アプリで難読化を有効にしている場合は、ベンチマークに対して難読化を無効にする必要があります。

そのためには、proguard ファイルを :app モジュールに追加し、そこで難読化を無効にして、proguard ファイルを benchmark buildType に追加します。

:app モジュール内に benchmark-rules.pro という名前の新しいファイルを作成します。このファイルは、モジュール固有の build.gradle ファイルの横にある /app/ フォルダに配置する必要があります。27bd3b1881011d06.png

次のスニペットに示すように、このファイルに -dontobfuscate を追加することにより、難読化を無効にします。

# Disables obfuscation for benchmark builds.
-dontobfuscate

次に、:app モジュール固有の build.gradlebenchmark buildType を変更し、作成したファイルを追加します。buildType として initWith release を使用しているので、この行により benchmark-rules.pro proguard ファイルがリリース proguard ファイルに追加されます。

buildTypes {
   release {
      // ...
   }

   benchmark {
      initWith buildTypes.release
      // ...
      proguardFiles('benchmark-rules.pro')
   }
}

それでは、ベースライン プロファイルのジェネレータ クラスを作成しましょう。

5. ベースライン プロファイルのジェネレータ クラスを作成する

通常は、アプリの一般的なユーザー ジャーニーについてベースライン プロファイルを生成します。

この例では、次の 3 つのジャーニーを確認できます。

  1. アプリを起動する(これはほとんどのアプリで必要不可欠です)。
  2. スナックリストをスクロールする
  3. スナックの詳細に移動する

ベースライン プロファイルを生成するため、:macrobenchmark モジュールに新しいテストクラス BaselineProfileGenerator を追加します。このクラスは BaselineProfileRule テストルールを使用し、プロファイルを生成するテストメソッドを 1 つ含みます。プロファイルを生成するためのエントリ ポイントは、collectBaselineProfile 関数です。必要なパラメータは次の 2 つのみです。

  • packageName(アプリのパッケージ)
  • profileBlock(最後のラムダ パラメータ)
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {

   @get:Rule
   val rule = BaselineProfileRule()

   @Test
   fun generate() {
       rule.collectBaselineProfile("com.example.macrobenchmark_codelab") {
           // TODO Add interactions for the typical user journeys
       }
   }
}

profileBlock ラムダでは、アプリの一般的なユーザー ジャーニーに対応するインタラクションを指定します。ライブラリは profileBlock を何度か実行し、最適化の対象である呼び出されたクラスと関数を収集して、デバイスにベースライン プロファイルを生成します。

次のスニペットで、一般的なジャーニーをカバーするベースライン プロファイル ジェネレータの概要を確認できます。

@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {

   @get:Rule
   val rule = BaselineProfileRule()

   @Test
   fun generate() {
       rule.collectBaselineProfile("com.example.macrobenchmark_codelab") {
           startApplicationJourney() // TODO Implement
           scrollSnackListJourney() // TODO Implement
           goToSnackDetailJourney() // TODO Implement
       }
   }
}

それでは、前述のジャーニーごとにインタラクションを記述しましょう。これを MacrobenchmarkScope の拡張関数として記述すると、それによって提供されるパラメータと関数にアクセスできます。そのように記述すると、ベンチマークに対するインタラクションを再利用して、パフォーマンスの向上を検証できます。

アプリを起動するジャーニー

アプリを起動するジャーニー(startApplicationJourney)では、次のインタラクションをカバーする必要があります。

  1. ホームボタンを押し、アプリの状態が「再起動」になったことを確認する
  2. デフォルトのアクティビティを開始し、最初のフレームがレンダリングされるのを待つ
  3. コンテンツが読み込まれてレンダリングされ、ユーザー操作が可能になるまで待つ
fun MacrobenchmarkScope.startApplicationJourney() {
   pressHome()
   startActivityAndWait()
   val contentList = device.findObject(By.res("snack_list"))
   // Wait until a snack collection item within the list is rendered
   contentList.wait(Until.hasObject(By.res("snack_collection")), 5_000)
}

リストをスクロールするジャーニー

スナックリストをスクロールするジャーニー(scrollSnackListJourney)では、次のインタラクションを実行できます。

  1. スナックリストの UI 要素を見つける
  2. システム ナビゲーションがトリガーされないように、ジェスチャー マージンを設定する
  3. リストをスクロールして UI が確定するまで待つ
fun MacrobenchmarkScope.scrollSnackListJourney() {
   val snackList = device.findObject(By.res("snack_list"))
   // Set gesture margin to avoid triggering gesture navigation
   snackList.setGestureMargin(device.displayWidth / 5)
   snackList.fling(Direction.DOWN)
   device.waitForIdle()
}

詳細に移動するジャーニー

最後のジャーニー(goToSnackDetailJourney)では、次のインタラクションを実装します。

  1. スナックリストを検索し、操作できるスナック アイテムをすべて見つける
  2. リストからアイテムを選択する
  3. アイテムをクリックし、詳細画面が読み込まれるまで待つ(スナックリストが画面に表示されなくなることを利用できます)
fun MacrobenchmarkScope.goToSnackDetailJourney() {
   val snackList = device.findObject(By.res("snack_list"))
   val snacks = snackList.findObjects(By.res("snack_item"))
   // Select random snack from the list
   snacks[Random.nextInt(snacks.size)].click()
   // Wait until the screen is gone = the detail is shown
   device.wait(Until.gone(By.res("snack_list")), 5_000)
}

以上で、ベースライン プロファイル ジェネレータを実行するために必要なインタラクションをすべて定義しました。しかし、それを実行するデバイスを定義する必要があります。

6. Gradle で管理されているデバイスを用意する

ベースライン プロファイルを生成するには、まず userdebug エミュレータを用意する必要があります。Gradle で管理されているデバイスを使用すると、ベースライン プロファイル作成プロセスを自動化できます。Gradle で管理されているデバイスについて詳しくは、ドキュメントをご覧ください。

まず、次のスニペットに示すように、:macrobenchmark モジュールの build.gradle ファイルで、Gradle で管理されているデバイスを定義します。

testOptions {
    managedDevices {
        devices {
            pixel2Api31(com.android.build.api.dsl.ManagedVirtualDevice) {
                device = "Pixel 2"
                apiLevel = 31
                systemImageSource = "aosp"
            }
        }
    }
}

ベースライン プロファイルを生成するには、root 権限のある Android 9(API 28)以上のデバイスを使用する必要があります。

この例では、Android 11(API レベル 31)を使用します。aosp システム イメージは、root 権限でのアクセスが可能です。

Gradle で管理されているデバイスでは、Android Emulator を手動で起動して破棄することなく、Android Emulator でテストを実行できます。定義を build.gradle に追加すると、新しい pixel2Api31[BuildVariant]AndroidTest タスクが実行可能になります。次のステップでは、このタスクを使用してベースライン プロファイルを生成します。

7. ベースライン プロファイル ジェネレータ テストを実行する

Gradle で管理されているデバイスを用意したら、ジェネレータ テストを開始できます。

実行構成からジェネレータを実行する

Gradle で管理されているデバイスでは、Gradle タスクとしてテストを実行する必要があります。すぐにテストを開始できるように、実行に必要なすべてのパラメータがタスクに指定された実行構成があらかじめ作成されています。

タスクを実行するには、generateBaselineProfile 実行構成を選択して [Run] ボタン 229e32fcbe68452f.png をクリックします。

8f6b7c9a5da6585.png

このテストは、先ほど定義したエミュレータ イメージを作成し、インタラクションを何度か実行した後、エミュレータを破棄して、結果を Android Studio に出力します。

4b5b2d0091b4518c.png

(省略可)コマンドラインからジェネレータを実行する

コマンドラインからジェネレータを実行する場合は、Gradle で管理されているデバイスによって作成されたタスク(:macrobenchmark:pixel2Api31BenchmarkAndroidTest)を利用できます。

このコマンドは、プロジェクト内のすべてのテストを実行して、失敗します。これは、後でパフォーマンスの向上を検証するために使用するベンチマークもモジュールに含まれているためです。

したがって、パラメータ -P android.testInstrumentationRunnerArguments.class を使用して実行するクラスをフィルタし、前に作成した com.example.macrobenchmark.BaselineProfileGenerator を指定します。

コマンド全体は次のようになります。

./gradlew :macrobenchmark:pixel2Api31BenchmarkAndroidTest -P android.testInstrumentationRunnerArguments.class=com.example.macrobenchmark.BaselineProfileGenerator

8. 生成したベースライン プロファイルを適用する

ジェネレータが正常に終了したら、ベースライン プロファイルをアプリで機能させるために、いくつかのことを行う必要があります。

まず、生成したベースライン プロファイルのファイルを src/main フォルダ(AndroidManifest.xml の横にあります)に配置します。ファイルを取得するには、次のスクリーンショットに示すように、/macrobenchmark/build/outputs/ にある managed_device_android_test_additional_output/ フォルダからコピーします。

b104f315f06b3578.png

または、Android Studio の出力に含まれている results リンクをクリックしてコンテンツを保存するか、出力に含まれている adb pull コマンドを使用します。

次に、ファイルの名前を baseline-prof.txt に変更します。

8973f012921669f6.png

続いて、:app モジュールに profileinstaller 依存関係を追加します。

dependencies {
  implementation("androidx.profileinstaller:profileinstaller:1.2.0-rc01")
}

この依存関係を追加すると、次のことが可能になります。

  • ローカルでベースライン プロファイルのベンチマークを行う。
  • クラウド プロファイルをサポートしていない Android 7(API レベル 24)と Android 8(API レベル 26)でベースライン プロファイルを使用する。
  • Google Play 開発者サービスがインストールされていないデバイスでベースライン プロファイルを使用する。

最後に、1079605eb7639c75.png アイコンをクリックして、プロジェクトを Gradle ファイルと同期します。

40cb2ba3d0b88dd6.png

次のステップでは、ベースライン プロファイルによってアプリのパフォーマンスがどれくらい向上するかを検証します。

9. 起動のパフォーマンスの向上を検証する

ここまでで、ベースライン プロファイルを生成してアプリに追加しました。それによってアプリのパフォーマンスに望ましい効果が現れるかどうかを検証しましょう。

アプリの起動時間を測定するベンチマークを含む ExampleStartupBenchmark クラスに戻ります。startup() テストを少し変更して、別のコンパイル モードで再利用できるようにする必要があります。これにより、ベースライン プロファイルを使用したときの違いを確認できます。

CompilationMode

CompilationMode パラメータは、アプリをマシンコードにプリコンパイルする方法を決定します。以下のオプションがあります。

  • DEFAULT - 可能な場合は、ベースライン プロファイルを使用して、アプリを部分的にプリコンパイルします(compilationMode パラメータが適用されない場合は、このオプションが使用されます)。
  • None() - アプリのコンパイル状態をリセットします。アプリのプリコンパイルは行いません。アプリの実行中は、引き続き実行時コンパイル(JIT)が有効です。
  • Partial() - ベースライン プロファイルでのアプリのプリコンパイルとウォームアップ実行を行うか、どちらかを行います。
  • Full() - アプリコード全体をプリコンパイルします。Android 6(API 23)以下では、これが唯一のオプションです。

アプリのパフォーマンスの最適化を開始するときは、DEFAULT コンパイル モードを選択できます。Google Play からアプリをインストールするときと似たパフォーマンスが得られるからです。ベースライン プロファイルがもたらすパフォーマンス上のメリットを確認するには、コンパイル モード NonePartial の結果を比較します。

異なる CompilationMode で起動テストを変更する

まず、startup メソッドから @Test アノテーションを削除し(JUnit テストにはパラメータを指定できないため)、compilationMode パラメータを追加して measureRepeated 関数で使用します。

// Remove @Test annotation and add the compilationMode parameter.
fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated(
   packageName = "com.google.samples.apps.sunflower",
   metrics = listOf(StartupTimingMetric()),
   iterations = 5,
   compilationMode = compilationMode, // Set the compilation mode
   startupMode = StartupMode.COLD
) {
   pressHome()
   startActivityAndWait()
}

これが完了したら、CompilationMode が異なる 2 つのテスト関数を追加します。1 つ目のテストでは CompilationMode.None を使用します。つまり、各ベンチマークの実行前にアプリの状態はリセットされ、アプリのコードはまったくプリコンパイルされません。

@Test
fun startupCompilationNone() = startup(CompilationMode.None())

2 つ目のテストでは CompilationMode.Partial を使用します。つまり、ベンチマークを実行する前にベースライン プロファイルを読み込み、プロファイルで指定されたクラスと関数をプリコンパイルします。

@Test
fun startupCompilationPartial() = startup(CompilationMode.Partial())

オプションとして、CompilationMode.Full を使用してアプリ全体をプリコンパイルする 3 つ目のメソッドを追加できます。Android 6(API 23)以下では、システムはプリコンパイルされたアプリのみを実行するので、これが唯一のオプションです。

@Test
fun startupCompilationFull() = startup(CompilationMode.Full())

次に、前と同様に(物理デバイスで)ベンチマークを実行し、Macrobenchmark が各種のコンパイル モードで起動時間を測定するのを待ちます。

ベンチマークが完了したら、次のスクリーンショットに示すように、Android Studio の出力で結果を確認できます。

cbbc9660374a438.png

スクリーンショットを見ると、CompilationMode に応じてアプリの起動時間が異なることがわかります。次の表に中央値を示します。

timeToInitialDisplay(ミリ秒)

timeToFullDisplay(ミリ秒)

None

364.4

846.5

Full

325.8

739.1

Partial

296.1

708.1

None モードの場合、デバイスはアプリの起動時にほとんどのコンパイルを JIT コンパイルで行わなくてはならないので、パフォーマンスが最も低くなることは直感的に理解できます。しかし、Full モードの場合にコンパイルのパフォーマンスが最も高くならないのは直感に反するように思えます。このモードではコード全体がコンパイルされるので、アプリの odex ファイルが非常に大きくなります。したがって、通常、アプリの起動時にシステムは IO を大幅に増やさなくてはなりません。パフォーマンスが最も高いのは、ベースライン プロファイルを使用する Partial の場合です。なぜなら、部分的なコンパイルでは、ユーザーが使用する頻度が高いコードをバランス良くコンパイルする一方で、重要でないコードはすぐに読み込まなくてもよいため、プリコンパイルしないからです。

10. スクロールのパフォーマンスを検証する

前のステップと同様に、スクロールのベンチマークを測定して検証できます。前と同様に ScrollBenchmarks クラスを変更しましょう。scroll テストにパラメータを追加し、別のコンパイル モード パラメータを使用するテストを追加します。

ScrollBenchmarks.kt ファイルを開き、scroll() 関数を変更して、compilationMode パラメータを追加します。

fun scroll(compilationMode: CompilationMode) {
        benchmarkRule.measureRepeated(
            compilationMode = compilationMode, // Set the compilation mode
            // ...

次に、別のパラメータを使用する複数のテストを定義します。

@Test
fun scrollCompilationNone() = scroll(CompilationMode.None())

@Test
fun scrollCompilationPartial() = scroll(CompilationMode.Partial())

前と同様にベンチマークを実行すると、次のスクリーンショットのような結果が得られます。249af52e917a4fcf.png

結果を見ると、CompilationMode.Partial ではフレーム時間が平均で 0.5 ミリ秒短くなっていることがわかります。これはユーザーが気づくほどの違いではありませんが、他のパーセンタイルでは結果に顕著な差があります。P90 では差は 11.7 ミリ秒で、フレーム生成の指定時間(Pixel 3 では約 16 ミリ秒)のおよそ 70% です。

11. 完了

お疲れさまでした。以上でこの Codelab は無事に終了し、ベースライン プロファイルを使用してアプリのパフォーマンスを改善できました。

次のステップ

performance-samples GitHub リポジトリをご覧ください。Macrobenchmark およびその他のパフォーマンスに関するサンプルが格納されています。また、Now In Android サンプルアプリもご覧ください。これは、ベンチマークとベースライン プロファイルを使用してパフォーマンスを改善できる実用的なアプリです。

リファレンス ドキュメント