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

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

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

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

  • スクロール ジャンク
    • 「ジャンク」という用語は、システムが要求された頻度(60 Hz 以上)で画面に描画するフレームを時間内に作成、提供できない場合に発生する視覚的な中断を表すために使用します。ジャンクはスクロール時に最も顕著に現れ、スムーズにアニメーション化されるべきフローで中断が発生します(アプリがコンテンツをレンダリングするためにかかる時間がシステム上のフレーム継続期間よりも長いため、1 つ以上のフレームの途中で動きが一時停止します)。
    • アプリは 90 Hz のリフレッシュ レートをターゲットにする必要があります。従来のレンダリング レートは 60 Hz でしたが、新しいデバイスの多くはスクロールなどのユーザー操作中に 90 Hz モードで動作します。一部のデバイスでは、さらに高いレート(120 Hz まで)がサポートされています。
      • ある時点でデバイスが使用しているリフレッシュ レートを確認するには、[開発者向けオプション] > [リフレッシュ レートの表示]([デバッグ] セクション内)を使用してオーバーレイを有効にします。
  • 起動レイテンシ

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

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

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

      • P95/P99 レイテンシが中央レイテンシに非常に近くなるようにする。アプリの起動に時間が非常に長くかかることがあると、ユーザーの信頼が損なわれます。アプリ起動のクリティカル パスに IPC や不要な I/O があると、ロック競合が発生し、こうした不整合が生じることがあります。

  • 滑らかでない遷移

    • こうした懸念は、タブの切り替えや新しいアクティビティの読み込みなどの操作中に発生します。この種の遷移には滑らかなアニメーションを使用し、遅延やちらつきがないようにしてください。
  • 電力の非効率性

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

      ただし、まだ労力は必要なため、内部ループに多数のオブジェクトを割り当てているかどうかに注意する必要があります(パフォーマンスの問題につながる可能性があります)。

問題の特定

パフォーマンスの問題を特定して修正するためのおすすめのワークフローは次のとおりです。

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

こうしたパフォーマンスの問題を理解してデバッグするには、個々のテスト実行を手動でデバッグすることが重要です。前述の手順は、集計データを分析することで置き換えることはできません。ただし、ユーザーが実際に何を見ているかを理解し、いつ回帰が発生するかを特定するためには、フィールドだけでなく自動テストで指標の収集を設定することも重要です。

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

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

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

トレースポイント

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

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

APK に関する考慮事項

注意: デバッグビルドではパフォーマンスを測定しないでください。

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

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

コンパイル

デバイス上のアプリを既知の状態(通常は速度または速度プロファイル)にコンパイルします。バックグラウンド 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 権限のあるデバイスでは、マイクロ ベンチマークに lockClocks スクリプトを使用することを検討してください。特に、これらのスクリプトは次の処理を行います。

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

これは、ユーザー エクスペリエンスに重点を置いたテスト(アプリの起動、DoU テスト、ジャンクテストなど)には推奨されませんが、マイクロ ベンチマーク テストでノイズを減らすには不可欠です。

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

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

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

alt_text

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

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

systrace でガベージ コレクション(GC)が予想以上に頻繁に発生することもあります。

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

alt_text

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

ジャンクのあるフレーム

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

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

alt_text

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

alt_text

この一定の周期に乱れが生じると、ジャンクのあるフレームになります。

alt_text

少し練習するだけで簡単に見つけられます。

alt_text

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

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

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

  • RecyclerView のバッキング データ全体を不必要に無効化する。こうするとフレームのレンダリング時間が長くなり、ジャンクが発生する可能性があります。代わりに、変更されたデータのみを無効にして、更新が必要なビューの数を最小限に抑えます。
    • コストのかかる notifyDatasetChanged() 呼び出し(コンテンツが完全に置き換えられるのではなく更新される)を避ける方法については、動的データの提示をご覧ください。
  • ネストされた RecyclerView を適切にサポートできず、内部 RecyclerView が毎回完全に再作成される。
    • ネストされた内側のすべての RecyclerView には、内側の RecyclerView 間でビューをリサイクルできるよう、RecycledViewPool を設定する必要があります。
  • 十分なデータをプリフェッチしていないか、適切なタイミングでプリフェッチしていない。スクロール リストの一番下をすばやくクリックして、サーバーからのデータが増えるまで待つ必要があり、不快です。フレームのデッドラインには間に合うため、これは厳密には「ジャンク」ではありませんが、ユーザーがデータを持つ必要がないようにプリフェッチのタイミングと量を変更すると、ユーザー エクスペリエンスが大幅に改善する可能性があります。