1. 始める前に
この Codelab では、ベースライン プロファイルを生成してアプリのパフォーマンスを最適化する方法と、ベースライン プロファイルの使用がもたらすパフォーマンス上のメリットを検証する方法を学びます。
前提条件
この Codelab は、Macrobenchmark ライブラリでアプリのパフォーマンスを測定する方法を説明している Macrobenchmark を使用してアプリのパフォーマンスを調査する Codelab を土台としています。
必要なもの
- Android Studio Dolphin(2021.3.1)以降
- Kotlin に関する知識
- Jetpack Macrobenchmark に関する基礎知識
- Android 6(API レベル 23)以上を搭載した物理的な Android デバイス
- Android 9(API レベル 28)以上を搭載し、Google Play がインストールされていない Android Emulator
演習内容
- パフォーマンスを最適化するためにベースライン プロファイルを生成する
- Macrobenchmark ライブラリを使用してパフォーマンスの向上を確認する
学習内容
- ベースライン プロファイルを生成する
- ベースライン プロファイルによるパフォーマンスの向上を分析する
2. 設定方法
最初に、コマンドラインで次のコマンドを使用して、GitHub リポジトリのクローンを作成します。
$ git clone --branch baselineprofiles-main https://github.com/googlecodelabs/android-performance.git
または、次の 2 つの zip ファイルをダウンロードします。
Android Studio でプロジェクトを開く
- [Welcome to Android Studio] ウィンドウで、
[Open an Existing Project] を選択します。
[Download Location]/android-performance/benchmarking
フォルダを選択します(ヒント:build.gradle
が格納されているbenchmarking
ディレクトリを選択してください)。- Android Studio にプロジェクトがインポートされたら、
app
モジュールを実行して、ベンチマーク対象のサンプルアプリをビルドできることを確認します。
3. ベースライン プロファイルとは
ベースライン プロファイルで、含まれているコードパスの解釈とジャストインタイム(JIT)のコンパイル手順を回避することで、初回起動からコード実行速度が約 30% 向上します。アプリまたはライブラリでベースライン プロファイルを配布することで、Android ランタイム(ART)は、事前(AOT)コンパイルによって含まれるコードパスを最適化し、すべての新規ユーザー、すべてのアプリ更新でパフォーマンスを向上できます。このプロファイルに基づく最適化(PGO)を使用すると、起動の最適化、インタラクション ジャンクの削減、エンドユーザーの全体的なランタイム パフォーマンスの向上がアプリの初回起動時から可能になります。
ベースライン プロファイルのメリット
ベースライン プロファイルを使用すると、ユーザーの操作(アプリの起動、画面間の移動、コンテンツのスクロールなど)がすべて、初回の実行時からスムーズに行えるようになります。アプリの速度と応答性を高めると、1 日あたりのアクティブ ユーザー数が増加し、平均リピーター率が高まります。
ベースライン プロファイルは、アプリの最初の起動からアプリのランタイムを改善する一般的なユーザー インタラクションを提供して、アプリの起動以外も含めて最適化する際の指針となります。指針に沿った AOT コンパイルは、ユーザーのデバイスに依存せず、モバイル デバイスではなく開発マシンでリリースごとに 1 回実行できます。ベースライン プロファイルでリリースを送信することで、Cloud プロファイルのみを使用する場合よりもはるかに早くアプリ最適化を利用できるようになります。
ベースライン プロファイルを使用しない場合、すべてのアプリコードは、解釈後にメモリ内で JIT コンパイルされるか、デバイスがアイドル状態のときにバックグラウンドで odex ファイルに JIT コンパイルされます。アプリをインストールまたは更新した後に初めてアプリを実行する際は、新しいパスが最適化されるまで、ユーザーに最適なエクスペリエンスは提供されません。多くのアプリで、30% 前後のパフォーマンス改善が測定されています。
4. ベンチマーク モジュールを設定する
アプリ デベロッパーは、Jetpack Macrobenchmark ライブラリを使用して、ベースライン プロファイルを自動的に生成できます。ベースライン プロファイルを生成する際は、アプリのベンチマーク用に作成したのと同じモジュールを利用して変更を加えることができます。
ベースライン プロファイルの難読化を無効にする
アプリで難読化を有効にしている場合は、ベンチマークに対して難読化を無効にする必要があります。
そのためには、proguard ファイルを :app
モジュールに追加し、そこで難読化を無効にして、proguard ファイルを benchmark
buildType に追加します。
:app
モジュール内に benchmark-rules.pro
という名前の新しいファイルを作成します。このファイルは、モジュール固有の build.gradle
ファイルの横にある /app/
フォルダに配置する必要があります。
次のスニペットに示すように、このファイルに -dontobfuscate
を追加することにより、難読化を無効にします。
# Disables obfuscation for benchmark builds.
-dontobfuscate
次に、:app
モジュール固有の build.gradle
で benchmark
buildType を変更し、作成したファイルを追加します。buildType として initWith
release を使用しているので、この行により benchmark-rules.pro
proguard ファイルがリリース proguard ファイルに追加されます。
buildTypes {
release {
// ...
}
benchmark {
initWith buildTypes.release
// ...
proguardFiles('benchmark-rules.pro')
}
}
それでは、ベースライン プロファイルのジェネレータ クラスを作成しましょう。
5. ベースライン プロファイル ジェネレータを作成する
通常は、アプリの一般的なユーザー ジャーニーについてベースライン プロファイルを生成します。
この例では、次の 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
)では、次のインタラクションをカバーする必要があります。
- ホームボタンを押し、アプリの状態が「再起動」になったことを確認する
- デフォルトのアクティビティを開始し、最初のフレームがレンダリングされるのを待つ
- コンテンツが読み込まれてレンダリングされ、ユーザー操作が可能になるまで待つ
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
)では、次のインタラクションを実行できます。
- スナックリストの UI 要素を見つける
- システム ナビゲーションがトリガーされないように、ジェスチャー マージンを設定する
- リストをスクロールして 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
)では、次のインタラクションを実装します。
- スナックリストを検索し、操作できるスナック アイテムをすべて見つける
- リストからアイテムを選択する
- アイテムをクリックし、詳細画面が読み込まれるまで待つ(スナックリストが画面に表示されなくなることを利用できます)
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] ボタン をクリックします。
このテストは、先ほど定義したエミュレータ イメージを作成し、インタラクションを何度か実行した後、エミュレータを破棄して、結果を Android Studio に出力します。
(省略可)コマンドラインからジェネレータを実行する
コマンドラインからジェネレータを実行する場合は、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/
フォルダからコピーします。
または、Android Studio の出力に含まれている results
リンクをクリックしてコンテンツを保存するか、出力に含まれている adb pull
コマンドを使用します。
次に、ファイルの名前を baseline-prof.txt
に変更します。
続いて、:app
モジュールに profileinstaller
依存関係を追加します。
dependencies {
implementation("androidx.profileinstaller:profileinstaller:1.2.0")
}
この依存関係を追加すると、次のことが可能になります。
- ローカルでベースライン プロファイルのベンチマークを行う。
- クラウド プロファイルをサポートしていない Android 7(API レベル 24)と Android 8(API レベル 26)でベースライン プロファイルを使用する。
- Google Play 開発者サービスがインストールされていないデバイスでベースライン プロファイルを使用する。
最後に、 アイコンをクリックして、プロジェクトを Gradle ファイルと同期します。
次のステップでは、ベースライン プロファイルによってアプリのパフォーマンスがどれくらい向上するかを検証します。
9. 起動のパフォーマンスの向上を検証する
ここまでで、ベースライン プロファイルを生成してアプリに追加しました。それによってアプリのパフォーマンスに望ましい効果が現れるかどうかを検証しましょう。
アプリの起動時間を測定するベンチマークを含む ExampleStartupBenchmark
クラスに戻ります。startup()
テストを少し変更して、別のコンパイル モードで再利用できるようにする必要があります。これにより、ベースライン プロファイルを使用したときの違いを確認できます。
CompilationMode
CompilationMode
パラメータは、アプリをマシンコードにプリコンパイルする方法を決定します。以下のオプションがあります。
DEFAULT
- 可能な場合は、ベースライン プロファイルを使用して、アプリを部分的にプリコンパイルします(compilationMode
パラメータが適用されない場合は、このオプションが使用されます)。None()
- アプリのコンパイル状態をリセットします。アプリのプリコンパイルは行いません。アプリの実行中は、引き続き実行時コンパイル(JIT)が有効です。Partial()
- ベースライン プロファイルでのアプリのプリコンパイルとウォームアップ実行を行うか、どちらかを行います。Full()
- アプリコード全体をプリコンパイルします。Android 6(API 23)以下では、これが唯一のオプションです。
アプリのパフォーマンスの最適化を開始するときは、DEFAULT
コンパイル モードを選択できます。Google Play からアプリをインストールするときと似たパフォーマンスが得られるからです。ベースライン プロファイルがもたらすパフォーマンス上のメリットを確認するには、コンパイル モード None
と Partial
の結果を比較します。
異なる 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 の出力で結果を確認できます。
スクリーンショットを見ると、CompilationMode
に応じてアプリの起動時間が異なることがわかります。次の表に中央値を示します。
timeToInitialDisplay(ミリ秒) | timeToFullDisplay(ミリ秒) | |
None | 396.8 | 818.1 |
Full | 373.9 | 755.0 |
Partial | 352.9 | 720.9 |
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())
前と同様にベンチマークを実行すると、次のスクリーンショットのような結果が得られます。
結果を見ると、CompilationMode.Partial
ではフレーム時間が平均で 0.4 ミリ秒短くなっていることがわかります。これはユーザーが気づくほどの違いではありませんが、他のパーセンタイルでは結果に顕著な差があります。P99 では、36.9 ミリ秒差となり、スキップされたフレーム 3 つ分より大きくなります(Google Pixel 7 の実行速度は 90 FPS で、11 ミリ秒以下です)。
11. 完了
お疲れさまでした。以上でこの Codelab は無事に終了し、ベースライン プロファイルを使用してアプリのパフォーマンスを改善できました。
次のステップ
performance-samples GitHub リポジトリをご覧ください。Macrobenchmark およびその他のパフォーマンスに関するサンプルが格納されています。また、Now In Android サンプルアプリもご覧ください。これは、ベンチマークとベースライン プロファイルを使用してパフォーマンスを改善できる実用的なアプリです。