Android Rendering Profiler

なんかすごいツールの話ではなく、画面の上に出せるアレ

実際に遅さを調べる段になると結構良い。自分の場合、普段さわっていない画面の何かが遅いから直せをバグをよこされ、まずどのくらい遅いのかを調べるのに使った。Systrace の方が詳しいけれど、この on screen profiler にも良い所がある。リアルタイムで結果が見えるし、ずっと動かしっぱなしにできる。ダンプした結果をホスト側で見ないといけないし、一回あたり数秒しかトレースできない Systrace と比べ、探索的な調査に向いている。なんとなくアプリをつつきながらグラフを眺め、平均的な挙動のトレンドを眺めたり、スパイクの出るタイミングを探したりする。

件のバグはタッチイベントがくるたび全ての View が rebind されるというもので、この rendering profiler で眺めたら UI thread の負荷が一目でわかる異常を示していた。でもこれって肉眼で見てるだけだと案外気づかないのだよね。デバイスが速かったり自分の目が節穴だったりして。

表示されているバーのうち、寒色系の部分は UI thread, 暖色系の部分は Render thread での経過時間を示している。二つのスレッドの結果を一つのバーに押し込んでいいのか疑問だったが、コードをみると所定のチェックポイントでのクロックを記録し描画の際にチェックポイントの間隔を長さとして描いていた。つまり並列に動いたぶんは本筋の処理にマスクされ、バー表示には計上されない。正しい。

別の見方をすると、Render thread  は UI thread で view tree の drawing traversal がおわるまで仕事ができない。その順序は直列化されている。当たり前だけど UI thread  がさっさとdraw を終わらせないと Render thread を utilize できない。各スレッドが素朴に 16ms まるごと使えるわけではない.

GapWorker

Draw のおわった UI thread はもうそのフレームでやることがない。だとしたら Render thread に描画を切り出すのって微妙に無駄じゃね?そう思いつつアプリの Systrace を眺めていたら面白いことに気づいた: RecyclerView は draw 後にある UI Thread の空白時間を使い、先のフレームで描くことになるであろう View を前もってレイアウトしている (GapWorker.java など参照。) これなら次のフレームの view traversal でレイアウトを計算せずに済み、Render thread を待たせない。UI thread の時間を最大限ににつかっている。賢い。

必要な View を事前に予測できる RecyclerView ならではのテクニックだけれど、そもそも UI を jank-free にしたい場面の多くは RecyclerView 的なスクロールなので、この頑張りはまったく正しい。RecyclerView の評価があがった。

Render Thread vs UI Thread

Rendering profiler を眺めていると、フレームの大半の時間は暖色系の render thread で使われいることがわかる。なにかと非難されがちな UI thread, 案外遅くない。とはいえ多くのアプリはぽろりぽろりとフレームを落とす。そしてフレームを落とす原因の多くは UI thread の spike にある。つまり UI thread には大体速いが時々遅い。

UI thread を spike される原因はアプリ次第だけれど、ありがちなのはどういうものだろう。

しょぼい理由: 本来バックグランドでできるものをさぼって UI thread でやっている。ちょっとしたサービスのメソッドを呼んだら実は遅かったとか、サーバからきた JSON をもとに view-model 的な UI 寄りのオブジェクトを作る部分が UI thread だったとか。特に API なりデータベースなりから帰ってきたデータがでかいと UI thread  を spike させがち。

もうちょっと本質的に大変なもの: View の遅さ。Inflation や layout といった処理は基本的に off-thread にはできないので、たまに遅い。Recycler View みたいに必要な差分だけ処理するならまに合うけれど, たとえば ViewPager とか Drawer みたいに大量の View を一度に新しく表示しようすると、inflate/layout/draw にはどうしても時間がかかりがち。事前に inflate しておいたり、逆に最初は placeholder を出しておき後から段階的に中身を埋めていくなどの小細工が必要になる。が、めんどい。理想的にはがんばって view を flatten し絶対的な計算量を減らせるのが一番。でもそれは更に大変。

UI thread 以外での計算も影響する。Sync や fetch なんかが典型だけど、あとは GC もけっこう影響がある。Stop the world は随分少なくなった最近の Android GC だけれど、別スレッドで動く collector や finalizer みたいなやつらもけっこう CPU を使う。あとは他のアプリのサービスとか。

結局コアが 4 つとかしかないところで並列に動く計算がそれ以上あるとメモリアクセスやロックの競合みたいな細かい話をするまでもなく UI thread は遅くなる。そしてコア以上に仕事があることはぼちぼちある。なにしろ UI thread と render thread だけで 1.5 コアくらいつかっているから、他の仕事のための余裕それほど多くない。

Systrace はどの software thread がどの CPU で動いていたかを記録してくれる。CPU時間不足が一目でわかってよい。

Render Thread の内訳

Rendering profiler では、Render Thread の仕事を 1, Bitmap の upload, 2. Display list の traversal, 3. GPU の計算待ち の三段階に分類している。で、特別な場合を除くとだいたいは 2 が支配的。というか、UI thread を含めても 2 の display list traversal (と、それにともなう GL API 呼び出し) が一番遅い。バーのうち赤い部分。

伝統的な 3DCG の視点で見ると、これはちょっと面白い。GPU に計算をさせるというとき、3DCG だと vertex buffer とかに描画に必要なデータを詰めこんでまとめて GPU に送り、粗粒度で GPU に計算を任せる。テクスチャとかも atlas とかにまとめる。Android の render thread はそういうことはしない。というかまあ、できない。アプリの UI はバッチ描画向けに作られていないから。描画順序の reordering で状態の flush を最小化するような工夫はあるらしいけれど、基本的には毎フレームベタベタと描いていく。

Core Graphics みたいに描画結果をテクスチャとしてキャッシュすることもない。いちおう Surface を使えば似たようことができなくもないけれど、すくなくとも RecyclerView はそういう作りにはなってない。毎フレーム律儀に GL で順番にビューを描いて、それで 60fps を目指す。険しい道のり。現実的でもある。

Display list のトラバースが遅いのを見ると GPU 化も台無しじゃん、とか一瞬思ってしまうけれど、よく考えると描画を GPU に offload できたおかげで display list の遅さだけが残ったとも言える。そのうち Vulkan の command buffer  を cache するとかいいはじめるとかっこいいんだけど、どうかねえ。

あわせてよみたい: Android Graphics Architecture