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

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

UI パフォーマンスの測定

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

dumpsys はデバイス上で動作し、システム サービスのステータスに関する重要な情報をダンプする Android のツールです。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レンダリングのプロファイル作成] を [In 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 に含まれている新しいロギング機能を使用すると、アプリのアニメーションで発生するジャンクの量と重大度を簡単に確認できます。また、この機能を利用して、現在のパフォーマンスを測定し将来のパフォーマンス目標を達成するための綿密なプロセスを構築できます。

その他のリソース

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

コードラボ