Off-CPU Analysis

Android アプリの高速化は、プロファイラを睨みながら CPU のサイクル数を減らしていく古典的なチューニングばかりではない。遅さの原因はしばしばスレッドが何らかのロックや I/O に待たされているために起こる。特に platform の API は思わぬ場所で Binder call を呼び出すことが多く、不思議な遅さにプログラマは首を傾げる。こうしたブロッキングコールによって眠ってしまったコードの所在を絞り込み、なんとかしてクリティカルパスから追い出したい。

前に書いた通り、Android の CPU プロファイラにはブロッキングコールでの待ち状態を扱うのが得意でないものもある。それを踏まえ、この記事ではクリティカルパスからブロッキングを探し出す手順を紹介する。なお Brendan Gregg はこの手の作業を Off-CPU Analysis と読んでいる。

題材として、わざとらしくワーカースレッドのループでスリープを続けるアプリを書いた。ここにある

Perfetto Trace

まずアプリを実行して Perfetto Trace を取ろう。Perfetto はピンポイントでロックのありかを教えてはくれないが、かわりにスレッドが寝ているか起きているかをひと目で判断できる。対象のコードパスでスレッドは眠っていないかもしれず、それなら Off-CPU Analysis は必要ない。要するに Perfetto はおおまかな現状を知る役に立つ。

スレッドが眠っている場合、Perfetto にはその原因となった Binder の行き先プロセスを追跡する機能、またロックの競合を可視化する機能もある。また時系列の可視化は問題のロックの背後でアクティブなスレッドを探すなどブロッキングの相関関係を憶測する助けにもなる。

サンプルアプリのトレースはこんなかんじ(実データはこれ):

MyWorker スレッドが LockWorker スレッドとロックの競合で待っているのがひと目でわかる。

スレッドが何かを待っていたなら、次はその原因を絞り込む。以下で二つのやり方を紹介する。

Trace API + Instrumentation Profiler

1つ目の方法では引き続き Perfetto を使う。まず beginSection()/endSection() といった Trace API で怪しい場所をマークし、ブロッキングを含む区間を絞り込んでいく。

ある程度絞り込んだら今度は startMethodTracing() で Instrumentation-based profile をキャプチャする。Instrumentation-based profile はスレッドの待ち時間もカウントしてれるため、遅い関数を特定できる。

この方法は確実だが、ややめんどくさい。

Simpleperf Off-CPU Analysis

Simpleperf にはそのものズバリ --trace-offcpu というフラグがある。

$ python ~/Android/Sdk/ndk/21.3.6528147/simpleperf/app_profiler.py \
  -p org.dodgson.android.sleepandprofile \
  -a .MainActivity  -r "-g -e cpu-cycles:u --trace-offcpu --duration 10"

(実行結果)

MyWorkerLockWorker がそれぞれロックや sleep() を待ち続けているのがわかる。

Perfetto と異なり Simpleperf では二つのスレッドの相関はわからない。また、たとえばスレッド数が多い上にライブラリが勝手に作る短命スレッドの中で問題が起きていたりすると simpleperf は使いにくい。とはいえ Instrumentatino Profiler を使う前述のベタな方法と併用すればだいたいのケースはカバーできるだろう。

隠れた Binder calls

以下では森田が一番最近みつけた隠れた Binder call を紹介する。この例が特別面白いわけではなく、これと似たようなことがしょっちゅうランダムに起こる Android API の雰囲気を伝えたい。

android.media.MediaCodecList整数のフラグ一つを受け取るコンストラクタを持つなんてことないデータクラスのように見える。このリストの中身を埋めるために設定ファイルか何かに書いた値を読むかもしれないから、ファイルを読むコストくらいは予期できる。

しかしこのクラスは初回初期化時に複数の Binder call を使い他のプロセス (mediaserver) から Codec のメタデータをロードする。システムの混雑状況次第でこの処理に 100ms 近くかかる。

この遅さは高負荷時にしか再現しないため素朴なテストでは発覚せず、社内ベータテスターが実用デバイスで採取した Perfetto トレースをみて初めて明らかになることが多い。

関連リンク

Ack

草稿に目を通して感想をくれた皆様ありがとう: Kenichi Ishibashi, Jun Mukai, karino2, Kensuke Nagae, Hiroshi Kurokawa