Profilazione basata sulle app

Su Android 15 (livello API 35) e versioni successive, la profilazione basata sull'app consente alla tua app di avviare e interrompere esplicitamente le registrazioni dei profili supportate nel codice utilizzando l'API ProfilingManager o i builder AndroidX Core (androidx.core:core 1.15.0 o versioni successive). Utilizza la profilazione diretta quando vuoi controllare i tempi di profilazione o i parametri della sessione, come durate, policy del buffer e frequenze di campionamento, o quando vuoi scegliere come target un percorso dell'utente o una condizione di runtime specifici.

Aggiungi dipendenze

Per un'esperienza ottimale con l'API ProfilingManager, aggiungi le seguenti librerie Jetpack al file build.gradle.kts.

Kotlin

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

Alla moda

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

Registra una traccia di sistema

Dopo aver aggiunto le dipendenze richieste, utilizza il seguente codice per registrare un tracciamento del sistema. Questo esempio mostra come avviare una sessione di profilazione gestendo in sicurezza le operazioni pesanti al di fuori del thread principale.

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.
}

Il codice campione configura e gestisce la sessione di profilazione nei seguenti passaggi:

  1. Configura l'esecutore: crea un Executor non UI per ricevere i risultati della profilazione al di fuori del thread principale.
  2. Gestisci i risultati della profilazione: crea un callback Consumer<ProfilingResult> per ricevere il risultato completato o l'errore da ProfilingManager.
  3. Crea la richiesta di profilazione: crea un SystemTraceRequestBuilder per personalizzare le impostazioni della sessione (o ometti le personalizzazioni per utilizzare le impostazioni predefinite del sistema):

    • setTag(): associa un tag fornito dal chiamante che viene restituito da ProfilingResult.getTag e aggiunto al nome file di output (fino ai primi 20 caratteri alfanumerici minuscoli e trattini).
    • setDurationMs(): imposta la durata massima della registrazione in millisecondi (ad esempio, 60000 per 60 secondi).
    • setBufferFillPolicy(): definisce il modo in cui il buffer gestisce i limiti di capacità: BufferFillPolicy.RING_BUFFER sovrascrive i dati meno recenti quando è pieno, mentre BufferFillPolicy.DISCARD interrompe la registrazione di nuovi dati quando il buffer è pieno.
    • setBufferSizeKb(): imposta la dimensione del buffer di registrazione in kilobyte.
  4. (Facoltativo) Interrompi la registrazione in anticipo: passa un CancellationSignal (stopSignal) per interrompere la sessione non appena l'operazione target termina, salvando i dati registrati fino a quel momento e inviando il ProfilingResult al tuo callback. Per maggiori dettagli, consulta Interrompi una registrazione attiva.

  5. Avvia e ricevi i risultati: chiama requestProfiling() per avviare la registrazione in background. Al termine della sessione, resultCallback.accept() riceve un ProfilingResult:

    • In caso di esito positivo (ERROR_NONE), chiama getResultFilePath() per ottenere il percorso sul dispositivo. Per istruzioni su come estrarre i file con adb o caricarli con WorkManager, consulta Recupera e carica i profili.
    • In caso di errore, esamina getErrorCode() e getErrorMessage(). Le richieste possono non riuscire a causa della limitazione di frequenza e di diversi altri motivi.
    • Se l'app viene terminata prima che il risultato venga inviato, il sistema lo mette in coda e lo invia la prossima volta che l'app registra un listener globale con registerForAllProfilingResults.
  6. Aggiungi punti di tracciamento personalizzati: racchiudi le operazioni in trace("...") { ... } (o Trace.beginSection()/Trace.endSection()) per etichettare le regioni della cronologia.

Interrompere una registrazione attiva

Per interrompere un'acquisizione non appena un'interazione viene completata, anziché attendere la scadenza di setDurationMs(), passa un CancellationSignal a setCancellationSignal() o requestProfiling(). Se specifichi sia CancellationSignal che setDurationMs(), la sessione termina con l'evento che si verifica per primo; se non specifichi nessuno dei due, il sistema applica una durata predefinita.

La chiamata al numero cancel() non elimina la registrazione. Il sistema interrompe l'acquisizione, oscura e salva i dati raccolti fino a quel momento e invia ProfilingResult al listener con ERROR_NONE.

Registra il tracciamento del sistema con il campionamento dello stack di chiamate

Sui dispositivi con Android 17 (livello API 37) e versioni successive (o Android 16 con un modulo principale Profiling aggiornato), ProfilingManager supporta la raccolta di stack di chiamate campionati insieme a un tracciamento del sistema in un'unica sessione, in modo da poter visualizzare gli eventi della cronologia, come stati dei thread, frame e sezioni di traccia personalizzate, insieme agli stack di chiamate della CPU per i metodi senza strumentazione di traccia esplicita.

Questo esempio mostra anche come chiamare ProfilingManager.requestProfiling direttamente senza i builder Jetpack specificando uno dei tipi di profilo supportati (PROFILING_TYPE_SYSTEM_TRACE) e passando una configurazione Bundle:

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
);

Quando apri il profilo risultante nell'interfaccia utente di Perfetto, gli stack di chiamate campionati vengono visualizzati come tracce Stack di chiamate dedicate accanto alle normali sezioni di traccia dell'app. Seleziona una regione di una traccia dello stack di chiamate per visualizzare un grafico a fiamme degli stack di chiamate campionati per quell'intervallo di tempo, come mostrato nella Figura 1.

UI di Perfetto che mostra un tracciamento del sistema con tracce dello stack di chiamate campionate, sezioni di traccia su RenderThread e un grafico a fiamme dello stack di chiamate per l'area selezionata.
Figura 1. Un tracciamento del sistema combinato con il campionamento dello stack di chiamate nella UI di Perfetto.

Acquisisci un dump dell'heap Java

Puoi richiedere uno snapshot della memoria Java della tua app utilizzando JavaHeapDumpRequestBuilder (o PROFILING_TYPE_JAVA_HEAP_DUMP). A differenza dei dump heap .hprof standard acquisiti con Debug.dumpHprofData, i dump heap ProfilingManager sono progettati per una raccolta sicura e con overhead ridotto in produzione:

  • Raccolta senza blocco: il sistema esegue il fork di un clone copy-on-write del processo della tua app e raccoglie l'istantanea heap dal processo clonato, in modo che l'app non si blocchi mentre il dump viene scritto sul disco.
  • Solo grafico di riferimento (nessun dato di oggetti non elaborati): anziché eseguire il dump dei contenuti della memoria non elaborata come un file .hprof, ProfilingManager registra un grafico protobuf .perfetto-java-heap-dump contenente classi, dimensioni degli oggetti, catene di riferimento e radici di garbage collection (GC), omettendo i valori primitivi, i contenuti delle stringhe e i payload degli array. In questo modo, la privacy degli utenti viene protetta e le dimensioni dei file vengono ridotte. Per un'analisi completa, consulta Differenze tra i dump dell'heap di ProfilingManager e i dump HPROF.

Utilizzando la stessa configurazione di Context, Executor e resultCallback mostrata in Registra un tracciamento del sistema, crea una richiesta con 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
);

Per una procedura dettagliata end-to-end completa sull'analisi di un dump dell'heap Java per diagnosticare una perdita di memoria, consulta Case study: esegui il debug di un arresto anomalo OOM.

Passaggi successivi

Dopo aver acquisito i profili, recuperali localmente con adb o caricali sul tuo backend con WorkManager.