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

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

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

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

起動レイテンシ

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

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

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

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

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

スクロール ジャンク

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

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

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

スムーズでない遷移

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

電力の非効率性

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

コードに新しいオブジェクトを作成することで発生するメモリ割り当てが原因で、システムに大きな負荷がかかることがあります。これは、割り当て自体が 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 指標は、フレームの描画にかかる時間を強調するものであり、回帰または改善の特定では、ジャンクのあるフレームの数よりも重要です。

アプリリンクは、ウェブサイトに属していることが検証されたウェブサイトの URL に基づくディープリンクです。アプリリンクの検証が失敗する理由は次のとおりです。

  • インテント フィルタ スコープ: アプリが応答できる URL のインテント フィルタにのみ autoVerify を追加します。
  • 未検証のプロトコル スイッチ: 未検証のサーバー側とサブドメインのリダイレクトはセキュリティ リスクとみなされ、検証で不合格になります。これらが含まれていると、すべての autoVerify リンクが失敗します。たとえば、HTTPS リンクを検証せずに HTTP から HTTPS(example.com など www.example.com など)にリダイレクトすると、検証が失敗する可能性があります。インテント フィルタを追加して、アプリリンクを検証します。
  • 検証できないリンク: テスト目的で検証できないリンクを追加すると、アプリのアプリリンクがシステムで検証されない場合があります。
  • 信頼性の低いサーバー: サーバーがクライアント アプリに接続できることを確認してください。

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

正確で再現可能かつ実用的なベンチマークをアプリから取得するには、適切なセットアップが不可欠です。ノイズの原因を抑制しながら、できるだけ本番環境に近いシステムでテストします。以降のセクションでは、テストのセットアップを準備するための、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 を設定する必要があります。

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

アプリをデバッグする

アプリのパフォーマンスをデバッグするためのさまざまな方法を以下に示します。システム トレースと Android Studio Profiler の使用方法については、次の動画をご覧ください。

Systrace を使用してアプリの起動をデバッグする

アプリの起動プロセスの概要については、アプリの起動時間をご覧ください。システム トレースの概要については、次の動画をご覧ください。

起動の種類は、次の段階で明確にできます。

  • コールド スタートアップ: 保存済み状態のない新しいプロセスの作成から始めます。
  • ウォーム スタートアップ: プロセスを再利用しながらアクティビティを再作成するか、保存された状態でプロセスを再作成します。
  • ホット スタートアップ: アクティビティを再起動し、インフレーションから開始します。

デバイスのシステム トレース アプリを使用して Systrace をキャプチャすることをおすすめします。 Android 10 以降の場合は、Perfetto を使用します。Android 9 以前の場合は、Systrace を使用します。また、ウェブベースの Perfetto トレース ビューアでトレース ファイルを表示することをおすすめします。詳細については、システム トレースの概要をご覧ください。

確認すべき点は次のとおりです。

  • モニターの競合: モニターで保護されたリソースの競合により、アプリの起動が大幅に遅れることがあります。
  • 同期バインダー トランザクション: アプリのクリティカル パスで不要なトランザクションを探します。必要なトランザクションにコストがかかる場合は、関連するプラットフォーム チームと協力して改善することを検討してください。

  • 同時 GC: これは一般的で影響は比較的小さいですが、頻繁に発生する場合は、Android Studio の Memory Profiler で調べることを検討してください。

  • I/O: 起動時に実行された I/O をチェックし、長いストールを探します。

  • 他のスレッドでの大量のアクティビティ: UI スレッドと干渉する可能性があるため、起動時のバックグラウンド処理に注意します。

アプリの起動に関する指標のレポートを改善するために、アプリの観点からは、起動が完了したときに reportFullyDrawn を呼び出すことをおすすめします。reportFullyDrawn の使用の詳細については、完全表示までの時間セクションをご覧ください。Perfetto トレース プロセッサを使用して RFD で定義された開始時間を抽出でき、ユーザーに表示されるトレース イベントが生成されます。

デバイスでシステム トレースを使用する

「System Tracing」というシステムレベルのアプリを使用すると、デバイスのシステム トレースをキャプチャできます。このアプリでは、デバイスを adb に接続したり接続したりすることなく、デバイスからトレースを記録できます。

Android Studio Memory Profiler を使用する

Android Studio Memory Profiler を使用すると、メモリリークや不正な使用パターンに起因する可能性のあるメモリ プレッシャーを検査できます。オブジェクト割り当てのライブビューを提供します。

Memory Profiler を使用して GC が発生する理由と頻度を追跡することで、アプリのメモリの問題を解決できます。

アプリメモリをプロファイリングするには、次の手順を行います。

  1. メモリの問題を検出します。

    重視するユーザー ジャーニーのメモリ プロファイリング セッションを記録します。 図 7 のようにオブジェクト数の増加を確認します。これが最終的に GC につながります(図 8 を参照)。

    alt_text 図 7.オブジェクト数の増加。

    alt_text 図 8.ガベージ コレクション。

    メモリ プレッシャーを増大させているユーザー ジャーニーを特定したら、メモリ プレッシャーの根本原因を分析します。

  2. メモリ プレッシャーのホットスポットを診断する。

    図 9 に示すように、タイムラインで範囲を選択して、[Allocations] と [Shallow Size] の両方を可視化します。

    alt_text 図 9.[Allocations] と [Shallow Size] の値。

    このデータは、複数の項目で並べ替えを行えます。以下に、各ビューが問題の分析にどのように役立つかの例を示します。

    • Arrange by class: キャッシュに保存されているオブジェクトや、メモリプールから再利用されるオブジェクトを生成しているクラスを検索する場合に便利です。

      たとえば、「Vertex」というクラスのオブジェクトを毎秒 2,000 個作成するアプリの場合、Allocations 数は 1 秒ごとに 2,000 個ずつ増え、クラスで並べ替えると確認できます。これらのオブジェクトを再利用してガベージの生成を回避するには、メモリプールを実装します。

    • コールスタックごとに並べ替える: ループ内や、多くの割り当て作業を行う特定の関数内など、メモリが割り当てられているホットパスがある場所を確認する場合に便利です。

    • Shallow Size: オブジェクト自体のメモリのみをトラッキングします。主にプリミティブ値のみで構成された単純なクラスをトラッキングする場合に役立ちます。

    • Retained Size: オブジェクトと、そのオブジェクトによってのみ参照される参照による合計メモリが表示されます。複雑なオブジェクトによるメモリ プレッシャーを追跡するのに役立ちます。この値を取得するには、図 10 に示すようにフルメモリダンプを作成し、図 11 に示すように [Retained Size] を列として追加します。

      alt_text 図 10.フルメモリダンプ。

      [Retained Size] 列をご覧ください。
      図 11. [Retained Size] 列
  3. 最適化の効果を測定する。

    GC がより明確になり、メモリ最適化の影響を簡単に測定できるようになりました。最適化によってメモリ プレッシャーが軽減されると、GC は少なくなります。

    最適化の影響を測定するには、プロファイラのタイムラインで GC 間の時間を測定します。これにより、GC 間の処理に時間がかかっていることがわかります。

    メモリが改善された場合の最終的な影響は次のとおりです。

    • アプリが常にメモリ プレッシャーにならなければ、メモリ不足によるシャットダウンは減少する可能性があります。
    • GC を減らすと、特に P99 ではジャンク指標が改善されます。これは、GC が CPU の競合を引き起こし、その間のレンダリング処理を遅延させる可能性があるものだからです。