GPU レンダリングのプロファイル作成ツールによる分析

GPU レンダリングのプロファイル作成ツールを使って、レンダリング パイプラインの各ステージで前のフレームをレンダリングするのにかかる相対時間を確認できます。この情報からパイプライン内のボトルネックを特定できるため、アプリのレンダリング パフォーマンスを改善するために何を最適化すべきかがわかります。

このページでは、各パイプライン ステージで行われる内容について簡単に説明し、ボトルネックの原因になり得る問題を検討します。このページを読む前に、GPU レンダリングのプロファイル作成に記載されている内容を理解しておいてください。また、各ステージが全体としてどのように連携しているかを理解するためにも、レンダリング パイプラインの仕組みを確認しておくことをおすすめします。

視覚的な表示

GPU レンダリングのプロファイル作成ツールには、各ステージとその相対時間がグラフの形式(色分けされたヒストグラム)で表示されます。図 1 に表示例を示します。

図 1. GPU レンダリングのプロファイル作成ツールのグラフ

GPU レンダリングのプロファイル作成ツールのグラフに表示される各縦棒の各セグメントはパイプラインのステージを表し、棒グラフ内で特定の色を使って表示されます。図 2 に、各色の意味の凡例を示します。

図 2. GPU レンダリングのプロファイル作成ツールのグラフの凡例

各色の意味を把握したら、アプリの特定の要素にターゲットを絞って、レンダリング パフォーマンスを最適化できます。

ステージとその意味

このセクションでは、図 2 の色に対応する各ステージで行われることと、注意すべきボトルネックの原因について説明します。

Input Handling(入力処理)

パイプラインの Input Handling ステージでは、アプリが入力イベントの処理に費やした時間を測定します。この指標は、入力イベントのコールバックの結果として呼び出されるコードの実行にアプリが費やした時間を示します。

このセグメントが大きい場合

このセグメントが大きくなるのは、通常、入力ハンドラのイベント コールバック内で行われる処理が多すぎるか、複雑すぎる場合です。 これらのコールバックは必ずメインスレッドで行われるため、この問題を解決するためには、処理を直接最適化するか、処理を別のスレッドにオフロードすることに重点を置きます。

また、このフェーズで RecyclerView のスクロールを表示できることも注目に値します。RecyclerView は、タッチイベントを処理するとすぐにスクロールします。その結果、新しいアイテムビューがインフレートまたはポピュレートされることがあります。そのため、この処理をできるだけ高速化することが重要です。Traceview や Systrace などのプロファイル作成ツールを使用すると、さらに詳しく調べることができます。

アニメーション

Animation フェーズは、フレーム内で実行されていたすべてのアニメーターを評価するのにかかった時間を示します。最も一般的なアニメーターには、ObjectAnimatorViewPropertyAnimatorTransition があります。

このセグメントが大きい場合

このセグメントが大きくなるのは、通常、アニメーションのプロパティ変更に伴って実行される処理に起因します。たとえば、ListView または RecyclerView をスクロールするフリング アニメーションは、ビューのインフレーションとポピュレーションが何度も行われる原因となります。

Measurement / Layout(測定 / レイアウト)

Android がビューアイテムを画面に描画するために、ビュー階層内のレイアウトとビューで 2 つの特定のオペレーションを実行します。

まず、システムがビューのアイテムを測定します。すべてのビューとレイアウトには、画面上のオブジェクトのサイズを示す特定のデータが含まれています。ビューには、特定のサイズを指定できるものと、親レイアウト コンテナのサイズに適合するサイズを指定できるものがあります。

次に、システムがビューのアイテムをレイアウトします。システムが子ビューのサイズを計算すると、レイアウトを開始して、画面上でビューのサイズ調整と位置決めを行えるようになります。

システムは、描画されるビューだけでなく、それらのビューの親階層について、ルートビューまでの測定とレイアウトを実行します。

このセグメントが大きい場合

アプリがこのステージで費やしたフレームあたりの時間が長い場合、たいていは、レイアウトする必要があるビューが多いか、階層内の不適切な場所で二重課税などの問題が発生していることが原因です。いずれの場合も、パフォーマンスの問題に対処するには、ビュー階層のパフォーマンスを改善する必要があります。

onLayout(boolean, int, int, int, int) または onMeasure(int, int) に追加したコードが、パフォーマンスの問題の原因となることもあります。TraceviewSystrace を使用すると、コールスタックを調べてコードの潜在的な問題を特定することができます。

Draw

Draw ステージでは、ビューのレンダリング処理(背景やテキストの描画など)を一連のネイティブな描画コマンドに変換します。システムはこれらのコマンドをキャプチャして表示リストに追加します。

Draw セグメントは、コマンドをキャプチャして表示リストに追加するのにかかる時間を示します(画面のフレーム内で更新する必要があるすべてのビューが対象)。測定された時間は、アプリ内で UI オブジェクトに追加したコードに適用されます。このようなコードの例として、onDraw()dispatchDraw()、および Drawable クラスのサブクラスに属している各種の draw ()methods があります。

このセグメントが大きい場合

この指標は簡略化して述べると、無効化されたビューごとに onDraw() の呼び出しをすべて行うのにかかった時間を示します。この測定には、存在する可能性がある子およびドローアブルに描画コマンドをディスパッチするのにかかった時間が含まれます。そのため、このセグメントが急に大きくなった場合、多数のビューが急に無効化されたことが原因である可能性があります。ビューが無効化された場合は、ビューの表示リストを再生成する必要があります。また、時間がかかる原因として、いくつかのカスタムビューの onDraw() メソッドのロジックが非常に複雑であることも考えられます。

Sync / Upload(同期 / アップロード)

同期とアップロードの指標は、現在のフレームでビットマップ オブジェクトを CPU メモリから GPU メモリに転送するのにかかる時間を表します。

CPU と GPU は別々のプロセッサであるため、処理専用の RAM 領域も異なります。Android では、ビットマップを描画する際に、GPU がビットマップを画面にレンダリングする前にシステムによってビットマップが GPU メモリに転送されます。さらに、テクスチャが GPU テクスチャ キャッシュから削除されない限りシステムがデータを再転送する必要が生じないよう、GPU がビットマップをキャッシュに保存します。

注: Lollipop デバイスでは、このステージは紫色で表示されます。

このセグメントが大きい場合

フレーム用のすべてのリソースは、フレームの描画に使用する前に GPU メモリ内に存在している必要があります。つまり、このセグメントが大きい場合、少量のリソースの読み込みを何度も行ったか、大量のリソースの読み込みを数回だけ行ったことを意味します。よくあるのは、画面のサイズに近い単一のビットマップをアプリで表示するケースです。また、アプリで多数のサムネイルを表示するケースもあります。

このセグメントを小さくするには、次のような手法を使用します。

  • ビットマップの解像度が表示サイズよりあまり大きくならないようにします。たとえば、アプリで 1024x1024 の画像を 48x48 の画像として表示しないようにします。
  • prepareToDraw() を利用して、次の同期フェーズの前にビットマップを非同期で事前アップロードします。

Issue Commands(コマンドの発行)

Issue Commands セグメントは、表示リストを画面に描画するために必要なすべてのコマンドを発行するのにかかる時間を表します。

システムは、ディスプレイ リストを画面に描画するために、必要なコマンドを GPU に送信します。通常、このアクションは OpenGL ES API を介して実行します。

システムがコマンドを GPU に送信する前にコマンドごとに最終的な変換とクリップを実行するため、このプロセスには時間がかかります。さらに、GPU 側で追加のオーバーヘッド(最終的なコマンドの計算)が生じます。これらのコマンドには、最終的な変換と追加のクリップが含まれています。

このセグメントが大きい場合

このステージで費やされる時間は、システムが特定のフレームにレンダリングする表示リストの複雑さと量の直接的な指標になります。たとえば、描画処理が多い場合(特に、各描画プリミティブに固有のコストが少しずつある場合)、この時間が長くなる可能性があります。 次に例を示します。

Kotlin

for (i in 0 until 1000) {
    canvas.drawPoint()
}

Java

for (int i = 0; i < 1000; i++) {
    canvas.drawPoint()
}

上のコードは次のコードより発行コストがかなり大きくなります。

Kotlin

canvas.drawPoints(thousandPointArray)

Java

canvas.drawPoints(thousandPointArray);

コマンドの発行と表示リストの実際の描画の間に、常に 1 対 1 の関係があるとは限りません。Draw 指標は、描画コマンドを GPU に送信するのにかかる時間をキャプチャする Issue Commands とは異なり、発行済みのコマンドをキャプチャして表示リストに追加するのにかかった時間を表します。

この違いが生じるのは、表示リストが可能な限りシステムによってキャッシュに保存されるためです。そのため、スクロール、変換、アニメーションを行うためにシステムが表示リストを再送信しなければならないことはありますが、実際に表示リストをゼロから再作成(描画コマンドを再キャプチャ)する必要はありません。結果として、Draw セグメントは大きくならず、Issue Commands セグメントが大きくなることがあります。

Process / Swap Buffers(バッファの処理 / 交換)

Android がすべての表示リストを GPU に送信し終えると、現在のフレームの描画が完了したことをグラフィックス ドライバに伝えるために、システムが最後の 1 つのコマンドを発行します。ドライバはこの時点でようやく、更新された画像を画面に表示できます。

このセグメントが大きい場合

重要なのは、GPU と CPU が同時に処理を実行することを理解することです。Android システムは描画コマンドを GPU に発行してから次のタスクに進みます。GPU は描画コマンドをキューから読み取って処理します。

GPU によるコマンドの処理が CPU によるコマンドの発行に追いつかない場合、プロセッサ間の通信キューがいっぱいになることがあります。その場合、CPU はブロックを行い、次のコマンドをキューに登録できるようになるまで待機します。キューがいっぱいの状態は、Swap Buffers ステージでよく発生します。これは、この時点でフレーム全体のコマンドがすでに送信されているためです。

この問題を軽減する鍵は、「コマンドを発行」フェーズで行うのと同様に、GPU で発生する処理の複雑さを軽減することです。

その他

レンダリング システムが処理を実行するのにかかる時間に加え、メインスレッドで実行される、レンダリングと無関係の処理もあります。この処理にかかる時間は、「その他の時間」として報告されます。これは一般に、2 つの連続するレンダリング フレーム間において UI スレッドで実行される可能性がある処理を表します。

このセグメントが大きい場合

このセグメントが大きい場合、コールバック、インテント、または別のスレッドで実行する必要があるその他の処理がアプリに含まれている可能性があります。メソッド トレースSystrace などのツールを使って、メインスレッドで実行されているタスクを表示できます。この情報を活用することで、パフォーマンスの改善点を絞り込むことができます。