アプリ パフォーマンスの測定の概要

このドキュメントでは、アプリの主なパフォーマンスの問題を特定して修正する方法について説明します。

主なパフォーマンスの問題

アプリのパフォーマンス低下の原因となる問題は数多くありますが、アプリで注意すべき一般的な問題としては、次のようなものが挙げられます。

スクロール ジャンク

「ジャンク」という用語は、システムが要求された頻度(60 Hz 以上)で画面に描画するフレームを時間内にビルド、提供できない場合に発生する視覚的な中断を表します。ジャンクはスクロール時に最も顕著に現れ、スムーズにアニメーション化されるべきフローで中断が発生します。アプリがコンテンツをレンダリングするためにかかる時間がシステム上のフレーム継続期間よりも長いため、1 つ以上のフレームの途中で動きが一時停止し、ジャンクが現れます。

アプリは 90 Hz のリフレッシュ レートをターゲットにする必要があります。従来のレンダリング レートは 60 Hz ですが、新しいデバイスの多くは、スクロールなどのユーザー操作中に 90 Hz モードで動作します。一部のデバイスでは、さらに高いレート(120 Hz まで)がサポートされています。

ある時点でデバイスが使用しているリフレッシュ レートを確認するには、[開発者向けオプション] > [リフレッシュ レートの表示]([デバッグ] セクション内)を使用してオーバーレイを有効にします。

起動レイテンシ

起動レイテンシは、アプリアイコン、通知、その他のエントリ ポイントをタップしてから、ユーザーのデータが画面に表示されるまでにかかる時間です。

アプリで次の起動目標を目指します。

  • 500 ミリ秒未満でコールド スタート。コールド スタートは、起動中のアプリがシステムのメモリに存在しない場合に発生します。これは、再起動後、あるいはユーザーまたはシステムによるアプリプロセスの強制終了後、最初にアプリを起動したときに発生します。

    一方、ウォーム スタートはアプリがすでにバックグラウンドで実行されているときに発生します。コールド スタートでは、ストレージからすべてを読み込んでアプリを初期化する必要があるため、システムの負荷が最も多くなります。コールド スタートにかかる時間は、500 ミリ秒以下を目指してください。

  • レイテンシの中央値に非常に近い P95 レイテンシおよび P99 レイテンシ。アプリの起動に時間がかかると、ユーザー エクスペリエンスが低下します。アプリ起動のクリティカル パスにプロセス間通信(IPC)や不要な I/O があると、ロック競合が発生し、不整合が生じることがあります。

スムーズでない遷移

タブの切り替えや新しいアクティビティの読み込みなどの操作中に発生します。このタイプの遷移は、滑らかなアニメーションでなければならず、遅延や視覚的なちらつきを含んではいけません。

電力の非効率性

負荷がかかると電力が消費されます。不要な負荷がかかるとバッテリー駆動時間が短くなります。

コードに新しいオブジェクトを作成することで発生するメモリ割り当てが原因で、システムに大きな負荷がかかることがあります。これは、割り当て自体が Android ランタイム(ART)の労力を必要とするためだけでなく、後でそのオブジェクトを解放する場合(「ガベージ コレクション」)に時間と労力が必要になるためでもあります。割り当てとコレクションはどちらも、特に一時オブジェクトについては、以前に比べてはるかに高速かつ効率的です。以前は、可能な限りオブジェクトの割り当てを避けることがベスト プラクティスでしたが、アプリとアーキテクチャに最も適したものにすることをおすすめします。ART の能力を考えると、コードの管理が困難になるリスクを冒してまで割り当てを控えることはおすすめしません。

ただし、労力が必要になるため、内部ループで多くのオブジェクトを割り当てると、パフォーマンスの問題を引き起こす可能性があることに留意してください。

問題を特定する

パフォーマンスの問題を特定して修正するために、次のワークフローをおすすめします。

  1. 以下のクリティカル ユーザー ジャーニーを特定して検査します。
    • 一般的な起動フロー(ランチャーや通知を含む)。
    • ユーザーがデータをスクロールする画面。
    • 画面間の遷移。
    • ナビゲーションや音楽の再生のような長時間実行フロー。
  2. 次のデバッグツールを使用して、フローで何が起こっているかを検査します。
    • Perfetto: 正確なタイミング データを使用して、デバイス全体で何が起こっているかを確認できます。
    • Memory Profiler: ヒープで発生しているメモリ割り当てを確認できます。
    • Simpleperf: 特定の期間にどの関数呼び出しが最も多く CPU を使用しているかを示すフレームグラフを表示します。Systrace で時間がかかっているものを特定しても、その原因がわからなかった場合、Simpleperf が追加情報を提供します。

こうしたパフォーマンスの問題を確認してデバッグするには、個々のテスト実行を手動でデバッグすることが重要です。集計データを分析しても、前の手順を置き換えることはできません。ただし、ユーザーに実際に何が表示されているかを理解し、いつ回帰が発生する可能性があるかを特定するには、自動テストとフィールドで指標の収集をセットアップすることが重要です。

  • 起動フロー
  • ジャンク
    • フィールド指標
      • Google Play Console のフレーム指標: Google Play Console では、指標を特定のユーザー ジャーニーに絞り込むことはできません。アプリ全体のジャンクが報告されるだけです。
      • FrameMetricsAggregator によるカスタム測定: FrameMetricsAggregator を使用して、特定のワークフロー中にジャンク指標を記録できます。
    • ラボテスト
      • Macrobenchmark によるスクロール
      • Macrobenchmark は、単一のユーザー ジャーニーを囲む dumpsys gfxinfo コマンドを使用してフレーム時間を収集します。これにより、特定のユーザー ジャーニーに対するジャンクの変化を把握します。RenderTime 指標は、フレームの描画にかかる時間を強調するものであり、回帰または改善の特定では、ジャンクのあるフレームの数よりも重要です。

パフォーマンス分析向けにアプリをセットアップする

正確で再現可能かつ実用的なベンチマークをアプリから取得するには、適切なセットアップが不可欠です。ノイズの原因を抑制しながら、できるだけ本番環境に近いシステムでテストします。以降のセクションでは、テストのセットアップを準備するための、APK とシステム固有の手順をいくつか紹介します。一部はユースケース固有のものです。

トレースポイント

アプリは、カスタム トレース イベントを使用してコードをインストルメント化できます。

トレースをキャプチャしている間、トレースにはセクションごとにわずかなオーバーヘッド(約 5 マイクロ秒)が発生するため、すべてのメソッドにトレースを適用することは避けてください。0.1 ミリ秒を超える大きな単位の処理をトレースすることで、ボトルネックに関する重要な分析情報を得ることができます。

APK に関する考慮事項

デバッグ バリアントは、スタック サンプルのトラブルシューティングやシンボル化に役立ちますが、パフォーマンスに著しい非線形の影響を与えます。Android 10(API レベル 29)以降を搭載しているデバイスでは、マニフェストで profileable android:shell="true" を使用して、リリースビルドでのプロファイリングを有効にできます。

本番環境レベルのコード圧縮構成を使用します。アプリで使用するリソースによっては、これがパフォーマンスに大きな影響を与える可能性があります。一部の ProGuard 構成ではトレースポイントが削除されるため、テストを実行する構成でこれらのルールを削除することを検討してください。

コンパイル

デバイス上のアプリを既知の状態(通常は speed または speed-profile)にコンパイルします。バックグラウンドのジャストインタイム(JIT)アクティビティにより、重大なパフォーマンス オーバーヘッドが生じる可能性があり、テスト実行の間に APK を再インストールする際に頻繁に生じます。これを行うコマンドは次のとおりです。

adb shell cmd package compile -m speed -f com.google.packagename

speed コンパイル モードでは、アプリが完全にコンパイルされます。speed-profile モードでは、アプリの使用中に収集された利用コードパスのプロファイルに沿ってアプリがコンパイルされます。プロファイルを一貫して正確に収集することは難しいため、使用する場合は、想定どおりに収集されていることを確認してください。プロファイルは次の場所にあります。

/data/misc/profiles/ref/[package-name]/primary.prof

Macrobenchmark では、コンパイル モードを直接指定できます。

システムに関する考慮事項

低レベルと高忠実度の測定では、デバイスを調整します。同じデバイス、同じ OS バージョンで、A/B 比較を実行します。同じデバイスタイプであっても、パフォーマンスが大幅に異なる場合があります。

ユーザーに root 権限のあるデバイスでは、Microbenchmark に lockClocks スクリプトを使用することを検討してください。特に、これらのスクリプトは次の処理を行います。

  • CPU を固定周波数で配置する。
  • 小さなコアを無効にして、GPU を構成する。
  • サーマル スロットリングを無効にする。

lockClocks スクリプトは、アプリの起動、DoU テスト、ジャンクテストなど、ユーザー エクスペリエンスに重点を置いたテストに使用することはおすすめしませんが、Microbenchmark テストではノイズを減らすために不可欠な場合があります。

可能であれば、Macrobenchmark などのテスト フレームワークを使用することを検討してください。測定時のノイズを減らし、不正確な測定を防ぐことができます。

アプリの起動が遅い: 不要なトランポリン アクティビティ

トランポリン アクティビティでは、アプリの起動時間が不必要に長くなる可能性があり、アプリがそれを行っているかどうかを認識することが重要です。次のトレース例に示すように、最初のアクティビティによってフレームが描画されることなく、1 つの activityStart の直後に別の activityStart が続きます。

alt_text

図 1. トランポリン アクティビティを示すトレース。

これは、通知エントリポイントと通常のアプリ起動エントリポイントの両方で発生し、多くの場合はリファクタリングで対処できます。たとえば、別のアクティビティが実行される前にそのアクティビティを使用してセットアップを行う場合は、そのコードを再利用可能なコンポーネントまたはライブラリに分解します。

頻繁に GC をトリガーする不要な割り当て

Systrace では、ガベージ コレクション(GC)が想定よりも頻繁に発生することがあります。

次の例では、長時間実行オペレーション中の 10 秒ごとに、アプリが時間の経過とともに不必要に、一貫して割り当てを行っている可能性があることを示しています。

alt_text

図 2. GC イベント間のスペースを示すトレース。

また、Memory Profiler を使用する場合、特定のコールスタックが割り当ての大部分を行っていることがわかります。コードの保守が困難になる可能性があるため、すべての割り当てを積極的に排除する必要はありません。代わりに、割り当てのホットスポットに取り組むことから始めます。

ジャンクのあるフレーム

グラフィック パイプラインは比較的複雑で、最終的にフレーム落ちが発生するかどうかの判断には微妙な違いが生じる可能性があります。場合によっては、プラットフォームがバッファリングを使用してフレームを「レスキュー」できます。しかし、その微妙な違いの大部分を無視して、アプリの観点から問題のあるフレームを特定できます。

アプリからの処理をほとんど必要とせずにフレームが描画されているとき、60 FPS デバイスでは Choreographer.doFrame() トレースポイントは 16.7 ミリ秒周期で発生します。

alt_text

図 3. 頻繁な高速フレームを示すトレース。

ズームアウトしてトレースを移動すると、完了までにもう少し時間がかかるフレームが表示されることがありますが、割り当てられている 16.7 ミリ秒よりも時間がかかることはないため問題ありません。

alt_text

図 4. 処理が周期的にバーストする、頻繁な高速のフレームを示すトレース。

図 5 に示すように、この規則的な頻度が中断された場合は、ジャンクのあるフレームになります。

alt_text

図 5. ジャンクのあるフレームを示すトレース。

これらを特定する練習をしましょう。

alt_text

図 6. さらにジャンクのあるフレームを示すトレース。

場合によっては、どのビューがインフレートされているか、または RecyclerView が何を行っているかに関する詳細情報を得るために、トレースポイントにズームインする必要があります。また、さらなる調査が必要となることもあります。

ジャンクのあるフレームを特定して原因をデバッグする方法について詳しくは、遅いレンダリングをご覧ください。

RecyclerView に関するよくある間違い

RecyclerView のバッキング データ全体を不必要に無効にすると、フレームのレンダリング時間が長くなり、ジャンクが発生する可能性があります。代わりに、更新が必要なビューの数を最小限に抑えるために、変更されたデータのみを無効にします。

コストのかかる notifyDatasetChanged() 呼び出し(コンテンツを完全に置き換えるのではなく更新する)を回避する方法については、動的データを提示するをご覧ください。

すべてのネストされた RecyclerView が適切にサポートされていない場合、内部の RecyclerView が毎回完全に再作成される可能性があります。すべてのネストされた内部 RecyclerView に対して、すべての内部 RecyclerView 間でビューをリサイクルできるように、RecycledViewPool を設定する必要があります。

十分なデータをプリフェッチしなかったり、適切なタイミングでプリフェッチしなかったりすると、ユーザーがサーバーから追加のデータを待たなければならないときに、スクロール リストの一番下まで到達しづらくなる可能性があります。フレームのデッドラインには間に合うため、これは厳密にはジャンクではありませんが、ユーザーがデータを待つ必要がないようにプリフェッチのタイミングと量を変更すると、ユーザー エクスペリエンスが大幅に改善する可能性があります。