Profilage basé sur les applications

Sur Android 15 (niveau d'API 35) ou version ultérieure, le profilage basé sur l'application permet à votre application de démarrer et d'arrêter explicitement les enregistrements de profils compatibles dans le code à l'aide de l'API ProfilingManager ou des compilateurs AndroidX Core (androidx.core:core 1.15.0 ou version ultérieure). Utilisez le profilage direct lorsque vous souhaitez contrôler le moment du profilage ou les paramètres de session (durées, règles de mise en mémoire tampon et taux d'échantillonnage, par exemple), ou cibler un parcours utilisateur ou une condition d'exécution spécifiques.

Ajouter des dépendances

Pour une expérience optimale avec l'API ProfilingManager, ajoutez les bibliothèques Jetpack suivantes à votre fichier 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'
   }
   

Enregistrer une trace système

Après avoir ajouté les dépendances requises, utilisez le code suivant pour enregistrer une trace système. Cet exemple montre comment démarrer une session de profilage tout en gérant de manière sécurisée les opérations lourdes en dehors du thread principal.

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

L'exemple de code configure et gère la session de profilage en procédant comme suit :

  1. Configurer l'exécuteur : créez un Executor non-UI pour recevoir les résultats du profilage en dehors du thread principal.
  2. Gérer les résultats du profilage : créez un rappel Consumer<ProfilingResult> pour recevoir le résultat final ou l'erreur de ProfilingManager.
  3. Créer la requête de profilage : créez un SystemTraceRequestBuilder pour personnaliser les paramètres de session (ou omettez les personnalisations pour utiliser les valeurs par défaut du système) :

    • setTag() : associe un tag fourni par l'appelant, qui est renvoyé par ProfilingResult.getTag et ajouté au nom du fichier de sortie (jusqu'aux 20 premiers caractères alphanumériques minuscules et traits d'union).
    • setDurationMs() : définit la durée d'enregistrement maximale en millisecondes (par exemple, 60000 pour 60 secondes).
    • setBufferFillPolicy() : définit la manière dont le tampon gère les limites de capacité. BufferFillPolicy.RING_BUFFER écrase les données les plus anciennes lorsqu'il est plein, tandis que BufferFillPolicy.DISCARD arrête d'enregistrer de nouvelles données une fois le tampon plein.
    • setBufferSizeKb() : définit la taille de la mémoire tampon d'enregistrement en kilo-octets.
  4. Facultatif : Arrêter l'enregistrement plus tôt : transmettez un CancellationSignal (stopSignal) pour arrêter la session dès que l'opération cible se termine, en enregistrant les données enregistrées jusqu'à présent et en envoyant le ProfilingResult à votre rappel. Pour en savoir plus, consultez Arrêter un enregistrement actif.

  5. Démarrer et recevoir des résultats : appelez requestProfiling() pour démarrer l'enregistrement en arrière-plan. À la fin de la session, resultCallback.accept() reçoit un ProfilingResult :

    • En cas de réussite (ERROR_NONE), appelez getResultFilePath() pour obtenir le chemin d'accès sur l'appareil. Pour savoir comment extraire des fichiers avec adb ou les importer avec WorkManager, consultez Récupérer et importer des profils.
    • En cas d'échec, inspectez getErrorCode() et getErrorMessage(). Les requêtes peuvent échouer en raison de la limitation du débit, ainsi que pour plusieurs autres raisons.
    • Si votre application se ferme avant que le résultat ne soit fourni, le système met le résultat en file d'attente et le fournit la prochaine fois que votre application enregistre un écouteur global avec registerForAllProfilingResults.
  6. Ajouter des points de trace personnalisés : encapsulez les opérations dans trace("...") { ... } (ou Trace.beginSection()/Trace.endSection()) pour étiqueter les régions de la chronologie.

Arrêter un enregistrement actif

Pour arrêter une capture dès qu'une interaction est terminée plutôt que d'attendre l'expiration de setDurationMs(), transmettez un CancellationSignal à setCancellationSignal() ou requestProfiling(). Si vous spécifiez à la fois CancellationSignal et setDurationMs(), la session se termine dès que l'un des deux événements se produit. Si vous ne spécifiez aucun des deux, le système applique une durée par défaut.

L'appel de cancel() ne supprime pas l'enregistrement. Le système arrête la capture, masque et enregistre les données collectées jusqu'à ce moment-là, puis transmet le ProfilingResult à votre écouteur avec ERROR_NONE.

Enregistrer des traces système avec l'échantillonnage de la pile d'appel

Sur les appareils équipés d'Android 17 (niveau d'API 37) ou version ultérieure (ou d'Android 16 avec un module principal Profiling mis à jour), ProfilingManager permet de collecter des piles d'appels échantillonnées en même temps qu'une trace système dans une seule session. Vous pouvez ainsi voir les événements de la chronologie, tels que les états des threads, les frames et les segments de trace personnalisés, ainsi que les piles d'appels du processeur pour les méthodes sans instrumentation de trace explicite.

Cet exemple montre également comment appeler ProfilingManager.requestProfiling directement sans les compilateurs Jetpack en spécifiant l'un des types de profils compatibles (PROFILING_TYPE_SYSTEM_TRACE) et en transmettant une configuration 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
);

Lorsque vous ouvrez le profil obtenu dans l'interface utilisateur de Perfetto, les piles d'appels échantillonnées s'affichent sous forme de pistes Callstacks dédiées, à côté des segments de trace standards de votre application. Sélectionnez une région d'une piste de pile d'appel pour afficher un graphique de type "flamme" des piles d'appel échantillonnées pour cette période, comme illustré à la figure 1.

Interface utilisateur de Perfetto montrant une trace système avec des pistes de pile d'appel échantillonnées, des segments de trace sur RenderThread et un graphique de type &quot;flamme&quot; de la pile d'appel pour la zone sélectionnée.
Figure 1. Trace système combinée à l'échantillonnage de la pile d'appel dans l'UI de Perfetto.

Capturer une empreinte de la mémoire Java

Vous pouvez demander un instantané de la mémoire Java de votre application à l'aide de JavaHeapDumpRequestBuilder (ou PROFILING_TYPE_JAVA_HEAP_DUMP). Contrairement aux empreintes de la mémoire .hprof standards capturées avec Debug.dumpHprofData, les empreintes de la mémoire ProfilingManager sont conçues pour une collecte sécurisée et à faible coût en production :

  • Collecte sans blocage : le système duplique un clone en copie sur écriture du processus de votre application et collecte l'instantané de segment de mémoire à partir du processus cloné. Ainsi, votre application ne se bloque pas pendant l'écriture du vidage sur le disque.
  • Graphique de référence uniquement (pas de données d'objet brutes) : au lieu de vider le contenu de la mémoire brute comme un fichier .hprof, ProfilingManager enregistre un graphique protobuf .perfetto-java-heap-dump contenant des classes, des tailles d'objets, des chaînes de référence et des racines de récupération de mémoire (GC), tout en omettant les valeurs primitives, le contenu des chaînes et les charges utiles de tableau. Cela protège la confidentialité des utilisateurs et réduit la taille du fichier. Pour en savoir plus, consultez Différences entre les empreintes de la mémoire ProfilingManager et les empreintes HPROF.

En utilisant la même configuration Context, Executor et resultCallback que celle présentée dans Enregistrer une trace système, créez une requête avec 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
);

Pour obtenir un tutoriel complet sur l'analyse d'une empreinte de la mémoire Java afin de diagnostiquer une fuite de mémoire, consultez Étude de cas : Déboguer un plantage OOM.

Étapes suivantes

Après avoir capturé les profils, récupérez-les localement avec adb ou importez-les dans votre backend avec WorkManager.