ベースライン プロファイル

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

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

プロファイル ルールは、APK でバイナリ形式にコンパイルされています(assets/dexopt/baseline.prof)。

インストールの際、ART はプロファイル内のメソッドの事前(AOT)コンパイルを行い、メソッドの実行を高速化します。アプリの起動時やフレーム レンダリング時に使用されるメソッドがプロファイルに含まれていれば、起動時間が短くなったり、ジャンクが減ったりします。

アプリやライブラリを開発するときは、起動、遷移、スクロールなど、レンダリング時間やレイテンシが重要となるクリティカル ユーザー ジャーニーで特定のホットパスをカバーするベースライン プロファイルを定義することを検討してください。

ベースライン プロファイルは、APK とともに(Google Play を通じて)ユーザーに直接配信されます。

この図は、アップロードからエンドユーザーへの配信までの、ベースライン プロファイルのワークフローと、そのワークフローがクラウド プロファイルとどのように関連しているかを示しています。

ベースライン プロファイルを使用する理由

起動時間は、アプリのユーザー エンゲージメントを改善するための重要な要素です。アプリの速度と応答性を高めると、1 日あたりのアクティブ ユーザー数が増加し、平均再利用率が高まります。

クラウド プロファイルでも同じインタラクションが最適化されますが、ユーザーがクラウド プロファイルを利用できるのはアップデートのリリースから 1 日以上経過した後であり、Android 7(API 24)から Android 8(API 26)まではサポートされていません。

Android バージョン間のコンパイル動作

Android プラットフォームの各バージョンでは、これまでさまざまなアプリのコンパイル方法が採用され、それぞれの方法にトレードオフがありました。ベースライン プロファイルは、すべてのインストールにプロファイルを提供することで、以前のコンパイル方法を改善します。

Android バージョン コンパイル方法 最適化アプローチ
Android 5(API レベル 21)から Android 6(API レベル 23)まで 完全 AOT アプリ全体がインストール時に最適化されるため、アプリ使用時の待ち時間が長くなり、RAM やディスクの使用量が増え、ディスクからコードを読み込む時間が長くなるため、コールド スタートアップ時間が長くなる可能性があります。
Android 7(API レベル 24)から Android 8.1(API レベル 27)まで 部分 AOT(ベースライン プロファイル) ベースライン プロファイルは、アプリ モジュールがこの依存関係を定義した場合、最初の実行時に androidx.profileinstaller によってインストールされます。ART は、アプリ使用時に他のプロファイル ルールを追加し、デバイスがアイドル状態のときにそれらをコンパイルすることで、これをさらに改善できます。これにより、ディスク容量と、ディスクからコードを読み込む時間が最適化され、アプリの待ち時間が短くなります。
Android 9(API レベル 28)以降 部分 AOT(ベースライン + クラウド プロファイル) Play は、アプリのインストール時にベースライン プロファイルを使用して、APK とクラウド プロファイル(利用可能な場合)を最適化します。インストール後、ART プロファイルは Play にアップロードされ、集計されます。その後、ユーザーがアプリをインストールまたはアップデートする際にクラウド プロファイルとして提供されます。

ベースライン プロファイルを作成する

BaselineProfileRule を使用してプロファイル ルールを自動的に作成する

アプリ デベロッパーは、Jetpack Macrobenchmark ライブラリを使用することで、アプリのリリースごとにプロファイルを自動的に生成できます。

Macrobenchmark ライブラリを使用してベースライン プロファイルを作成するには:

  1. アプリの build.gradleProfileInstaller ライブラリに依存関係を追加し、ローカルと Play ストアのベースライン プロファイルのコンパイルを有効にします。これは、ベースライン プロファイルをローカルでサイドローディングする唯一の方法です。

    dependencies {
         implementation("androidx.profileinstaller:profileinstaller:1.2.0-alpha01")
    }
    
  2. Gradle プロジェクトに Macrobenchmark モジュールをセットアップします。

  3. 次のような、BaselineProfileGenerator という新しいテストを定義します。

    @ExperimentalBaselineProfilesApi
    @RunWith(AndroidJUnit4::class)
    class BaselineProfileGenerator {
        @get:Rule val baselineProfileRule = BaselineProfileRule()
    
        @Test
        fun startup() =
            baselineProfileRule.collectBaselineProfile(packageName = "com.example.app") {
                pressHome()
                // This block defines the app's critical user journey. Here we are interested in
                // optimizing for app startup. But you can also navigate and scroll
                // through your most important UI.
                startActivityAndWait()
            }
    }
    
  4. Android 9 以降を実行する userdebug または root 権限取得済みの Android オープンソース プロジェクト(AOSP)エミュレータを接続します。

  5. adb root を実行して、root 権限取得済みであることを確認します。

  6. テストを実行し、logcat で、生成されたプロファイルの場所を探します。ログタグ Benchmark を検索します。

    com.example.app D/Benchmark: Usable output directory: /storage/emulated/0/Android/media/com.example.app
    
    # List the output baseline profile
    $ ls /storage/emulated/0/Android/media/com.example.app
    SampleStartupBenchmark_startup-baseline-prof.txt
    
  7. 生成されたファイルをデバイスから pull します。

    $ adb pull storage/emulated/0/Android/media/com.example.app/SampleStartupBenchmark_startup-baseline-prof.txt .
    
  8. 生成されたファイルの名前を baseline-prof.txt に変更し、アプリ モジュールの src/main ディレクトリにコピーします。

プロファイル ルールを手動で定義する

src/main ディレクトリに baseline-prof.txt というファイルを作成することで、アプリまたはライブラリ モジュールでプロファイル ルールを手動で定義できます。これは、AndroidManifest.xml ファイルが入っているフォルダと同じです。

このファイルでは、1 行に 1 つのルールを指定します。各ルールは、最適化が必要なアプリやライブラリのメソッドまたはクラスをマッチングするためのパターンを表します。

ルールの構文は、adb shell profman --dump-classes-and-methods を使用する際の、人が読める ART プロファイル形式(HRF)のスーパーセットです。この構文は記述子と署名の構文とよく似ていますが、ルール作成プロセスを簡素化するためにワイルドカードも使用できます。

Jetpack Compose ライブラリに含まれるベースライン プロファイル ルールの例を次に示します。

HSPLandroidx/compose/runtime/ComposerImpl;->updateValue(Ljava/lang/Object;)V
HSPLandroidx/compose/runtime/ComposerImpl;->updatedNodeCount(I)I
HLandroidx/compose/runtime/ComposerImpl;->validateNodeExpected()V
PLandroidx/compose/runtime/CompositionImpl;->applyChanges()V
HLandroidx/compose/runtime/ComposerKt;->findLocation(Ljava/util/List;I)I
Landroidx/compose/runtime/ComposerImpl;

ルールの構文

ルールは、メソッドまたはクラスをターゲットとする 2 つの形式のいずれかを使用します。

[FLAGS][CLASS_DESCRIPTOR]->[METHOD_SIGNATURE]

クラスルールでは、次のパターンを使用します。

[CLASS_DESCRIPTOR]
構文 説明
FLAGS HSP のうち 1 つ以上の文字を表し、起動タイプに関して HotStartupPost Startup のフラグをこのメソッドに付けるべきかどうかを示します。

H フラグの付いたメソッドは、「ホット」メソッドです。つまり、アプリの存続期間中に何度も呼び出されます。

S フラグの付いたメソッドは、起動時に呼び出されるメソッドです。

P フラグの付いたメソッドは、起動に関係のないホットメソッドです。

このファイルに存在するクラスは、起動時に使用されるクラスであり、クラス読み込みのコストを回避するためにヒープで事前割り当てする必要があることを示しています。ART コンパイラは、上述のメソッドの AOT コンパイルや、生成された AOT ファイルでのレイアウト最適化など、さまざまな最適化戦略を採用しています。
CLASS_DESCRIPTOR ターゲットとなるメソッドのクラスの記述子。たとえば、androidx.compose.runtime.SlotTable の記述子は Landroidx/compose/runtime/SlotTable; です。注: Dalvik Executable(DEX)形式に従って L が先頭に付加されています。
METHOD_SIGNATURE メソッドのシグネチャ(メソッドの名前、パラメータの型、戻り値の型を含む)。たとえば、LayoutNode のメソッド

// LayoutNode.kt

fun isPlaced():Boolean {
// ...
}

のシグネチャは isPlaced()Z です。

1 つのルールに複数のメソッドまたはクラスを含める場合は、これらのパターンにワイルドカードを使用できます。Android Studio でルールの構文を記述する際のガイドについては、Android Baseline Profiles プラグインをご覧ください。

ワイルドカード ルールの例を次に示します。

HSPLandroidx/compose/ui/layout/**->**(**)**

ベースライン プロファイル ルールでサポートされている型

ベースライン プロファイル ルールでサポートされている型は次のとおりです。これらの型の詳細については、Dalvik Executable(DEX)形式をご覧ください。

文字 説明
B byte 符号付きバイト
C char UTF-16 でエンコードされた Unicode 文字コードポイント
D double 倍精度浮動小数点値
F float 単精度浮動小数点値
I int 整数
J long 長整数
S short 符号付き短整数
V void Void
Z boolean True または False
L(クラス名) reference クラス名のインスタンス

また、ライブラリでは、AAR アーティファクトにパッケージ化されるルールを定義できます。このアーティファクトを含むように APK をビルドすると、ルールはマージされ(マニフェストのマージと同様の方法)、APK に固有のコンパクトなバイナリ ART プロファイルにコンパイルされます。

ART は、デバイスで APK が使用される際にこのプロファイルを活用して、Android 9(API レベル 28)、または ProfileInstaller を使用する場合は Android 7(API レベル 24)でのインストール時に、アプリの特定のサブセットを AOT コンパイルします。

その他の注意事項

ベースライン プロファイルを作成するときは、次の点にも注意してください。

  • Android 5 から Android 6 まで(API レベル 21、23)は、インストール時にすでに APK を AOT コンパイルしています。

  • デバッグ可能なアプリは、トラブルシューティングのために AOT コンパイルされることはありません。

  • ルールファイルには baseline-prof.txt という名前を付けて、メイン ソースセットのルート ディレクトリに配置する必要があります(AndroidManifest.xml ファイルの兄弟ファイルにする必要があります)。

  • これらのファイルは、Android Gradle プラグイン 7.1.0-alpha05 以降(Android Studio Bumblebee Canary 5)を使用している場合にのみ利用できます。

  • Bazel は現在のところ、ベースライン プロファイルの読み込みと APK へのマージをサポートしていません。

  • ベースライン プロファイルは、圧縮して 1.5 MB 以下にする必要があります。そのためライブラリとアプリは、定義するプロファイル ルールを抑えながらも影響を最大化できるように努める必要があります。

  • アプリのコンパイルが多すぎる広範なルールでは、ディスク アクセスの増加により起動が遅くなる可能性があります。ベースライン プロファイルのパフォーマンスをテストする必要があります。

改善を測定する

Macrobenchmark ライブラリで測定を自動化する

Macrobenchmarks を使用すると、ComilationMode API を介して、測定前のコンパイルを制御できます(BaselineProfile の使用を含む)。

Macrobenchmark モジュールで BaselineProfileRule テストをすでに設定している場合は、そのモジュールで新しいテストを定義して、パフォーマンスを評価できます。

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

  @Test
  fun startupNoCompilation() {
    startup(CompilationMode.None())
  }

  @Test
  fun startupBaselineProfile() {
    startup(CompilationMode.Partial(
      baselineProfileMode = BaselineProfileMode.Require
    ))
  }

  private fun startup(compilationMode: CompilationMode) {
    benchmarkRule.measureRepeated(
      packageName = "com.example.app",
      metrics = listOf(StartupTimingMetric()),
      iterations = 10,
      startupMode = StartupMode.COLD,
      compilationMode = compilationMode
    ) { // this = MacrobenchmarkScope
        pressHome()
        startActivityAndWait()
    }
  }
}

テスト結果を渡す例を次に示します。

これは小規模なテストの結果です。アプリの規模が大きいほど、ベースライン プロファイルのメリットも大きくなります。

上の例では StartupTimingMetric を見ていますが、ジャンク(フレーム指標)のように、Jetpack Macrobenchmark を使用して測定できる考慮すべき重要な指標もあります。

アプリの改善を手動で測定する

まず、参考のために最適化されていないアプリの起動を測定してみましょう。

PACKAGE_NAME=com.example.app

# Force Stop App
adb shell am force-stop $PACKAGE_NAME
# Reset compiled state
adb shell cmd package compile --reset $PACKAGE_NAME

# Measure App startup
# This corresponds to `Time to initial display` metric
# For additional info https://developer.android.com/topic/performance/vitals/launch-time#time-initial
adb shell am start-activity -W -n $PACKAGE_NAME/.ExampleActivity \
 | grep "TotalTime"

次に、ベースライン プロファイルをサイドローディングしましょう。

# Unzip the Release APK first
unzip release.apk

# Create a ZIP archive
# Note: The name should match the name of the APK
# Note: Copy baseline.prof{m} and rename it to primary.prof{m}
cp assets/dexopt/baseline.prof primary.prof
cp assets/dexopt/baseline.profm primary.profm

# Create an archive
zip -r release.dm primary.prof primary.profm

# Confirm that release.dm only contains the two profile files:
unzip -l release.dm
# Archive:  release.dm
#   Length      Date    Time    Name
# ---------  ---------- -----   ----
#      3885  1980-12-31 17:01   primary.prof
#      1024  1980-12-31 17:01   primary.profm
# ---------                     -------
#                               2 files

# Install APK + Profile together
adb install-multiple release.apk release.dm

インストール時にパッケージが最適化されたことを確認するために、次のコマンドを実行します。

# Check dexopt state
adb shell dumpsys package dexopt | grep -A 1 $PACKAGE_NAME

出力に、パッケージがコンパイルされたことが示されます。

[com.example.app]
  path: /data/app/~~YvNxUxuP2e5xA6EGtM5i9A==/com.example.app-zQ0tkJN8tDrEZXTlrDUSBg==/base.apk
  arm64: [status=speed-profile] [reason=install-dm]

これで、コンパイル状態をリセットせずに、以前と同じようにアプリの起動パフォーマンスを測定できるようになりました。

# Force Stop App
adb shell am force-stop $PACKAGE_NAME

# Measure App startup
adb shell am start-activity -W -n $PACKAGE_NAME/.ExampleActivity \
 | grep "TotalTime"