From Pinning To IORap (2): IORap

IORap はアプリ起動時の I/O を最小化する Android 11 の新機能である。以下の記事で詳しい仕組みが説明されている:

Improving app startup with I/O prefetching | by yanwang | Android Developers | Medium

詳細はリンク先に譲るとして、ざっくりいうと IORap は “Profile-Guided Prefetching” である。つまり:

  1. アプリの実行時にフレームワークが Linux カーネルのトレーシング機能を頼りにファイルアクセスをプロファイルし、特定アプリの起動に必要なファイルとレンジを記録する。
  2. あるタイミングでその記録を集計し「アクセスされうるファイル(の所定のページ)」リストをアプリ(正確には Activity) ごとに作る。
  3. このリストができたら、以降はアプリの起動にあわせ IORap の補助プロセスがリストにあるファイルを prefetch する。具体的にはファイルの先読みを指示する fadvise() システムコール を呼ぶ。

前回書いた Profile-Guided Pinning を考えると IORap の良さがわかる:

  • そもそも pinning が高価である: IORap は pinning と比べシステムのメモリを圧迫しない。iorapd という小さいプロセスが常駐するだけ。 アプリ単位のメモリオーバーヘッドがないため IORap は全てのアプリに適用できる。pinning は特定のアプリだけが対象だった。
  • pinlist はデバイスや設定の違いからくる差を表現できない: IORap はオンデバイスでプロファイルを取るためそのデバイスで実際に使われたファイルだけが prefetch される。
  • アプリの起動完了からプロファイルの収集の間にラグがある: IORap は Activity#reportFullyDrawn() というアプリ側からのシグナルでプロファイルを打ち切るためラグがない。
  • Pinlist の生成がめんどくさい: アプリは何もしなくていい!

IORap には pinning にない大きな利点がもう一つある: Pinning は対象が APK 内のファイルだけだったのに対し、IORap はシステムの全てのファイルを prefetch できる。つまりアプリだけでなく HAL (メディアアプリなら Codec HAL, カメラなら Camera HAL など), システムが持つ XML リソースやネイティブライブラリ、Play Services のファイルなども先読みできる。

性能テストによる比較

IORap と pinning を比較してみると性能はだいたい同じくらいだった。理論上は APK 外のファイルを扱える IORap の方が有利に思えるが、実行結果を詳しく見ると IORap はアプリの起動直後にアクセスされる DEX や OAT ファイルの prefetch がアプリからのファイルアクセスに追いつかないケースがあった。Pinning にその問題はない。特にシステムの負荷が高く,起動中のアプリプロセス以外から大量のファイルアクセスがあると出遅れの幅が大きくなるため、高負荷時は pinning が有利のようだ。(Caveat: 高負荷時の性能はテストが難しいため、この結論にそれほど確信はない。)

当たり前といえば当たり前な現象: IORap と pinning の両方を有効にするとどちらか一方だけを有効にするより常に速い。Pinning が起動直後のファイルアクセスを引き取っている間に IORap がそれ以降のファイルアクセス、特に HAL などプロセス外のファイルを準備できるためだろう。

ただ両方を有効にしてしまうと “高価” “面倒” という pinning の問題を解決できない。それでも pinning の主要な貢献が起動直後の特定のファイルだけなのだとしたら必要な pin は小さくなりそうだし、Profile-Guided-Pinning のような入り組んだ仕組みは必要なくなるかもしれない。

なお pinning が無関係なふつうのアプリにとって IORap は pure win である。

挙動を調べる

IORap が読み込むファイルサイズは adb dumpsys iorap でわかる。具体的なファイル名は以下のフラグをセットするとログに出力される。

$ adb shell setprop iorapd.log.verbose true

先読みファイルのリストはデバイスのアイドル時に計算される。アプリを 3-4 回 cold start したあとに以下のコマンドを呼び出すと計算を強制できる。

$ adb shell cmd jobscheduler run -f android 283673059

どうでもいいことだが 283673059 は乱数でない

欠点

Pinning にも IORap にも一つ欠点があると自分は考えている: どちらもクリティカルパス以外で発生しているファイルアクセスをクリティカルパスの blocker と区別できない。

アプリの起動にはクリティカルパスがある。多くの場合はメインスレッドがクリティカルパスの大半だろう。メインスレッドは UI リソースなどのファイルにアクセスする。これが prefetch なり pin なりされるのは良い。一方、もし起動中のアプリが背後のスレッドでクリティカルでない初期化を投機的に行っていたら、その非クリティカルな初期化コードがアクセスするファイルも先読みプロファイルに含まれてしまう。もし ML のモデルのようにサイズの大きなファイルを投機的に読んでいると、その prefetch がクリティカルパスにあるファイルの prefetch を阻害しうる。Pinning なら IO の阻害は起きないが、クリティカルでないぶんの pin は無駄に思える。

ただ背後のスレッドからのファイルアクセスがクリティカルパスのファイルアクセスを阻害するのは IORap と無関係に望ましくない。アプリの起動中には投機的なファイルアクセスをしない方が良いだけの話かもしれない。

IORap のコードは これ。割と複雑。森田は少しだけしか読んでいない。見どころの一つはファイルアクセスのプロファイリングに ftrace を直接は使わず Android のオンデバイストレーシングフレームワークである Perfetto を使っている点。Platform のサブシステムがプログラマブルにトレースを扱える Perfetto は observability の地平を広げている。そのうち紹介するかもしれない。

関連リンク