UI パフォーマンスをテストする

ユーザー インターフェース(UI)のパフォーマンス テストでは、アプリが機能要件を満たすことだけでなく、ユーザーがアプリをスムーズに操作できることを確認します。具体的には、安定して毎秒 60 フレーム(Why 60fps? の動画を参照)で動作し、フレームのドロップまたは遅延(ジャンクとも呼びます)が起こらないことを確かめます。このドキュメントでは、UI パフォーマンスの測定に使用できるツールについて説明し、UI パフォーマンスの測定値を実際のテストで利用する方法を示します。

UI パフォーマンスを測定する

パフォーマンスを改善するには、まずシステムのパフォーマンスを測定し、次にパイプラインのさまざまな場所で発生する問題を診断および特定する必要があります。

Android のツールである dumpsys は、デバイス上で動作し、システム サービスのステータスに関する重要な情報をダンプします。gfxinfo コマンドを dumpsys に渡すと、記録フェーズ中に実行されたアニメーションのフレームに関するパフォーマンス情報が logcat に出力されます。

    > adb shell dumpsys gfxinfo <PACKAGE_NAME>
    

このコマンドは、フレーム時間データの複数の異なるバリアントを生成します。

フレーム統計情報を集計する

Android 6.0(API レベル 23)では、コマンドにより、プロセスのライフタイム全体を通して収集されたフレームデータ分析の集計結果を logcat に出力できます。例:

    Stats since: 752958278148ns
    Total frames rendered: 82189
    Janky frames: 35335 (42.99%)
    90th percentile: 34ms
    95th percentile: 42ms
    99th percentile: 69ms
    Number Missed Vsync: 4706
    Number High input latency: 142
    Number Slow UI thread: 17270
    Number Slow bitmap uploads: 1542
    Number Slow draw: 23342
    

この統計データは、アプリのレンダリングのパフォーマンスと、大量のフレームの全体的な安定性を大まかに示します。

正確なフレーム時間情報

Android 6.0 には、gfxinfo 用の新しいコマンドが用意されています。それは、直近のフレームのきわめて詳細なフレーム時間情報を提供する framestats です。このコマンドにより、問題をより正確に追跡してデバッグできます。

    >adb shell dumpsys gfxinfo <PACKAGE_NAME> framestats
    

このコマンドは、アプリによって生成された直近の 120 フレームから、ナノ秒単位のタイムスタンプでフレーム時間情報を出力します。以下の例は、adb dumpsys gfxinfo <PACKAGE_NAME> framestats の未加工の出力を示しています。

    0,27965466202353,27965466202353,27965449758000,27965461202353,27965467153286,27965471442505,27965471925682,27965474025318,27965474588547,27965474860786,27965475078599,27965479796151,27965480589068,
    0,27965482993342,27965482993342,27965465835000,27965477993342,27965483807401,27965486875630,27965487288443,27965489520682,27965490184380,27965490568703,27965491408078,27965496119641,27965496619641,
    0,27965499784331,27965499784331,27965481404000,27965494784331,27965500785318,27965503736099,27965504201151,27965506776568,27965507298443,27965507515005,27965508405474,27965513495318,27965514061984,
    0,27965516575320,27965516575320,27965497155000,27965511575320,27965517697349,27965521276151,27965521734797,27965524350474,27965524884536,27965525160578,27965526020891,27965531371203,27965532114484,
    

この出力の各行は、アプリによって生成されたフレームを表します。各行には、フレーム生成パイプラインの各段階で費やされた時間を示す一定数の列があります。次のセクションでは、各列が何を表しているかなど、この形式について詳しく説明します。

framestats のデータ形式

データのブロックは CSV 形式で出力されるため、お好みのスプレッドシート ツールに貼り付けることも、スクリプトで集計および解析することも簡単にできます。下記のリストで、出力データ列の形式について説明します。タイムスタンプはすべてナノ秒単位です。

  • FLAGS
    • FLAGS 列が「0」の行は、FRAME_COMPLETED 列の値から INTENDED_VSYNC 列の値を引いた値、つまり合計フレーム時間を示します。
    • FLAGS 列が「0」でない場合、そのフレームは正常なパフォーマンスの外れ値である(レイアウトと描画の時間が 16 ミリ秒を超えている)と規定されているため、この行は無視する必要があります。これは、以下の原因で発生する可能性があります。
      • ウィンドウのレイアウトが変更された(アプリの最初のフレームが処理されたときや、回転された後など)。
      • フレームがスキップされた(この場合、一部の値に無意味なタイムスタンプが示されます)。たとえば、フレームレートが 60 fps を超える場合や、画面上にダーティなものが何も残らない場合は、フレームがスキップされる可能性があります。これは必ずしもアプリに問題がある兆候ではありません。
  • INTENDED_VSYNC
    • フレームの意図された開始ポイント。この値が VSYNC と異なる場合、vsync 信号にすぐに応答することを妨げる処理が UI スレッドで発生していたことを意味します。
  • VSYNC
    • すべての vsync リスナーとフレームの描画(Choreographer フレーム コールバック、アニメーション、View.getDrawingTime() など)で使用された時間の値。
    • VSYNC と、VSYNC がアプリに及ぼす影響の詳細については、Understanding VSYNC の動画をご覧ください。
  • OLDEST_INPUT_EVENT
    • 入力キュー内の最も古い入力イベントのタイムスタンプ。フレームの入力イベントがなかった場合は、Long.MAX_VALUE。
    • この値は、主にプラットフォーム処理の分析を目的としており、アプリ デベロッパーにとっての有用性は限られています。
  • NEWEST_INPUT_EVENT
    • 入力キュー内の最も新しい入力イベントのタイムスタンプ。フレームの入力イベントがなかった場合は、0。
    • この値は、主にプラットフォーム処理の分析を目的としており、アプリ デベロッパーにとっての有用性は限られています。
    • ただし、FRAME_COMPLETED から NEWEST_INPUT_EVENT を引いた値を確認すると、アプリに起因する待ち時間を大まかに知ることができます。
  • HANDLE_INPUT_START
    • 入力イベントがアプリにディスパッチされたときのタイムスタンプ。
    • この時刻から ANIMATION_START までの時間を確認することで、アプリが入力イベントの処理に費やした時間を測定できます。
    • この値が大きい(2 ミリ秒を超える)場合、View.onTouchEvent() などの入力イベントの処理にアプリが異常に長い時間を費やしていることを意味します。この処理の最適化または別のスレッドへのオフロードが必要かもしれません。状況によっては、この値が大きいことは想定の範囲内であり、許容されます。たとえば、新しいアクティビティ(またはそれに類するもの)を起動するクリック イベントなどです。
  • ANIMATION_START
    • Choreographer を使用して登録されたアニメーションが実行されたときのタイムスタンプ。
    • この時刻から PERFORM_TRANVERSALS_START までの時間を確認することで、実行中のすべてのアニメーター(ObjectAnimator、ViewPropertyAnimator、共通の遷移となっている Transitions)を評価するのにかかった時間を特定できます。
    • この値が大きい(2 ミリ秒を超える)場合、アプリでカスタム アニメーターを記述しているかどうかを確認してください。また、ObjectAnimators がどのフィールドをアニメーション化しているかを調べ、それらのフィールドがアニメーションに適しているかどうかを確かめてください。
    • Choreographer の詳細については、For Butter or Worse の動画をご覧ください。
  • PERFORM_TRAVERSALS_START
    • この値から DRAW_START の値を引くと、レイアウトと測定のフェーズが完了するまでにかかった時間を知ることができます(スクロールまたはアニメーションでは、この時間がゼロに近いことが望ましいことにご注意ください)。
    • レンダリング パイプラインのレイアウトと測定のフェーズの詳細については、Invalidations, Layouts, and Performance の動画をご覧ください。
  • DRAW_START
    • performTraversals の描画フェーズが開始された時刻。これは、無効化されたビューの表示リストを記録する開始ポイントです。
    • この時刻から SYNC_START までの時間は、ツリー内のすべての無効化されたビューで View.draw() を呼び出すのにかかった時間です。
    • 描画モデルの詳細については、Hardware Acceleration または Invalidations, Layouts, and Performance の動画をご覧ください。
  • SYNC_QUEUED
    • 同期リクエストが RenderThread に送信された時刻。
    • これは、同期フェーズを開始するメッセージが RenderThread に送信された時点を示します。この時刻から SYNC_START までの時間がかなり長い(およそ 0.1 ミリ秒を超える)場合、RenderThread が別のフレームの処理でビジーだったことを意味します。この値は、処理量が多く 16 ミリ秒のバジェットを超えているフレームと、16 ミリ秒のバジェットを超えている前のフレームによって処理が滞っているフレームを区別するために、内部的に使用されます。
  • SYNC_START
    • 描画の同期フェーズが開始された時刻。
    • この時刻から ISSUE_DRAW_COMMANDS_START までの時間がかなり長い(およそ 0.4 ミリ秒を超える)場合、通常は、GPU にアップロードする必要がある新しいビットマップが多数描画されたことを意味します。
    • 同期フェーズの詳細については、Profile GPU Rendering の動画をご覧ください。
  • ISSUE_DRAW_COMMANDS_START
    • ハードウェア レンダラが GPU への描画コマンドの発行を開始した時刻。
    • この時刻から FRAME_COMPLETED までの時間を確認することで、アプリがどれくらいの量の GPU 処理を発生させているかを大まかに知ることができます。オーバードローが多すぎる、レンダリング効果が十分でないなどの問題があると、この値で示されます。
  • SWAP_BUFFERS
    • eglSwapBuffers が呼び出された時刻。プラットフォーム処理の分析以外では、あまり意味がないデータです。
  • FRAME_COMPLETED
    • これで最後です。FRAME_COMPLETED の値から INTENDED_VSYNC の値を引くと、フレームを処理するのにかかった合計時間を計算できます。

このデータは、さまざまな方法で使用できます。たとえば、各種の遅延バケットでフレーム処理にかかった時間(FRAME_COMPLETED - INTENDED_VSYNC)の分布を示すヒストグラムは、単純ですが役に立ちます(下記の図を参照)。このグラフをひと目見るだけで、ほとんどのフレームは 16 ミリ秒の限界線(赤い線)を十分に下回った良好な状態であるけれども、いくつかのフレームは限界線を著しく上回っていることがわかります。このヒストグラムで時間の経過に伴う変化を確認することで、大規模な変化が起きているのか、それとも新しい外れ値が作成されているのかを知ることができます。また、データに含まれる多くのタイムスタンプに基づいて、入力待ち時間、レイアウトにかかった時間、またはその他の興味深い指標をグラフ化できます。

簡易フレーム時間ダンプ

[開発者向けオプション] で [GPU レンダリングのプロファイル作成](または [HWUI レンダリングのプロファイル作成])を [adb shell dumpsys gfxinfo を使用] に設定すると、adb shell dumpsys gfxinfo コマンドにより、直近の 120 フレーム時間の情報がいくつかのカテゴリ別にタブ区切り形式で出力されます。このデータは、描画パイプラインのどの部分の処理が遅いかを大まかに知るのに役立ちます。

前述の framestats と同様に、お好みのスプレッドシート ツールに貼り付けることも、スクリプトで集計および解析することも簡単にできます。以下のグラフは、アプリによって生成された多数のフレームのどこで処理に時間がかかったかの内訳を示しています。

これは、gfxinfo を実行し、出力結果をコピーしてスプレッドシート アプリケーションに貼り付け、データを積み上げ棒グラフで表現したものです。

縦棒はそれぞれアニメーションの 1 フレームを表し、縦棒の高さはそのフレームを処理するのにかかった時間(ミリ秒)を表します。また、縦棒の色分けされた層は、それぞれレンダリング パイプラインの異なる段階を表します。これにより、アプリのどの部分がボトルネックになっている可能性があるかを確認できます。レンダリング パイプラインとそれを最適化する方法については、Invalidations, Layouts, and Performance の動画をご覧ください。

統計データ収集用の時間枠を制御する

framestats と簡易フレーム時間ダンプは、どちらも非常に短い時間枠(約 2 秒間)のレンダリングに関するデータを収集します。たとえば、収集するデータを特定のアニメーションに限定したい場合に、時間枠を正確に制御するには、すべてのカウンタをリセットして、収集したデータを集計します。

    >adb shell dumpsys gfxinfo <PACKAGE_NAME> reset
    

この方法をダンプコマンドと組み合わせて使用し、2 秒未満の時間枠で連続してフレームをキャプチャしながら、定期的な頻度で収集およびリセットすることもできます。

パフォーマンス上の不具合を診断する

不具合を見つけることは、問題を追跡し、アプリの状態を良好に保つための最初のステップです。しかしながら、dumpsys は、問題の存在とその相対的な重大度を明らかにするだけです。それにとどまらず、パフォーマンス問題の具体的な原因を診断し、適切な解決方法を見つける必要があります。そのためには、systrace ツールの使用を強くおすすめします。

参考情報

Android のレンダリング パイプラインの仕組み、検出される可能性がある一般的な問題、それらの問題の解決方法については、以下のリソースが参考になります。

UI パフォーマンス テストを自動化する

UI パフォーマンスをテストする手法の 1 つとして、対象のアプリで人間のテスターが一連のユーザー操作を実行する方法があります。目視でジャンクを探すか、またはツールを使用し、長い時間をかけてジャンクを見つけます。ただし、この人力による手法は危険を伴います。フレームレートの変化を感知する能力には、かなり個人差があります。また、時間がかかる退屈な作業であるため、ミスが起こりがちです。

より効率的な手法は、自動化された UI テストにより、主要なパフォーマンス指標のログを記録して解析することです。Android 6.0 に含まれている新しいロギング機能を使用すると、アプリのアニメーションで発生するジャンクの量と重大度を簡単に確認できます。また、この機能を利用して、現在のパフォーマンスを測定し将来のパフォーマンス目標を達成するための綿密なプロセスを構築できます。

参考情報

このトピックについてもっと学ぶには、以下のリソースをご利用ください。

コードラボ