ベースライン プロファイルをデバッグする

このドキュメントでは、問題を診断して、ベースライン プロファイルが正しく機能し、最大限のメリットをもたらすようにするためのおすすめの方法を紹介します。

ビルドの問題

Now in Android サンプルアプリにあるベースライン プロファイルのサンプルをコピーした場合、ベースライン プロファイルのタスク中に、エミュレータではテストを実行できないことを示すテスト失敗が発生することがあります。

./gradlew assembleDemoRelease
Starting a Gradle Daemon (subsequent builds will be faster)
Calculating task graph as no configuration cache is available for tasks: assembleDemoRelease
Type-safe project accessors is an incubating feature.

> Task :benchmarks:pixel6Api33DemoNonMinifiedReleaseAndroidTest
Starting 14 tests on pixel6Api33

com.google.samples.apps.nowinandroid.foryou.ScrollForYouFeedBenchmark > scrollFeedCompilationNone[pixel6Api33] FAILED
        java.lang.AssertionError: ERRORS (not suppressed): EMULATOR
        WARNINGS (suppressed):
        ...

この失敗の原因は、Now in Android はベースライン プロファイルの生成に Gradle で管理されているデバイスを使用することにあります。一般的に、エミュレータではパフォーマンスのベンチマークを実行しないため、この失敗は想定内です。ただし、ベースライン プロファイルの生成時にパフォーマンスの指標を収集していないため、エミュレータで便宜上、ベースライン プロファイルの収集を実行することは可能です。エミュレータでベースライン プロファイルを使用するには、コマンドラインからビルドとインストールを行い、以下の引数を設定してベースライン プロファイルのルールを有効にします。

installDemoRelease -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=BaselineProfile

また、Android Studio で [Run] > [Edit Configurations] を選択して、エミュレータでベースライン プロファイルを有効にするためのカスタムの実行構成を作成することもできます。

ベースライン プロファイルを作成するためのカスタムの実行構成を Now in Android に追加する
図 1. ベースライン プロファイルを作成するためのカスタムの実行構成を Now in Android に追加する。

インストールの問題

作成している APK または AAB のビルド バリアントにベースライン プロファイルが含まれていることを確認します。最も簡単な確認方法は、Android Studio で [Build] > [Analyze APK] を選択して APK を開き、/assets/dexopt/baseline.prof ファイルでプロファイルを探す方法です。

Android Studio の APK Viewer を使用してベースライン プロファイルがあるかどうかを確認する
図 2. Android Studio の APK Viewer を使用してベースライン プロファイルがあるかどうかを確認する。

ベースライン プロファイルは、アプリを実行しているデバイスでコンパイルする必要があります。アプリストアでインストールされたアプリと PackageInstaller を使用してインストールされたアプリの場合はどちらも、アプリのインストール プロセスの一環としてオンデバイスのコンパイルが行われます。ただし、Android Studio からサイドローディングされたアプリや、コマンドライン ツールを使用してサイドローディングされたアプリの場合は、Jetpack の ProfileInstallerライブラリにより、次回のバックグラウンドの DEX 最適化プロセスの際にコンパイルのキューにプロファイルが追加されます。この場合、ベースライン プロファイルが確実に使用されるようにするには、ベースライン プロファイルの強制コンパイルが必要になることがあります。以下の例に示すように、プロファイルのインストールとコンパイルのステータスは ProfileVerifier を使ってクエリできます。

Kotlin

private const val TAG = "MainActivity"

class MainActivity : ComponentActivity() {
  ...
  override fun onResume() {
    super.onResume()
    lifecycleScope.launch {
      logCompilationStatus()
    }
  }

  private suspend fun logCompilationStatus() {
     withContext(Dispatchers.IO) {
        val status = ProfileVerifier.getCompilationStatusAsync().await()
        when (status.profileInstallResultCode) {
            RESULT_CODE_NO_PROFILE ->
                Log.d(TAG, "ProfileInstaller: Baseline Profile not found")
            RESULT_CODE_COMPILED_WITH_PROFILE ->
                Log.d(TAG, "ProfileInstaller: Compiled with profile")
            RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION ->
                Log.d(TAG, "ProfileInstaller: Enqueued for compilation")
            RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING ->
                Log.d(TAG, "ProfileInstaller: App was installed through Play store")
            RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST ->
                Log.d(TAG, "ProfileInstaller: PackageName not found")
            RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ ->
                Log.d(TAG, "ProfileInstaller: Cache file exists but cannot be read")
            RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE ->
                Log.d(TAG, "ProfileInstaller: Can't write cache file")
            RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION ->
                Log.d(TAG, "ProfileInstaller: Enqueued for compilation")
            else ->
                Log.d(TAG, "ProfileInstaller: Profile not compiled or enqueued")
        }
    }
}

Java


public class MainActivity extends ComponentActivity {

    private static final String TAG = "MainActivity";

    @Override
    protected void onResume() {
        super.onResume();

        logCompilationStatus();
    }

    private void logCompilationStatus() {
         ListeningExecutorService service = MoreExecutors.listeningDecorator(
                Executors.newSingleThreadExecutor());
        ListenableFuture<ProfileVerifier.CompilationStatus> future =
                ProfileVerifier.getCompilationStatusAsync();
        Futures.addCallback(future, new FutureCallback<>() {
            @Override
            public void onSuccess(CompilationStatus result) {
                int resultCode = result.getProfileInstallResultCode();
                if (resultCode == RESULT_CODE_NO_PROFILE) {
                    Log.d(TAG, "ProfileInstaller: Baseline Profile not found");
                } else if (resultCode == RESULT_CODE_COMPILED_WITH_PROFILE) {
                    Log.d(TAG, "ProfileInstaller: Compiled with profile");
                } else if (resultCode == RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION) {
                    Log.d(TAG, "ProfileInstaller: Enqueued for compilation");
                } else if (resultCode == RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING) {
                    Log.d(TAG, "ProfileInstaller: App was installed through Play store");
                } else if (resultCode == RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST) {
                    Log.d(TAG, "ProfileInstaller: PackageName not found");
                } else if (resultCode == RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ) {
                    Log.d(TAG, "ProfileInstaller: Cache file exists but cannot be read");
                } else if (resultCode
                        == RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE) {
                    Log.d(TAG, "ProfileInstaller: Can't write cache file");
                } else if (resultCode == RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION) {
                    Log.d(TAG, "ProfileInstaller: Enqueued for compilation");
                } else {
                    Log.d(TAG, "ProfileInstaller: Profile not compiled or enqueued");
                }
            }

            @Override
            public void onFailure(Throwable t) {
                Log.d(TAG,
                        "ProfileInstaller: Error getting installation status: " + t.getMessage());
            }
        }, service);
    }
}

以下の結果コードに基づいて、問題の原因を特定します。

RESULT_CODE_COMPILED_WITH_PROFILE
プロファイルはインストールされ、コンパイルされて、アプリの実行時に常に使用されます。これが、目指している結果です。
RESULT_CODE_ERROR_NO_PROFILE_EMBEDDED
実行中の APK または AAB にプロファイルがありません。このエラーが表示された場合は、ベースライン プロファイルを含むビルド バリアントを使用していることと、APK にプロファイルが含まれていることを確認します。
RESULT_CODE_NO_PROFILE
アプリストアまたはパッケージ管理システムからアプリをインストールする際にこのアプリ用のプロファイルがインストールされませんでした。このエラーコードは主に、ProfileInstallerInitializer が無効になっているためにプロファイル インストーラが実行されなかった場合に表示されます。なお、このエラーが報告されても、アプリの APK には埋め込みのプロファイルがあります。埋め込みのプロファイルがない場合のエラーコードは RESULT_CODE_ERROR_NO_PROFILE_EMBEDDED です。
RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION
APK または AAB にプロファイルがあり、コンパイルのキューに登録されています。ProfileInstaller によってインストールされたプロファイルは、次回のバックグラウンド DEX 最適化がシステムで実行されたときにコンパイルのキューに追加されます。コンパイルが完了するまでプロファイルは有効になりません。コンパイルが完了するまでは、ベースライン プロファイルのベンチマークを試行しないでください。ベースライン プロファイルの強制コンパイルが必要になることがあります。コンパイルはインストール中に行われるため、Android 9(API 28)以降を搭載したデバイスでアプリストアまたはパッケージ管理システムからアプリをインストールした場合、このエラーは発生しません。
RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING
一致しないプロファイルがインストールされ、そのプロファイルでアプリがコンパイルされています。これは、Google Play ストアまたはパッケージ管理システムからのインストールの結果です。一致しないプロファイルでも、プロファイルとアプリ間で共有されているメソッドのみはコンパイルされるため、この結果は RESULT_CODE_COMPILED_WITH_PROFILE とは異なります。このプロファイルは想定よりも実質的に小さく、コンパイルされるメソッドはベースライン プロファイルに含まれていたメソッドより少なくなります。
RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE
ProfileVerifier が検証結果のキャッシュ ファイルを作成できません。アプリフォルダの権限に問題があるか、デバイスに十分なディスク空き容量がないかのいずれかの場合に発生する可能性があります。
RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION
ProfileVerifier の is running on an unsupported API version of Android. ProfileVerifier は Android 9(API レベル 28)以降でのみサポートされます。
RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST
アプリ パッケージで PackageManager をクエリすると PackageManager.NameNotFoundException がスローされます。このエラーが発生することはまずありません。アプリをアンインストールしてから、すべて再インストールしてみてください。
RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ
以前の検証結果のキャッシュ ファイルがありますが、読み取れません。このエラーが発生することはまずありません。アプリをアンインストールしてから、すべて再インストールしてみてください。

本番環境で ProfileVerifier を使用する

本番環境では、ProfileVerifierFirebase 向け Google アナリティクスなどの分析レポート ライブラリと組み合わせて使用することで、プロファイルのステータスを示す分析イベントを生成できます。たとえば、ベースライン プロファイルを含まない新しいアプリ バージョンがリリースされた場合は、すぐに通知されます。

ベースライン プロファイルのコンパイルを強制的に行う

ベースライン プロファイルのコンパイル ステータスが RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION の場合は、adb を使用して即時コンパイルを強制的に実行できます。

adb shell cmd package compile -r bg-dexopt PACKAGE_NAME

ProfileVerifier を使用せずにコンパイル ステータスを確認する

ProfileVerifier を使用していない場合は、adb を使用してコンパイル ステータスを確認できます。ただし、ProfileVerifier ほど詳細な分析情報は得られません。

adb shell dumpsys package dexopt | grep -A 2 PACKAGE_NAME

adb を使用すると、次のような結果が出力されます。

  [com.google.samples.apps.nowinandroid.demo]
    path: /data/app/~~dzJiGMKvp22vi2SsvfjkrQ==/com.google.samples.apps.nowinandroid.demo-7FR1sdJ8ZTy7eCLwAnn0Vg==/base.apk
      arm64: [status=speed-profile] [reason=bg-dexopt] [primary-abi]
        [location is /data/app/~~dzJiGMKvp22vi2SsvfjkrQ==/com.google.samples.apps.nowinandroid.demo-7FR1sdJ8ZTy7eCLwAnn0Vg==/oat/arm64/base.odex]

status 値はプロファイルのコンパイル ステータスを示します。以下のいずれかの値になります。

コンパイル ステータス 意味
speed‑profile コンパイル済みのプロファイルがあり、使用されている。
verify コンパイル済みのプロファイルがない。

verify ステータスは、APK または AAB にプロファイルが含まれていないという意味ではありません。この場合は、プロファイルは次回のバックグラウンド DEX 最適化タスクでコンパイルのキューに追加されます。

reason 値は、プロファイルがコンパイルされた状況を示します。以下のいずれかの値になります。

理由 意味
install‑dm ベースライン プロファイルがアプリのインストール時に手動または Google Play によってコンパイルされた。
bg‑dexopt デバイスがアイドル状態のときにプロファイルがコンパイルされた。これは、ベースライン プロファイルの場合もあれば、アプリの使用中に収集されたプロファイルの場合もあります。
cmdline コンパイルは adb を使用してトリガーされた。これは、ベースライン プロファイルの場合もあれば、アプリの使用中に収集されたプロファイルの場合もあります。

パフォーマンスの問題

このセクションでは、ベースライン プロファイルを正しく定義してベンチマークを実行し、そのメリットを最大限に引き出すためのおすすめの方法について説明します。

起動時の指標のベンチマークを正しく実行する

起動時の指標が明確に定義されていれば、ベースライン プロファイルの効果は高まります。主な 2 つの指標は、初期表示までの時間(TTID)完全表示までの時間(TTFD)です。

TTID はアプリが最初のフレームを描画する時点を指します。何かを表示するとは、アプリが実行されていることをユーザーに示すことであるため、可能な限り時間がかからないようにすることが重要です。アプリが応答していることを示す不確定形式の進行状況インジケーターを表示する方法もあります。

TTFD はアプリを実際に操作できるようになった時点を指します。ユーザーがフラストレーションを感じないよう、できる限り時間がかからないようにすることが重要です。TTFD を正しく通知すれば、TTFD までの間に実行されるコードがアプリの起動に関連するものであることをシステムに伝えることになり、結果として、そのコードがプロファイルに入る可能性が高くなります。

TTID と TTFD のどちらも可能な限り短くして、アプリが応答していると感じられるようにしてください。

システムは TTID を検出して Logcat に表示し、起動ベンチマークの一部として報告することはできますが、TTFD を判断することはできません。完全に描画された操作可能な状態に達したら、アプリが報告する必要があります。このためには、reportFullyDrawn() を呼び出すか、Jetpack Compose を使用している場合は ReportDrawn を呼び出します。アプリが完全に描画されたと見なされるまでにすべて完了する必要がある複数のバックグラウンド タスクがある場合は、FullyDrawnReporter を使用できます。これについては、起動時間の精度を改善するをご覧ください。

ライブラリ プロファイルとカスタム プロファイル

プロファイルの影響をベンチマークする場合、アプリのプロファイルのメリットと、ライブラリ(Jetpack ライブラリなど)が提供するプロファイルとを区別するのは難しい場合があります。APK をビルドすると、Android Gradle プラグインはカスタム プロファイルだけでなくライブラリ依存関係のプロファイルも追加します。これは、全体的なパフォーマンスの最適化に適しており、リリースビルドに推奨されています。ただし、カスタム プロファイルによってどの程度パフォーマンスが向上するかを測定することは困難です。

カスタム プロファイルによって提供される追加の最適化を手動で確認するには、それを削除してベンチマークを実行します。その後で置き換えて、ベンチマークを再度実行してください。この 2 つを比較すると、ライブラリ プロファイルのみによる最適化と、ライブラリ プロファイルとカスタム プロファイルによる最適化を比較できます。

プロファイルを自動的に比較するには、カスタム プロファイルではなく、ライブラリ プロファイルのみを含む新しいビルド バリアントを作成します。このバリアントのベンチマークを、ライブラリ プロファイルとカスタム プロファイルの両方を含むリリース バリアントと比較します。次の例は、ライブラリ プロファイルのみを含むバリアントの設定方法を示しています。releaseWithoutCustomProfile という名前の新しいバリアントをプロファイル コンシューマ モジュール(通常はアプリ モジュール)に追加します。

Kotlin

android {
  ...
  buildTypes {
    ...
    // Release build with only library profiles.
    create("releaseWithoutCustomProfile") {
      initWith(release)
    }
    ...
  }
  ...
}
...
dependencies {
  ...
  // Remove the baselineProfile dependency.
  // baselineProfile(project(":baselineprofile"))
}

baselineProfile {
  variants {
    create("release") {
      from(project(":baselineprofile"))
    }
  }
}

Groovy

android {
  ...
  buildTypes {
    ...
    // Release build with only library profiles.
    releaseWithoutCustomProfile {
      initWith(release)
    }
    ...
  }
  ...
}
...
dependencies {
  ...
  // Remove the baselineProfile dependency.
  // baselineProfile ':baselineprofile"'
}

baselineProfile {
  variants {
    release {
      from(project(":baselineprofile"))
    }
  }
}

上記のコードサンプルでは、すべてのバリアントから baselineProfile 依存関係を削除し、release バリアントにのみ選択的に適用しています。プロファイル プロデューサー モジュールへの依存関係が削除されても、ライブラリ プロファイルがまだ追加されるというのは、直感に反しているように思えるかもしれません。ただし、このモジュールはカスタム プロファイルの生成のみを行います。Android Gradle プラグインはすべてのバリアントに対して引き続き実行され、ライブラリ プロファイルをインクルードします。

また、新しいバリアントをプロファイル生成モジュールに追加する必要があります。この例では、プロデューサー モジュールの名前は :baselineprofile です。

Kotlin

android {
  ...
    buildTypes {
      ...
      // Release build with only library profiles.
      create("releaseWithoutCustomProfile") {}
      ...
    }
  ...
}

Groovy

android {
  ...
    buildTypes {
      ...
      // Release build with only library profiles.
      releaseWithoutCustomProfile {}
      ...
    }
  ...
}

ライブラリ プロファイルのみを使用してベンチマークを行うには、次のコマンドを実行します。

./gradlew :baselineprofile:connectedBenchmarkReleaseWithoutCustomProfileAndroidTest

ライブラリ プロファイルとカスタム プロファイルの両方でベンチマークを行うには、次のコマンドを実行します。

./gradlew :baselineprofile:connectedBenchmarkReleaseAndroidTest

Macrobenchmark サンプルアプリで上記のコードを実行すると、2 つのバリアントの間にパフォーマンスの違いがあることがわかります。ライブラリ プロファイルのみの場合、ウォーム startupCompose ベンチマークは次の結果を示します。

SmallListStartupBenchmark_startupCompose[mode=COLD]
timeToInitialDisplayMs   min  70.8,   median  79.1,   max 126.0
Traces: Iteration 0 1 2 3 4 5 6 7 8 9

多くの Jetpack Compose ライブラリにはライブラリ プロファイルがあるため、ベースライン プロファイル Gradle プラグインを使用するだけで、いくつかの最適化があります。ただし、カスタム プロファイルを使用する場合は、さらに最適化されます。

SmallListStartupBenchmark_startupCompose[mode=COLD]
timeToInitialDisplayMs   min 57.9,   median 73.5,   max 92.3
Traces: Iteration 0 1 2 3 4 5 6 7 8 9

I/O バウンドのアプリの起動を避ける

アプリが起動時に実行する I/O 呼び出しやネットワーク呼び出しが多いと、アプリの起動時間と起動ベンチマークの精度の両方に悪影響が及ぶ可能性があります。これらの重量級の呼び出しは所要時間が不確定で、時間の経過とともに変わる可能性があります。またベンチマークが同じでも反復処理ごとに変わる可能性もあります。ネットワーク呼び出しは、デバイスの外部要因やデバイス自体の要因の影響を受ける可能性があるため、どちらかと言えば I/O 呼び出しの方がネットワーク呼び出しよりましです。起動時のネットワーク呼び出しは回避し、どちらか一方の使用が避けられない場合は、I/O を使用してください。

起動ベンチマークを実行する場合のみ使用するとしても、アプリ アーキテクチャで、ネットワーク呼び出しや I/O 呼び出しのないアプリの起動をサポートすることをおすすめします。これにより、ベンチマークの反復処理間のばらつきを最小限に抑えることができます。

アプリで Hilt を使用している場合は、Microbenchmark と Hilt でベンチマークを実行するときに、I/O にバインドされた疑似実装を提供できます。

重要なユーザー ジャーニーをすべてカバーする

重要なユーザー ジャーニーはすべて、ベースライン プロファイルの生成で正確にカバーすることが大切です。カバーされていないユーザー ジャーニーは、ベースライン プロファイルで改善されません。一般的な起動のユーザー ジャーニーとパフォーマンス重視のアプリ内ユーザー ジャーニー(リストのスクロールなど)を含むベースライン プロファイルが、最も効果的です。