Macrobenchmark を使用してアプリのパフォーマンスを調査する

1. 始める前に

この Codelab では、Macrobenchmark ライブラリの使用方法を学びます。ユーザー エンゲージメントの重要な指標であるアプリの起動時間と、アプリでジャンクが発生する場所を示すヒントとなるフレーム時間を測定します。

必要なもの

演習内容

  • 既存のアプリにベンチマーク モジュールを追加する
  • アプリの起動時間とフレーム時間を測定する

学習内容

  • アプリケーションのパフォーマンスを確実に測定する

2. 設定方法

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

$ git clone https://github.com/googlecodelabs/android-performance.git

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

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

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

3. Jetpack Macrobenchmark の概要

Jetpack Macrobenchmark ライブラリは、起動、UI の操作、アニメーションなどの大規模なエンドユーザー インタラクションのパフォーマンスを測定します。このライブラリでは、テスト対象のパフォーマンス環境を直接制御できます。アプリのコンパイル、起動、停止を制御して、アプリの起動時間、フレーム時間、トレースされるコード セクションを直接測定できます。

Jetpack Macrobenchmark では、次のことができます。

  • 確定的な起動パターンとスクロール速度でアプリを複数回測定する
  • 複数のテスト実行の結果を平均化して、パフォーマンスのばらつきをならす
  • パフォーマンスの安定性に大きく影響する要素であるアプリのコンパイル状態を制御する
  • Google Play ストアで行われるインストール時の最適化をローカルで再現して、実際のパフォーマンスを確認する

このライブラリを使用したインストルメンテーションでは、アプリコードを直接呼び出す代わりに、タッチ、クリック、スワイプなどのユーザー操作でアプリを実行します。測定は、これらの操作中にデバイス上で行われます。アプリコードの一部を直接測定する場合は、Jetpack Microbenchmark をご覧ください。

ベンチマークの作成はインストルメンテーション テストの作成と似ていますが、アプリの状態を検証する必要はありません。ベンチマークでは JUnit 構文(@RunWith@Rule@Test など)を使用しますが、テストは別個のプロセスで実行されるため、アプリを再起動またはプリコンパイルできます。これにより、ユーザーが操作するときと同様に、アプリの内部状態に干渉することなくアプリを実行できます。そのためには、UiAutomator を使用してターゲット アプリを操作します。

サンプルアプリ

この Codelab では、JetSnack サンプルアプリを使用します。これは、Jetpack Compose を使用するオンライン スナック注文アプリです。アプリのパフォーマンスを測定するために、アプリの設計構造を詳細に知る必要はありません。知る必要があるのは、アプリの動作と UI の構造です。これは、ベンチマークから UI 要素にアクセスするために必要です。アプリを実行し、さまざまなスナックを注文して、基本的な画面に慣れてください。

70978a2eb7296d54.png

4. Macrobenchmark ライブラリを追加する

Macrobenchmark を使用するには、新しい Gradle モジュールをプロジェクトに追加する必要があります。モジュールをプロジェクトに追加する最も簡単な方法は、Android Studio モジュール ウィザードを使用することです。

新しいモジュールのダイアログを開きます(たとえば、[Project] パネルでプロジェクトまたはモジュールを右クリックして、[New] > [Module] を選択します)。

54a3ec4a924199d6.png

[Templates] ペインで [Benchmark] を選択し、ベンチマーク モジュール タイプとして [Macrobenchmark] が選択されていることと、詳細が想定どおりであることを確認します。

ベンチマーク モジュール タイプとして Macrobenchmark が選択されています。

  • Target application – ベンチマーク対象のアプリ
  • Module name – ベンチマーク用の Gradle モジュールの名前
  • Package name – ベンチマークのパッケージ名
  • Minimum SDK – Android 6(API レベル 23)以上が必要

[Finish] をクリックします。

モジュール ウィザードによる変更

モジュール ウィザードにより、以下のようないくつかの変更がプロジェクトに加えられます。

macrobenchmark という名前の Gradle モジュール(またはウィザードで選択した名前)が追加されます。このモジュールは com.android.test プラグインを使用します。このプラグインはそれをアプリに含めないように Gradle に指示するので、テストコード(つまりベンチマーク)のみが含められます。

また、選択されたターゲット アプリ モジュールにも変更が加えられます。具体的には、次のスニペットに示すように、新しい benchmark ビルドタイプが :app モジュールの build.gradle に追加されます。

benchmark {
   initWith buildTypes.release
   signingConfig signingConfigs.debug
   matchingFallbacks = ['release']
   debuggable false
}

この buildType は、release buildType を可能な限りエミュレートしたものです。release buildType との違いは、signingConfigdebug に設定されていることです。これは、本番環境のキーストアがなくてもローカルでアプリをビルドできるようにするために必要です。

ただし、debuggable フラグが無効になっているため、ウィザードは AndroidManifest.xml<profileable> タグを追加します。これにより、ベンチマークはアプリのリリース パフォーマンスをプロファイリングできます。

<application>

  <profileable
     android:shell="true"
     tools:targetApi="q" />

</application>

<profileable> の機能について詳しくは、ドキュメントをご覧ください。

最後に、ウィザードは、起動時間のベンチマークを行うためのスキャフォールドを作成します(これは次のステップで使用します)。

以上で、ベンチマークの作成を開始する準備が整いました。

5. アプリの起動時間を測定する

アプリの起動時間(ユーザーがアプリの使用を開始するまでにかかる時間)は、ユーザー エンゲージメントに影響する重要な指標です。モジュール ウィザードにより、アプリの起動時間を測定できる ExampleStartupBenchmark テストクラスが作成されます。これは次のようなクラスです。

@RunWith(AndroidJUnit4::class)
class ExampleStartupBenchmark {
   @get:Rule
   val benchmarkRule = MacrobenchmarkRule()

   @Test
   fun startup() = benchmarkRule.measureRepeated(
       packageName = "com.example.macrobenchmark_codelab",
       metrics = listOf(StartupTimingMetric()),
       iterations = 5,
       startupMode = StartupMode.COLD,
   ){
        pressHome()
        startActivityAndWait()
   }
}

すべてのパラメータの意味を理解する

ベンチマークを作成する際の出発点は、MacrobenchmarkRulemeasureRepeated 関数です。この関数はベンチマークに関するすべてを処理しますが、以下のパラメータを指定する必要があります。

  • packageName - ベンチマークはテスト対象のアプリとは別のプロセスで実行されるため、測定対象のアプリを指定する必要があります。
  • metrics - ベンチマーク中に測定する情報のタイプ。この例では、アプリの起動時間です。他のタイプの指標については、ドキュメントをご覧ください。
  • iterations - ベンチマークを反復する回数。反復する回数が多いほど結果は安定しますが、それと引き換えに実行時間が長くなります。理想的な回数は、この指標がアプリでどの程度重要視されるかに応じて異なります。
  • startupMode - このパラメータでは、ベンチマークの開始時にアプリを起動する方法を定義できます。指定可能な値は COLDWARMHOT です。この例では、COLD を使用します。これは、アプリが行う必要がある作業の量が最大であることを意味します。
  • measureBlock(最後のラムダ パラメータ)– この関数で、ベンチマーク中に測定するアクション(アクティビティの開始、UI 要素のクリック、スクロール、スワイプなど)を定義します。Macrobenchmark は、定義された metrics をこのブロックで収集します。

ベンチマーク アクションの記述方法

Macrobenchmark はアプリを再インストールして再起動します。インタラクションはアプリの状態から独立したものとして記述してください。Macrobenchmark には、アプリを操作するための便利な関数とパラメータが用意されています。

最も重要な関数は startActivityAndWait() です。この関数は、デフォルトのアクティビティを開始し、アクティビティが最初のフレームをレンダリングするまで待ってから、ベンチマークの手順を続行します。別のアクティビティを開始する場合や開始インテントを微調整する場合は、オプションの intent または block パラメータを使用します。

もう一つの便利な関数は pressHome() です。この関数を使用すると、各反復処理でアプリを強制終了しない場合(StartupMode.HOT を使用している場合など)に、ベンチマークを基本状態にリセットできます。

それ以外のインタラクションについては、device パラメータを使用して、UI 要素を見つけたり、スクロールしたり、特定のコンテンツを待機したりできます。

以上で、起動ベンチマークの定義が完了しました。次のステップでは、それを実行します。

6. ベンチマークを実行する

ベンチマーク テストを実行する前に、Android Studio で適切なビルド バリアントが選択されていることを確認してください。

  1. [Build Variants] パネルを選択します。
  2. [Active Build Variant] を [benchmark] に変更します。
  3. Android Studio が同期を完了するまで待ちます。

b8a622b5a347e9f3.gif

上記の操作を行わなかった場合は、実行時にベンチマークが失敗し、debuggable アプリのベンチマークを行う必要はないというエラーが表示されます。

java.lang.AssertionError: ERRORS (not suppressed): DEBUGGABLE
WARNINGS (suppressed):

ERROR: Debuggable Benchmark
Benchmark is running with debuggable=true, which drastically reduces
runtime performance in order to support debugging features. Run
benchmarks with debuggable=false. Debuggable affects execution speed
in ways that mean benchmark improvements might not carry over to a
real user's experience (or even regress release performance).

インストルメンテーション引数 androidx.benchmark.suppressErrors = "DEBUGGABLE" を使用すると、一時的にこのエラーを抑制できます。Android Emulator でベンチマークを実行する場合と同じ手順を使用できます。

これで、インストルメンテーション テストと同じ方法でベンチマークを実行できるようになりました。テストする関数またはクラス全体を実行するには、関数またはクラスの横のガターアイコンをクリックします。

e72cc74b6fecffdb.png

物理デバイスが選択されていることを確認してください。Android Emulator でベンチマークを実行すると、実行時に失敗し、誤った結果が生成されるという警告が表示されます。技術的にはベンチマークをエミュレータで実行することは可能ですが、基本的にホストマシンのパフォーマンスを測定することになります。負荷が高ければベンチマークのパフォーマンスは低下し、負荷が低ければその逆になります。

e28a1ff21e9b45b4.png

ベンチマークを実行すると、アプリが再ビルドされ、アプリによってベンチマークが実行されます。ベンチマークは、定義された iterations に基づいて、アプリの開始、停止、さらには再インストールを複数回行います。

7. (省略可)Android Emulator でベンチマークを実行する

物理デバイスがない状態でベンチマークを実行する場合は、インストルメンテーション引数 androidx.benchmark.suppressErrors = "EMULATOR" を使用して実行時エラーを抑制できます。

エラーを抑制するには、実行構成を編集します。

  1. 実行メニューから [Edit Configurations...] を選択します。354500cd155dec5b.png
  2. 表示されたウィンドウで、[Instrumentation arguments] の横のオプション アイコン d628c071dd2bf454.png を選択します。a4c3519e48f4ac55.png
  3. ➕ をクリックして詳細を入力することにより、インストルメンテーション追加パラメータを追加します。a06c7f6359d6b92c.png
  4. [OK] をクリックして、選択を確定します。[Instrumentation arguments] の行に引数が表示されます。c30baf54c420ed79.png
  5. [OK] をクリックして、実行構成を確定します。

また、この構成をコードベースに永続的に保持する必要がある場合は、build.gradle:macrobenchmark モジュールで設定できます。

defaultConfig {
    // ...
    testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = 'EMULATOR'
}

8. 起動の結果を分析する

ベンチマークの実行が完了すると、次のスクリーンショットのように、直接 Android Studio 内に結果が表示されます。

f934731d29dcd25c.png

この例では、ご覧のように、Google Pixel 7 の起動時間は、最小値が 294.8 ミリ秒、中央値が 301.5 ミリ秒、最大値が 314.8 ミリ秒です。なお、デバイスによっては、同じベンチマークを実行しても、得られる結果は異なる可能性があります。結果に影響する可能性がある要因には、次のようなものがあります。

  • デバイスの性能
  • デバイスで使用されているシステム バージョン
  • バックグラウンドで実行されているアプリ

したがって、同じデバイス(理想的には同じ状態にあるデバイス)で結果を比較することが重要です。そうしないと、大きな違いが生じます。同じ状態を保証できない場合は、結果の外れ値を適切に処理するために、iterations の回数を増やしてください。

調査用として、Macrobenchmark ライブラリはベンチマークの実行中にシステム トレースを記録します。利便性を考慮して、Android Studio は各反復処理と測定された時間をシステム トレースへのリンクとしてマークします。このリンクを開くことで、簡単に調査できます。

9. (オプション演習)アプリが使用可能になったときに宣言する

Macrobenchmark は、アプリが最初のフレームをレンダリングするまでの時間(timeToInitialDisplay)を自動的に測定できます。しかし、最初のフレームがレンダリングされるまでアプリのコンテンツの読み込みが完了しないことはよくあります。このような場合は、アプリが使用可能になるまでのユーザーの待ち時間を知ることが重要です。この時間は完全表示までの時間と呼ばれます。つまり、アプリがコンテンツを完全に読み込んでユーザーがアプリを操作できるようになるまでの時間です。Macrobenchmark ライブラリはこのタイミングを自動的に検出できますが、完全表示されたときに通知するように、Activity.reportFullyDrawn() 関数を利用してアプリを調整する必要があります。

サンプルアプリは、データが読み込まれるまで、単純な進行状況バーを示します。これによりユーザーは、データの準備が整い、スナックリストがレイアウトされて描画されるまで待っていられます。サンプルアプリを調整し、reportFullyDrawn() の呼び出しを追加しましょう。

[Project] ペインから、.ui.home パッケージの Feed.kt ファイルを開きます。

800f7390ca53998d.png

このファイル内で、スナックリストの作成を行う SnackCollectionList コンポーザブルを見つけます。

データの準備が整っているかどうかを確認する必要があります。コンテンツの準備ができるまでは ​​snackCollections パラメータから空のリストが返されるため、述語が true になったらレポートを処理する ReportDrawnWhen コンポーザブルを使用します。

ReportDrawnWhen { snackCollections.isNotEmpty() }

Box(modifier) {
   LazyColumn {
   // ...
}

または、suspend 関数を利用してこの関数が完了するまで待機する ReportDrawnAfter{} コンポーザブルを使用することもできます。これにより、一部のデータが非同期で読み込まれるまで待ったり、アニメーションが終了するまで待機したりできます。

これが完了したら、コンテンツを待機するように ExampleStartupBenchmark を調整する必要があります。そうしないと、最初にフレームがレンダリングされた時点でベンチマークが終了し、指標がスキップされてしまいます。

現在の起動ベンチマークは、最初にレンダリングされるフレームのみを待機します。待機そのものは startActivityAndWait() 関数に含めます。

@Test
fun startup() = benchmarkRule.measureRepeated(
   packageName = "com.example.macrobenchmark_codelab",
   metrics = listOf(StartupTimingMetric()),
   iterations = 5,
   startupMode = StartupMode.COLD,
) {
   pressHome()
   startActivityAndWait()

   // TODO wait until content is ready
}

この例では、コンテンツ リストが子を持つまで待機するので、次のスニペットに示すように、wait() を追加します。

@Test
fun startup() = benchmarkRule.measureRepeated(
   //...
) {
   pressHome()
   startActivityAndWait()

   val contentList = device.findObject(By.res("snack_list"))
   val searchCondition = Until.hasObject(By.res("snack_collection"))
   // Wait until a snack collection item within the list is rendered
   contentList.wait(searchCondition, 5_000)
}

このスニペットでは、次の処理を行っています。

  1. Modifier.testTag("snack_list") により、スナックリストを見つけます
  2. 待機対象の要素として snack_collection を使用する検索条件を定義します
  3. UiObject2.wait 関数を使用して UI オブジェクト内の条件を待機し、5 秒のタイムアウトを設定します

以上で、ベンチマークを再度実行すると、ライブラリが timeToInitialDisplaytimeToFullDisplay を自動的に測定するようになります。次のスクリーンショットをご覧ください。

3655d7199e4f678b.png

この例では、TTID と TTFD の差が 413 ミリ秒であることがわかります。つまり、最初のフレームが 319.4 ミリ秒でレンダリングされてユーザーに表示されたとしても、さらに 413 ミリ秒経過するまでユーザーはリストをスクロールできないということです。

10. フレーム時間のベンチマークを行う

ユーザーがアプリを開いた後で意識する第 2 の指標は、アプリがどの程度スムーズに動作するかです。つまり、デベロッパーの用語で言えば、アプリがフレームをドロップするかどうかです。これを測定するには、FrameTimingMetric を使用します。

アイテムのリストのスクロール動作を測定するが、それより前は何も測定しない場合を考えてみましょう。ベンチマークを、測定するインタラクションと測定しないインタラクションに分割する必要があります。そのためには、setupBlock ラムダ パラメータを使用します。

測定しないインタラクション(setupBlock で定義します)では、デフォルトのアクティビティを開始します。測定するインタラクション(measureBlock で定義します)では、UI リスト要素を見つけてリストをスクロールし、画面にコンテンツがレンダリングされるまで待ちます。インタラクションを 2 つの部分に分割しなければ、アプリの起動中に生成されたフレームとリストのスクロール中に生成されたフレームを区別できません。

フレーム時間ベンチマークを作成する

上記のフローを実現するため、スクロールのフレーム時間ベンチマークを行う scroll() テストを含む新しい ScrollBenchmarks クラスを作成しましょう。最初に、ベンチマーク ルールと空のテストメソッドを含むテストクラスを作成します。

@RunWith(AndroidJUnit4::class)
class ScrollBenchmarks {
   @get:Rule
   val benchmarkRule = MacrobenchmarkRule()

   @Test
   fun scroll() {
       // TODO implement scrolling benchmark
   }
}

次に、必須のパラメータを含むベンチマーク スケルトンを追加します。

@Test
fun scroll() {
   benchmarkRule.measureRepeated(
       packageName = "com.example.macrobenchmark_codelab",
       iterations = 5,
       metrics = listOf(FrameTimingMetric()),
       startupMode = StartupMode.COLD,
       setupBlock = {
           // TODO Add not measured interactions.
       }
   ) {
       // TODO Add interactions to measure list scrolling.
   }
}

今回のベンチマークでは、metrics パラメータと setupBlock を除いて、startup ベンチマークと同じパラメータを使用します。FrameTimingMetric は、アプリによって生成されたフレームのフレーム時間を収集します。

次に、setupBlock の内容を記述します。前述のように、このラムダ内のインタラクションはベンチマークによって測定されません。このブロックを使用すると、単にアプリを開いて、最初のフレームがレンダリングされるまで待つことができます。

@Test
fun scroll() {
   benchmarkRule.measureRepeated(
       packageName = "com.example.macrobenchmark_codelab",
       iterations = 5,
       metrics = listOf(FrameTimingMetric()),
       startupMode = StartupMode.COLD,
       setupBlock = {
           // Start the default activity, but don't measure the frames yet
           pressHome()
           startActivityAndWait()
       }
   ) {
       // TODO Add interactions to measure list scrolling.
   }
}

それでは、measureBlock(最後のラムダ パラメータ)を記述しましょう。まず、スナックリストへのアイテムの送信は非同期オペレーションであるため、コンテンツの準備が整うまで待機する必要があります。

benchmarkRule.measureRepeated(
   // ...
) {
    val contentList = device.findObject(By.res("snack_list"))

    val searchCondition = Until.hasObject(By.res("snack_collection"))
    // Wait until a snack collection item within the list is rendered
    contentList.wait(searchCondition, 5_000)

   // TODO Scroll the list
}

オプションとして、初期レイアウトのセットアップを測定する必要がない場合は、setupBlock でコンテンツの準備が整うのを待つこともできます。

次に、スナックリストにジェスチャー マージンを設定します。そうしないと、アプリがシステム ナビゲーションをトリガーして、コンテンツをスクロールせずに終了する可能性があります。

benchmarkRule.measureRepeated(
   // ...
) {
   val contentList = device.findObject(By.res("snack_list"))

   val searchCondition = Until.hasObject(By.res("snack_collection"))
   // Wait until a snack collection item within the list is rendered
   contentList.wait(searchCondition, 5_000)

   // Set gesture margin to avoid triggering system gesture navigation
   contentList.setGestureMargin(device.displayWidth / 5)

   // TODO Scroll the list
}

最後に、fling() ジェスチャーで実際にリストをスクロールし(スクロールする範囲の大きさと速度に応じて scroll() または swipe() も使用できます)、UI がアイドル状態になるのを待ちます。

benchmarkRule.measureRepeated(
   // ...
) {
   val contentList = device.findObject(By.res("snack_list"))

   val searchCondition = Until.hasObject(By.res("snack_collection"))
   // Wait until a snack collection item within the list is rendered
   contentList.wait(searchCondition, 5_000)

   // Set gesture margin to avoid triggering gesture navigation
   contentList.setGestureMargin(device.displayWidth / 5)

   // Scroll down the list
   contentList.fling(Direction.DOWN)

   // Wait for the scroll to finish
   device.waitForIdle()
}

ライブラリは、定義されたアクションの実行中に、アプリによって生成されるフレームのタイミングを測定します。

以上で、ベンチマークを実行する準備が整いました。

ベンチマークを実行する

ベンチマークは起動ベンチマークと同じ方法で実行できます。テストの横のガターアイコンをクリックして、[Run 'scroll()'] を選択します。

30043f8d11fec372.png

ベンチマークの実行についてもっと詳しい情報が必要な場合は、ベンチマークを実行する手順をご覧ください。

結果を分析する

FrameTimingMetric は、50 パーセンタイル、90 パーセンタイル、95 パーセンタイル、99 パーセンタイルのフレーム時間をミリ秒単位で出力します(frameDurationCpuMs)。Android 12(API レベル 31)以上では、フレーム時間が上限を超過した時間(frameOverrunMs)も返します。この値は負数になることがあります。負数の場合は、フレームを生成する時間が余ったことを意味します。

2e02ba58e1b882bc.png

この結果を見ると、Google Pixel 7 でフレームの生成にかかった時間の中央値(P50)が 3.8 ミリ秒で、フレーム時間の上限を 6.4 ミリ秒下回ったことがわかります。しかし、パーセンタイルが 99 を超えるフレーム(P99)がスキップされた可能性もあります。これは、フレームの生成に 35.7 ミリ秒かかり、上限の 33.2 ミリ秒を超えているためです。

アプリの起動の結果と同様に、iteration をクリックして、ベンチマーク中に記録されたシステム トレースを開き、結果の時間に影響した要因を調査することができます。

11. 完了

お疲れさまでした。以上でこの Codelab は無事に終了し、Jetpack Macrobenchmark を使用してパフォーマンスを測定できました。

次のステップ

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

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