Baseline Profilesでアプリのパフォーマンスを向上させよう

以前 LT で発表した記事のテキスト化 & 補足です。

https://speakerdeck.com/numeroanddev/improve-app-performance-with-baseline-profiles

Baseline Profiles はアプリのパフォーマンス改善を見込めるものになっているので、ぜひ検証 & 導入を進めていきたいですね。

## Baseline Profiles とは?

- ART (Android Runtime) がプリコンパイルするときに使用するアプリのクラスとメソッドのリスト
- Baseline Profiles を使うことでアプリの起動時間の短縮、ジャンクの削減、全体的なパフォーマンスの向上が見込める
    - Android 7.0 ~ 最新 OS までに効果がある
    - アプリのインストール後やアップデート後の初回起動はプリコンパイルされていない状態のため、特にアプリの起動速度やパフォーマンスが落ちる

## なぜ Baseline Profiles が必要なのか

- これまで Cloud Profile として Google Play が同じような仕組みを提供しているが、こちらは提供されるまで時間がかかるためにリリーススパンが短いアプリでは恩恵を受けることができない
  - Cloud Profile は実際にアプリをダウンロードしたユーザが起動した時の情報を Play 側で収集して Profile を作成しているので、Profile が生成されるまでタイムラグが発生する
  - 頻繁にリリースがあるアプリだとその生成が間に合わない
- OS で最適化がされる Android View とは異なり、Jetpack Compose は OS にバンドルされないライブラリであるため最適化の恩恵を受けずにパフォーマンスの問題がある
  - Jetpack Compose のライブラリ自体にも Baseline Profiles のルールが含まれている
  - Android View は Zygote プロセスの仕組みで View 関連のクラスと Drawable がプリロードされている最適化が行われている



## Baseline Profiles の導入

#### app モジュールに ProfileInstaller の依存を追加する

```gradle
dependencies {
     implementation("androidx.profileinstaller:profileinstaller:1.2.0")
}
```

#### Macrobenchmark のモジュールをアプリに追加する

- Android Studio から "New Module..." よりベンチマーク用のモジュールを追加するテンプレートが用意されている

![スクリーンショット 2022-09-09 20.57.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/9ffd4933-5ea2-87b4-bd06-8998e909a596.png)


- 注意点
    - `productFlavors` が app モジュールにある場合は benchmark のモジュールでも以下のうちどれかの対応をする必要がある
        - app モジュールの `productFlavors` を benchmark モジュールにも同様の定義する
        - `matchingFallbacks` を使って新規の flavor を追加する
        - `missingDimensionStrategy` を使用する



#### Baseline Profiles を生成するテストの追加

```kotlin
@ExperimentalBaselineProfilesApi
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
    @get:Rule val baselineProfileRule = BaselineProfileRule()

    @Test
    fun startup() =
        baselineProfileRule.collectBaselineProfile(packageName = "com.example.app") {
            pressHome()
            startActivityAndWait()
        }
}
```

#### Baseline Profiles を生成するテストをエミュレータで実行

- Google API かつ Android 9 ~ 12 のエミュレータを作成する
- テストの実行前に `adb root` で root 化しておく必要がある
- Baseline Profiles を生成する buildType では難読化されない設定が必要

```proguard-benchmark.pro
# When generating the baseline profile we want the proper names of
# the methods and classes
-dontobfuscate
```

```app/build.gradle
buildTypes {
    release {
        shrinkResources true
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
    debug {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
    benchmark {
        initWith buildTypes.release
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro', 'proguard-benchmark.pro'
        signingConfig signingConfigs.debug
        matchingFallbacks = ['release']
    }
}
```

- テストを実行するのと同様に Baseline Profiles を生成するテストを実行

#### 生成された Baseline Profiles をエミュレータから pull する

- Baseline Profiles 生成のテスト結果に　Baseline Profiles のファイルをエミュレータから取り出すコマンドが表示されるのでそれを実行する
- 取得したファイルを `baseline-prof.txt` の名前で app/src/main 以下に配置する


#### Macrobenchmark でアプリの起動時間が改善されるか確認

- デバッグビルドやリリースビルドでのアプリのインストールでは Baseline Profiles は働かないので、Macrobenchmark で実際に改善されるのかを確認する必要がある
  - 計測には物理デバイスでの確認が公式ドキュメントで推奨されている

```StartupBenchmark.kt
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
    @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 = 5,
            startupMode = StartupMode.COLD,
            compilationMode = compilationMode
        ) {
            pressHome()
            startActivityAndWait()
        }
    }
}
```

- `CompilationMode.None` は Baseline　Profiles を無効にした状態でのテストになり、アプリ起動時に ART が都度コンパイルしてアプリを起動させる
  - このテストでは Play Store からアプリをインストールやアップデートした後の初回起動の状態を再現している
- `CompilationMode.Partial` は Baseline Profiles のみを利用してプリコンパイルされた状態でのテストになる
  - このテストでは Play Store からアプリをインストールやアップデートした後の初回起動の状態で Baseline Profiles が適用された状態を再現している


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/33a5bdc2-0705-cd7a-de6f-df29f1e0be48.png" width="500px" />

- 上記は個人のプロジェクトである https://github.com/NUmeroAndDev/MaterialGallery-android で導入した結果
  - Android View のプロジェクトで、アプリ起動時の処理が少ないプロジェクトのため多少の改善程度になっている
  - Jetpack Compose の場合は Jetpack Compose のライブラリに Profiles のルールが仕組まれているため `CompilationMode.None` でのテストはかなりパフォーマンスが落ちる結果が出てくる

## 参考
- https://developer.android.com/topic/performance/baselineprofiles
- https://android-developers.googleblog.com/2022/01/improving-app-performance-with-baseline.html
- https://android-developers.googleblog.com/2019/04/improving-app-performance-with-art.html