トリガーベースのプロファイリング

Android 16(API レベル 36)以降では、ProfilingManager を使用すると、アプリケーション応答なし(ANR)エラー、メモリ不足(OutOfMemoryError)クラッシュ、アプリ起動イベントなどのシステム トリガーに応じて、プロファイルを自動的にキャプチャできます。これらのイベントの多くがいつ発生するかを予測することはできないため、事前に手動で録画を開始することは現実的ではありません。トリガーベースのプロファイリングでは、システムがキャプチャを管理し、結果のプロファイルをアプリに配信します。これにより、次のことが可能になります。

  • 安定性の問題をデバッグする: 予測できない ANR やメモリリークを診断します。
  • クリティカル ユーザー ジャーニー(CUJ)を最適化する: アプリの起動(TRIGGER_TYPE_COLD_START、TRIGGER_TYPE_APP_FULLY_DRAWN)などのフローを分析します。
  • ユーザーの行動を把握する: ユーザーが開始したアプリの終了などのイベントを検査します。

使用可能なトリガーの一覧については、ProfilingTrigger をご覧ください。

トリガーの種類

トリガーは、イベント発生時にシステムがプロファイルをキャプチャする方法が異なります。

バックグラウンド トレースを使用するトリガー

ANR などの多くのトリガーは、以前の問題の結果として発生します。システムが ANR を検出した後にのみトレースを開始すると、ANR の原因となったブロッキング処理を見逃してしまいます。イベントに至るまでのアクティビティをキャプチャするため、次のトリガーによって、システムの有効なバックグラウンド トレースのスナップショットが保存されます。イベントの発生時にバックグラウンド トレースが実行されていない場合、プロファイルは収集されません。

新しいキャプチャを開始するトリガー

次のトリガーは、バックグラウンド トレースに依存しません。代わりに、イベントが発生すると、システムは新しいプロファイルのキャプチャを開始します。

異常によってキャプチャ動作が異なるトリガー

次の異常検出トリガーの場合、キャプチャされたプロファイル タイプ(アクティブなバックグラウンド トレースのスナップショットの保存、または新しいキャプチャの開始)とレート制限の動作は、システムによって検出された特定の異常によって異なります。

トリガーを設定する

次のコードは、TRIGGER_TYPE_APP_FULLY_DRAWN トリガーに登録し、クールダウン期間を設定する方法を示しています。

Kotlin

@RequiresApi(Build.VERSION_CODES.BAKLAVA)
fun recordWithTrigger() {
    val profilingManager =
        applicationContext.getSystemService(ProfilingManager::class.java) ?: return

    val triggers = ArrayList<ProfilingTrigger>()

    val triggerBuilder = ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_APP_FULLY_DRAWN)
        .setRateLimitingPeriodHours(1)

    triggers.add(triggerBuilder.build())

    val callbackExecutor: Executor = Executors.newSingleThreadExecutor()

    val resultCallback = Consumer<ProfilingResult> { profilingResult ->
        if (profilingResult.errorCode == ProfilingResult.ERROR_NONE) {
            Log.d(
                "ProfileTest",
                "Received profiling result file=" + profilingResult.resultFilePath
            )
            setupProfileUploadWorker(profilingResult.resultFilePath)
        } else {
            Log.e(
                "ProfileTest",
                "Profiling failed errorcode=" + profilingResult.errorCode + " errormsg=" + profilingResult.errorMessage
            )
        }
    }

    profilingManager.registerForAllProfilingResults(callbackExecutor, resultCallback)
    profilingManager.addProfilingTriggers(triggers)

    Choreographer.getInstance().postFrameCallback { frameTimeNanos ->
        // This will cause the TRIGGER_TYPE_APP_FULLY_DRAWN to be emitted.
        reportFullyDrawn()
    }
}

Java

public void recordWithTrigger() {
  ProfilingManager profilingManager = getApplicationContext().getSystemService(
      ProfilingManager.class);
  List<ProfilingTrigger> triggers = new ArrayList<>();
  ProfilingTrigger.Builder triggerBuilder = new ProfilingTrigger.Builder(
      ProfilingTrigger.TRIGGER_TYPE_APP_FULLY_DRAWN);
  triggerBuilder.setRateLimitingPeriodHours(1);
  triggers.add(triggerBuilder.build());

  Executor callbackExecutor = Executors.newSingleThreadExecutor();
  Consumer<ProfilingResult> resultCallback =
      new Consumer<ProfilingResult>() {
        @Override
        public void accept(ProfilingResult profilingResult) {
          if (profilingResult.getErrorCode() == ProfilingResult.ERROR_NONE) {
            Log.d(
                "ProfileTest",
                "Received profiling result file=" + profilingResult.getResultFilePath());
            setupProfileUploadWorker(profilingResult.getResultFilePath());
          } else {
            Log.e(
                "ProfileTest",
                "Profiling failed errorcode="
                    + profilingResult.getErrorCode()
                    + " errormsg="
                    + profilingResult.getErrorMessage());
          }
        }
      };
  profilingManager.registerForAllProfilingResults(callbackExecutor, resultCallback);
  profilingManager.addProfilingTriggers(triggers);

  Choreographer.getInstance().postFrameCallback((f) -> {
    // This will cause the TRIGGER_TYPE_APP_FULLY_DRAWN to be emitted.
    reportFullyDrawn();
  });
}

このコードは次の手順を実行します。

  1. マネージャーを取得する: ProfilingManager サービスを取得します。
  2. トリガーを定義する: TRIGGER_TYPE_APP_FULLY_DRAWN の ProfilingTrigger をビルドします。このイベントは、アクティビティによって開始されたコールド スタート中にアプリが reportFullyDrawn() を呼び出し、起動が完了してインタラクティブになったことを報告したときに発生します。
  3. トリガーのクールダウンを設定する: システムのレートリミッターに加えて setRateLimitingPeriodHours を使用して、この特定のトリガーに 1 時間のクールダウン期間を適用します。このクールダウンにより、アプリが 1 時間に複数の起動プロファイルを記録することを防ぎます。
  4. リスナーを登録する: registerForAllProfilingResults を呼び出して、結果を処理するコールバックを定義します。このコールバックは、getResultFilePath() を介して保存されたプロファイルのパスを受け取ります。
  5. トリガーを追加する: addProfilingTriggers を使用して、トリガーリストを ProfilingManager に登録します。
  6. Fire event: Choreographer フレーム コールバックをポストします。このコールバックは、次のフレームが描画されると reportFullyDrawn() を呼び出し、TRIGGER_TYPE_APP_FULLY_DRAWN イベントをシステムに送信します。これにより、システムのバックグラウンド トレースが実行中で、レート制限の割り当てが利用可能であると想定して、プロファイルの収集がトリガーされます。このトリガーが発生するように、アプリで reportFullyDrawn() を呼び出してください。

トリガーのライフサイクル

登録されたトリガーは、アプリの再起動やシステムの再起動後も維持されます。アプリの起動時やシステムの再起動後にトリガーを再登録する必要はありません。addProfilingTriggers を使用してトリガーを登録すると、トリガーを削除するかアプリをアンインストールするまで、システムはアプリのトリガーを自動的に維持します。

トリガーの登録を解除するタイミング

登録されたトリガーはアプリの再起動やシステムの再起動後も保持されるため、以降の起動時に addProfilingTriggers をスキップしても、前のセッションで登録されたトリガーは停止しません。特定のイベントまたはすべてのイベントのプロファイルのキャプチャを停止するには、removeProfilingTriggersByType または clearProfilingTriggers を明示的に呼び出します。

次のような状況では、トリガーの登録を解除します。

  • コホートのターゲティングまたはリモートフラグのロールバック: リモート構成または A/B テストフラグを使用して、コホート間でプロファイリングをセグメント化することがあります。デバイスがサンプリングされたコホートから離脱するか、フラグがオフになった場合は、トリガーの登録を解除します。
  • デバッグ キャンペーンの終了: 問題の診断や修正のリリースに十分なプロファイルを収集したら、トリガーを削除します。不要なトリガーの登録を解除すると、デバイスのストレージと、他のプロファイリング リクエストに対するアプリのレートリミッターの割り当てが節約されます。

コールバックのライフサイクル

直接プロファイリング リクエストとは異なり、トリガーベースのプロファイルは、registerForAllProfilingResults に登録されているグローバル リスナーにのみ結果を配信します。コールバックで ProfilingResult.getTriggerType を呼び出して、キャプチャを開始したトリガーを特定し、ProfilingResult.getResultFilePath を呼び出して、保存されたプロファイル ファイルを見つけます。トリガー プロファイルのファイル名、adb を使用したファイルの pull、バックグラウンドでの結果のアップロードの詳細については、プロファイルの取得とアップロードをご覧ください。

トリガーはアプリの再起動やシステムの再起動後も維持されますが、結果コールバックは維持されません。アプリの起動が完了した後(コールド スタートを遅らせないため)、バックグラウンドの Executor にコールバックを登録して、次の両方のシナリオでプロファイルを受信します。

  • アプリの実行中: アプリを停止またはクラッシュさせないトリガーの場合、トリガーが発生してプロファイルが保存されると、システムは指定された Executor でコールバックを呼び出します。
  • アプリの終了後: アプリの終了を伴う、またはアプリの終了に先行することが多いトリガー(TRIGGER_TYPE_ANR、TRIGGER_TYPE_OOM、TRIGGER_TYPE_KILL_RECENTS など)の場合、またはキャプチャが完了する前にアプリが停止した場合、システムはバックグラウンドでプロファイルを保存し、アプリの再起動やシステムの再起動をまたいで結果をキューに入れます。システムは、未配信のキューに格納された結果を最大 7 日間保持し、次回アプリが実行されて registerForAllProfilingResults を呼び出したときに配信します。

バックグラウンド トレースの仕組み

プロファイリングのオーバーヘッドと有用なアーティファクトのキャプチャのバランスを取るため、システムはバックグラウンド トレースを継続的に実行しません。代わりに、ランダムな間隔でバックグラウンド トレースを開始して、定期的にサンプリングします。

システムによってトリガーされるバックグラウンド トレースでは、システム定義の最大サイズのリングバッファが使用されます。バッファがいっぱいになると、新しいトレースデータが最も古いデータを上書きします。図 1 に示すように、バックグラウンド トレースがアクティブで、レート制限の割り当てが利用可能なときに、登録されたトリガーが起動すると、システムは、トリガーに至るまでの最新のアクティビティを含むリングバッファ スナップショットを保存します。

バックグラウンド トレース スナップショットの仕組みを示す図。リングバッファがトリガー イベントの前にデータをキャプチャします。
図 1. バックグラウンド トレース スナップショットの仕組み。

トリガー固有のクールダウンを設定する

高頻度のトリガーは、アプリのレート制限の割り当てをすぐに消費する可能性があります。1 つのトリガータイプが割り当てを使い切らないようにするには、setRateLimitingPeriodHours を使用して、システム レートリミッターに加えてトリガーごとの最小クールダウン期間を追加します。

トリガーをローカルでデバッグする

本番環境ではバックグラウンド トレースがランダムな間隔で実行されるため、ローカルでトリガーをテストするには、継続的なバックグラウンド トレースを強制的に実行し、パッケージのシステムレベルのレート制限をバイパスする必要があります(setRateLimitingPeriodHours で設定されたトリガーごとのクールダウンは引き続き適用され、rate_limiter.disabled はアプリドリブンの requestProfiling 呼び出しにのみ適用されます)。

adb コマンドを使用して、バックグラウンド トレースを強制的に実行し、コマンドラインからトリガーをシミュレートして、サービスの状態を検査するには、ローカル プロファイリング用のデバッグ コマンドのトリガー テスト用にバックグラウンド トレースを強制有効化するをご覧ください。