性能テストでレイテンシのばらつきを減らしたい

Android アプリの性能を End-to-End でテストしようと起動時間などのレイテンシを素朴に計測すると、実行毎に結果のスコアがばらつく。そのせいで性能解析の試行錯誤がうまくいかない。以下ではそうしたレイテンシのばらつきをできるだけ減らすための方策を紹介する。

準備: Root のとれるデバイスを用意する

以下で紹介する方法の多くはシステムのパラメタを変更して性能を安定させようとする。そのために adb root は必須。なんとかして root になれるデバイスを確保したい。

多くのアプリでは実行に Google Play Services が必要な事実やインストールの手間を考えると、Lineage OS など何らかの custom ROM を使うのが現実的。

なおエミュレータは実行特性が実機とあまりに違うので性能を測るのには勧めない。ただ自分は仕事のアプリがエミュレータで動かないためエミュレータでの性能テストを真面目に試したことがない。アプリの性格によっては実用上問題ないこともありうる。各自ためされたし。

CPU クロックを固定する

システムはデバイスの負荷や発熱に応じて CPU や GPU のクロックを上げ下げする。クロックに応じてアプリの速さも変化してしまうので、これらのクロックは固定したい。

一番手堅いのは Jetpack Benchmark ライブラリに付属する lockClocks.sh を真似する方法。

それなりに複雑なスクリプトなので、このまま使うより自分たちの自動化の都合にあわせて断片をコピペして使う方が良いだろう。また固定したクロックを元にもどす方法が用意されていないため、テストが終了次第デバイスを再起動するとよい。

ページキャッシュを破棄する

アプリの実行性能に大きな影響を与える I/O のレイテンシはキャッシュの有無で大きく変化する。 テストの前には毎回キャッシュをクリアし、実行条件を揃えよう。

$ adb shell "echo 3 > /proc/sys/vm/drop_caches"

キャッシュがまったくない状態はテストとして現実的なのか?というと、まあまあ現実的である。Android はメモリ節約のため不要アプリのメモリをアグレッシブに破棄する。しばらく使われなかったアプリは、だいたいページキャッシュから追い出されていると思って良い。

Airplane Mode / オフラインにする

性能テストの大きなノイズ源の一つは背後でうごくアプリやサービスである。特に notification をうけとって行動する Play Services は悩みの種。Airplane Mode を有効にすると、それらの活動をある程度抑制できる。

性能のばらつきを減らす意味では WiFi や Cellular も無効にした方が良いが、それが現実的でないアプリも多いだろう。せめてテストの中でバックエンドの API レイテンシも測定しておき、API レイテンシに起因する性能のばらつきがどのくらい大きいのかは把握したい。

サーバサイドの API レイテンシのばらつきを小さくする方法についてここでは議論しない。

余計なアプリをインストールしない、無効化する

ノイズの原因となるアプリがわかっているなら、測定用デバイスではそれを無効化すると良い。

ART AOT を強制する

ART の AOT をテストの前に行い、最適化されたコードでテストを動かす。これはテスト毎に APK をインストールする場合は特に気をつけたい。そうしないと JIT, AOT 前の遅いコードが実行され、結果に影響する。

AOT を行うにはまずアプリを起動して 10 秒程度動かす(あるいはプロセスに SIGUSR1 を送る。) そのあとアプリのプロセスを殺し、以下のコマンドを実行する。

$ adb shell cmd package compile -m speed-profile -f your.package.name

IORap のプロファイルを収集する

TODO(morrita): あとで書く。参考: Improving app startup with I/O prefetching | by yanwang | Android Developers | Jul, 2020 | Medium

起動直後にテストしない

Android のシステムは起動直後に様々な初期化処理を行う。これには一分以上かかり、CPU も IO も忙しい。テストの冒頭でデバイスを再起動しているなら少なくとも一分程度は待ったほうが良い。

同じテストアカウントを使う

個人のリアルなアカウントで性能テストするとアカウントのもつデータに応じて結果がばらつく。もちろん自分のアカウントにおきている遅さを調べるためにテストをする場合はこの限りでない。

同じデバイスを使う

スマートホンの性能は同じデバイス間でもばらつきがあることが知られている。また経年による性能低下もありうる。

製品の規模によってはテストの並列化が必須でデバイスの固定が現実的でないこともある。仕方ないが、それでも bisect など数字の安定が重要な場面ではデバイスを固定できるとよい。

デバイスの温度を低く保つ

デバイスは温度が高くなると遅くなる。しかし性能のテストを繰り返し実行するとデバイスは熱くなりがちである。ソフトウェアで CPU のクロックを固定してもこうした発熱による性能低下を完全に防ぐことはできない。

デバイスの温度を監視し、温度が下がるまで待つことで温度の影響を小さくすることができる。

$ adb shell cat /sys/class/thermal/tz-by-name/battery/temp
# When the file above is missing...
$ adb shell cat /sys/class/thermal/thermal_zone1/temp
# See what it is for
$ adb shell cat /sys/class/thermal/thermal_zone1/type

システムは複数のセンサで温度を監視している。たとえばバッテリーと CPU にそれぞれセンサーがある。厳密にはどのセンサーを見張るべきかを議論すべきだが、個人的には “なんとなくそれっぽい値を返すセンサー” を使えば十分という印象。

できれば物理的な方策のほうが効き目が大きい。たとえば:

  • 冷房の効いた部屋にデバイスを置く。直射日光は論外。
  • 保冷パッドなどでデバイスを冷やす。

別の方策として、CPU の固定クロック数を低くしたり、意図的に CPU を停止することもできる。 ただし CPU を強制的に遅くするとテストでのレイテンシと現実のレイテンシが思わぬ形で乖離してしまうため、個人的には勧めない。

ばらつきをデバッグする

レイテンシにばらつきがあるときは、アプリやシステムの挙動をしらべて対策をする。まずは複数の計測結果をプロットしよう。時系列でのプロットとヒストグラムを眺める。

もしレイテンシが時間とともに長くなっていたら、おそらく発熱が原因だろう。先に紹介したコマンドでデバイスの温度も記録し、計測結果と照らし合わせよう。

ヒストグラムを眺める。結果は bell shape をしているだろうか。もし outlier (時々極端に遅いケース) があって最終スコアに影響を与えているなら

  1. もし最終スコアにレイテンシの平均を採用していたなら中央値を使うようにしよう。
  2. テストに Perfetto などを組み込み、遅さの原因を詳しく調査しよう。

ヒストグラムには思わぬ発見があることもある。たとえば結果が bell shape でなく bimodal (ふたこぶラクダ) であることに気づくかもしれない。そうした modality は大抵がアプリのバグである。

試行の独立性と熱

レイテンシが単にばらついているだけなら、試行回数を十分に増やせば平均値は安定するはずだ。ネットワークやバックグランドプロセスの影響はこうしたノイズとみなすことができる。従って試行回数を増やし数の力でねじ伏せることができる。ただしテストのターンアラウンドが長くなってしまうので、テスト環境の本物らしさを多少犠牲にしてでもノイズが少ないに越したことはない。別の言い方をすると、ネットワークやバックグランドプロセスについてだけ考えるならレイテンシの分布は i.i.d である。

一方、発熱からの影響は試行回数を増やしても消し去ることができない。試行のたびに「デバイスの温度」という試行をまたいだグローバル変数が一方向(=高い側)に変化し続けるからだ。試行の独立性が失われ、数の力でねじ伏せることができなくなる。だから性能テストにおける熱対策は重要で、かつ難しい。

先に紹介した各種熱対策はデバイス温度を低く保つことで試行の独立性を保とうとする。けれど「熱というグローバル変数を一定に保つ」という視点で見ると、アプローチはもう一つあり得る。つまり、デバイスの温度を「高く」保つことはできないだろうか。

デバイスの温度はテストを繰り返し実行するうちに段々上がっていくが、ある時点で頭打ちになる。レイテンシもその時点で安定する。だから温度を監視しながらテストを実行しつづけ、温度が安定した時点からのレイテンシだけを使えばばらつきを減らすことができる。

この方法には欠点もある:

  • 温度が頭打ちになるまでにかかる時間が長い。
  • 室温、デバイスの種類、テストの負荷によって頭打ちになる温度(およびスコア)が異なる。
  • テストの負荷やシステムの堅牢性によってはシステムがクラッシュしてしまう。いわゆる熱暴走。

だから普通は温度を低く保つ常識的なアプローチが望ましい。ただ色々な都合でそれができないとき(たとえばテスト結果提出の締切前に熱波が来てしまったが部屋に冷房がないとき)にはこの高どまりアプローチが助けになるかもしれない。

参考リンク

Ack

草稿に目を通して感想をくれた皆様ありがとう: Kazuyoshi Kato, Yuichi Tateno, nattou.org, karino2, Jun Mukai, Kenichi Ishibashi, Kensuke Nagae, Yohei Yukawa, Hiroshi Kurokawa