アプリ主導のプロファイリング

Android 15(API レベル 35)以降では、アプリ主導のプロファイリングにより、ProfilingManager API または AndroidX Core(androidx.core:core 1.15.0 以降)ビルダーを使用して、サポートされているプロファイル記録をコード内で明示的に開始および停止できます。プロファイルのタイミングやセッション パラメータ(期間、バッファ ポリシー、サンプリング レートなど)を制御する場合や、特定のユーザー ジャーニーやランタイム条件をターゲットにする場合は、直接プロファイリングを使用します。

依存関係を追加する

ProfilingManager API を最大限に活用するには、次の Jetpack ライブラリを build.gradle.kts ファイルに追加します。

Kotlin

   dependencies {
       implementation("androidx.tracing:tracing-ktx:2.0.5")
       implementation("androidx.core:core:1.19.1")
   }
   

Groovy

   dependencies {
       implementation 'androidx.tracing:tracing-ktx:2.0.5'
       implementation 'androidx.core:core:1.19.1'
   }
   

システム トレースを記録する

必要な依存関係を追加したら、次のコードを使用してシステム トレースを記録します。この例は、メインスレッド外で重いオペレーションを安全に管理しながら、プロファイリング セッションを開始する方法を示しています。

Kotlin

@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@Composable
fun ProfiledScreen(modifier: Modifier = Modifier) {
    // Use the application context: requestProfiling resolves the ProfilingManager
    // system service from it, so there's no reason to hand it a short-lived Activity.
    val appContext = LocalContext.current.applicationContext
    val scope = rememberCoroutineScope()

    Button(
        onClick = {
            // Run the orchestration off the main thread. Profiling a heavy operation
            // on the UI thread would freeze the UI (ANR) and distort the very metrics
            // you're trying to capture.
            //
            // Note: this scope is tied to composition. If the user leaves this screen
            // mid-session, the coroutine is cancelled and stopSignal.cancel() might not
            // run, but setDurationMs() acts as a safety net and ends the profile.
            scope.launch(Dispatchers.Default) {
                val callbackExecutor = Dispatchers.IO.asExecutor()
                val resultCallback = Consumer<ProfilingResult> { profilingResult ->
                    if (profilingResult.errorCode == ProfilingResult.ERROR_NONE) {
                        Log.d("ProfileTest", "Result file: ${profilingResult.resultFilePath}")
                    } else {
                        // errorMessage explains the failure (e.g., rate limiting); keep it.
                        Log.e(
                            "ProfileTest",
                            "Profiling failed errorCode=${profilingResult.errorCode} " +
                                "errorMessage=${profilingResult.errorMessage}"
                        )
                    }
                }

                val stopSignal = CancellationSignal()
                val requestBuilder = SystemTraceRequestBuilder().apply {
                    setCancellationSignal(stopSignal)
                    setTag("FOO") // Caller-supplied tag for identification.
                    setDurationMs(60000) // Hard cap: ends the session if cancel() never fires.
                    setBufferFillPolicy(BufferFillPolicy.RING_BUFFER)
                    setBufferSizeKb(32768)
                }

                // 1. Start the session. This is asynchronous system IPC. The tracing
                //    engine takes a moment to start and allocate buffers.
                requestProfiling(appContext, requestBuilder.build(), callbackExecutor, resultCallback)

                // 2. The API exposes no "profiling started" signal, so pad with a short,
                //    best-effort delay before running the code you care about. This is
                //    approximate. Increase it on slower or heavily loaded devices.
                delay(STARTUP_PADDING_MS)

                // 3. The session is already recording every thread in your app. This slice
                //    doesn't scope what's captured. It just labels this region of the
                //    timeline so heavyOperation() is easier to find. trace { } closes the
                //    section even if the block throws.

                trace("MyApp:HeavyOperation") {
                    heavyOperation()
                }

                // 4. Stop recording. Until this fires or the setDurationMs() cap is
                //    reached (whichever comes first), the session keeps capturing app-wide
                //    activity.

                stopSignal.cancel()
            }
        }
    ) {
        Text("Run & Profile Heavy Operation")
    }
}

// Best-effort wait for the system trace engine to initialize before profiling.
// There is no deterministic start callback; tune this for your target devices.
private const val STARTUP_PADDING_MS = 100L

fun heavyOperation() {
    // Background computations to profile.
}

Java

void sampleRecordSystemTrace(Context context) {
  // Use the application context: requestProfiling resolves the ProfilingManager
  // system service from it, so there's no reason to hand it a short-lived Activity.
  Context appContext = context.getApplicationContext();

  // Run the orchestration off the main thread. Profiling a heavy operation
  // on the UI thread would freeze the UI (ANR) and distort the very metrics
  // you're trying to capture.
  profilingExecutor.execute(() -> {
    Executor callbackExecutor = Executors.newSingleThreadExecutor();
    Consumer<ProfilingResult> resultCallback = profilingResult -> {
      if (profilingResult.getErrorCode() == ProfilingResult.ERROR_NONE) {
        Log.d("ProfileTest", "Result file: " + profilingResult.getResultFilePath());
      } else {
        // errorMessage explains the failure (e.g., rate limiting); keep it.
        Log.e(
            "ProfileTest",
            "Profiling failed errorCode=" + profilingResult.getErrorCode()
                + " errorMessage=" + profilingResult.getErrorMessage());
      }
    };

    CancellationSignal stopSignal = new CancellationSignal();
    SystemTraceRequestBuilder requestBuilder = new SystemTraceRequestBuilder();
    requestBuilder.setCancellationSignal(stopSignal);
    requestBuilder.setTag("FOO"); // Caller-supplied tag for identification.
    requestBuilder.setDurationMs(60000); // Hard cap: ends the session if cancel() never fires.
    requestBuilder.setBufferFillPolicy(BufferFillPolicy.RING_BUFFER);
    requestBuilder.setBufferSizeKb(32768);

    // 1. Start the session. This is asynchronous system IPC. The tracing
    //    engine takes a moment to start and allocate buffers.
    Profiling.requestProfiling(
        appContext, requestBuilder.build(), callbackExecutor, resultCallback);

    // 2. The API exposes no "profiling started" signal, so pad with a short,
    //    best-effort delay before running the code you care about. This is
    //    approximate. Increase it on slower or heavily loaded devices.
    try {
      Thread.sleep(STARTUP_PADDING_MS);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      stopSignal.cancel();
      return;
    }

    // 3. The session is already recording every thread in your app. This slice
    //    doesn't scope what's captured. It just labels this region of the
    //    timeline so heavyOperation() is easier to find. try/finally closes the
    //    section even if heavyOperation() throws.
    Trace.beginSection("MyApp:HeavyOperation");
    try {
      heavyOperation();
    } finally {
      Trace.endSection();
    }

    // 4. Stop recording. Until this fires or the setDurationMs() cap is
    //    reached (whichever comes first), the session keeps capturing app-wide
    //    activity.
    stopSignal.cancel();
  });
}

// Best-effort wait for the system trace engine to initialize before profiling.
// There is no deterministic start callback; tune this for your target devices.
private static final long STARTUP_PADDING_MS = 100L;

// Single background thread that runs the profiling orchestration.
private final ExecutorService profilingExecutor = Executors.newSingleThreadExecutor();

void heavyOperation() {
  // Background computations to profile.
}

サンプルコードは、次の手順でプロファイリング セッションを設定して管理します。

  1. エグゼキュータを設定する: メインスレッドからプロファイリング結果を受け取る、UI 以外の Executor を作成します。
  2. プロファイリング結果を処理する: Consumer<ProfilingResult> コールバックを作成して、ProfilingManager から完了した結果またはエラーを受け取ります。
  3. プロファイリング リクエストを作成する: SystemTraceRequestBuilder を作成してセッション設定をカスタマイズします(カスタマイズを省略してシステムのデフォルトを使用することもできます)。

    • setTag(): ProfilingResult.getTag によって返され、出力ファイル名に追加される、呼び出し元が指定したタグを添付します(最初の 20 文字の英数字とハイフンまで)。
    • setDurationMs(): 最大録画時間をミリ秒単位で設定します(例: 60 秒の場合は 60000)。
    • setBufferFillPolicy(): バッファが容量制限を処理する方法を定義します。BufferFillPolicy.RING_BUFFER は、バッファがいっぱいになると最も古いデータを上書きします。BufferFillPolicy.DISCARD は、バッファがいっぱいになると新しいデータの記録を停止します。
    • setBufferSizeKb(): 録画バッファサイズをキロバイト単位で設定します。
  4. 省略可: 録画を早期に停止する: CancellationSignal(stopSignal)を渡して、ターゲット オペレーションが終了するとすぐにセッションを停止し、それまでに録画されたデータを保存して、ProfilingResult をコールバックに配信します。詳しくは、アクティブな録画を停止するをご覧ください。

  5. 開始して結果を受け取る: requestProfiling() を呼び出して、バックグラウンドで録音を開始します。セッションが終了すると、resultCallback.accept() は ProfilingResult を受け取ります。

    • 成功すると(ERROR_NONE)、getResultFilePath() を呼び出してデバイス上のパスを取得します。adb でファイルを pull する、または WorkManager でファイルをアップロードする手順については、プロファイルの取得とアップロードをご覧ください。
    • 失敗した場合は、getErrorCode() と getErrorMessage() を確認します。リクエストは、レート制限やその他の理由で失敗することがあります。
    • 結果が配信される前にアプリが終了した場合、結果はキューに格納され、次回アプリが registerForAllProfilingResults でグローバル リスナーを登録したときに配信されます。
  6. カスタム トレース ポイントを追加する: オペレーションを trace("...") { ... }(または Trace.beginSection() / Trace.endSection())でラップして、タイムラインの領域にラベルを付けます。

アクティブな録画を停止する

setDurationMs() の期限が切れるのを待たずに、インタラクションが完了したらすぐにキャプチャを停止するには、CancellationSignal を setCancellationSignal() または requestProfiling() に渡します。CancellationSignal と setDurationMs() の両方を指定すると、どちらか早い方でセッションが終了します。どちらも指定しない場合、デフォルトの期間が適用されます。

cancel() を呼び出しても、録音は破棄されません。システムはキャプチャを停止し、その時点までに収集されたデータを秘匿化して保存し、ERROR_NONE を含む ProfilingResult をリスナーに配信します。

コールスタック サンプリングを使用してシステム トレースを記録する

Android 17(API レベル 37)以降(または更新された Profiling メインライン モジュールを搭載した Android 16)を搭載したデバイスでは、ProfilingManager は、サンプリングされたコールスタックをシステム トレースとともに 1 つのセッションで収集することをサポートしています。これにより、スレッドの状態、フレーム、カスタム トレース スライスなどのタイムライン イベントを、明示的なトレース インストルメンテーションのないメソッドの CPU コールスタックとともに表示できます。

この例では、サポートされているプロファイル タイプのいずれか(PROFILING_TYPE_SYSTEM_TRACE)を指定し、構成 Bundle を渡すことで、Jetpack ビルダーを使用せずに ProfilingManager.requestProfiling を直接呼び出す方法も示しています。

Kotlin

val profilingManager = context.getSystemService(ProfilingManager::class.java)

// Pass a Bundle to customize the capture, or pass null for default settings.
val config = Bundle().apply {
    putInt("KEY_DURATION_MS", 15000) // 15 seconds
    putInt("KEY_SIZE_KB", 20480) // 20 MB buffer size

    // Collect sampled CPU call stacks alongside the system trace.
    putBoolean("KEY_COLLECT_STACK_SAMPLING", true)
    putInt("KEY_FREQUENCY_HZ", 100) // Sampling frequency in Hz
    // Optional: Set KEY_SAMPLE_BINDER_ONLY to true to sample only binder calls.
}
profilingManager?.requestProfiling(
    ProfilingManager.PROFILING_TYPE_SYSTEM_TRACE,
    config,
    "trace-with-callstacks",
    null,
    callbackExecutor,
    resultCallback
)

Java

ProfilingManager profilingManager = context.getSystemService(ProfilingManager.class);
if (profilingManager == null) return;

// Pass a Bundle to customize the capture, or pass null for default settings.
Bundle config = new Bundle();
config.putInt("KEY_DURATION_MS", 15000); // 15 seconds
config.putInt("KEY_SIZE_KB", 20480); // 20 MB buffer size

// Collect sampled CPU call stacks alongside the system trace.
config.putBoolean("KEY_COLLECT_STACK_SAMPLING", true);
config.putInt("KEY_FREQUENCY_HZ", 100); // Sampling frequency in Hz
// Optional: Set KEY_SAMPLE_BINDER_ONLY to true to sample only binder calls.

profilingManager.requestProfiling(
    ProfilingManager.PROFILING_TYPE_SYSTEM_TRACE,
    config,
    "trace-with-callstacks",
    null,
    callbackExecutor,
    resultCallback
);

結果のプロファイルを Perfetto UI で開くと、サンプリングされたコールスタックが、アプリの通常のトレース スライスとともに、専用の Callstacks トラックとして表示されます。コールスタック トラックのリージョンを選択すると、図 1 に示すように、その期間にサンプリングされたコールスタックのフレームグラフが表示されます。

Perfetto UI に、サンプリングされたコールスタック トラック、RenderThread のトレース スライス、選択した領域のコールスタック フレームグラフを含むシステム トレースが表示されています。
図 1. Perfetto UI でのコールスタック サンプリングと組み合わせたシステム トレース。

Java ヒープダンプをキャプチャする

JavaHeapDumpRequestBuilder(または PROFILING_TYPE_JAVA_HEAP_DUMP)を使用して、アプリの Java メモリのスナップショットをリクエストできます。Debug.dumpHprofData でキャプチャされた標準の .hprof ヒープダンプとは異なり、ProfilingManager ヒープダンプは、本番環境で安全かつ低オーバーヘッドで収集できるように設計されています。

  • フリーズしない収集: システムはアプリのプロセスのコピーオンライト クローンをフォークし、クローンされたプロセスからヒープ スナップショットを収集します。そのため、ダンプがディスクに書き込まれている間もアプリはフリーズしません。
  • 参照グラフのみ(未加工のオブジェクト データなし): ProfilingManager は、.hprof ファイルのように未加工のメモリの内容をダンプするのではなく、クラス、オブジェクト サイズ、参照チェーン、ガベージ コレクション(GC)ルートを含む .perfetto-java-heap-dump protobuf グラフを記録します。プリミティブ値、文字列の内容、配列ペイロードは省略されます。これにより、ユーザーのプライバシーが保護され、ファイルサイズが小さくなります。詳細な内訳については、ProfilingManager のヒープダンプと HPROF ダンプの違いをご覧ください。

システム トレースを記録するに示されているのと同じ Context、Executor、resultCallback の設定を使用して、JavaHeapDumpRequestBuilder でリクエストを作成します。

Kotlin

val heapDumpRequest = JavaHeapDumpRequestBuilder()
    .setTag("memory-leak-check")
    .build()

requestProfiling(
    appContext,
    heapDumpRequest,
    callbackExecutor,
    resultCallback
)

Java

ProfilingRequest heapDumpRequest = new JavaHeapDumpRequestBuilder()
    .setTag("memory-leak-check")
    .build();

Profiling.requestProfiling(
    getApplicationContext(),
    heapDumpRequest,
    callbackExecutor,
    resultCallback
);

Java ヒープダンプを分析してメモリリークを診断するエンドツーエンドの完全なチュートリアルについては、ケーススタディ: OOM クラッシュをデバッグするをご覧ください。

次のステップ

プロファイルをキャプチャしたら、adb を使用してローカルで取得するか、WorkManager を使用してバックエンドにアップロードします。