Profilage basé sur les applications

Cette page explique comment enregistrer une trace système à l'aide de l'API ProfilingManager.

ProfilingManager peut également enregistrer d'autres types de profils. Ce processus est semblable à l'enregistrement d'une trace système, mais chaque type utilise un compilateur différent. Les profils compatibles et leurs compilateurs sont les suivants :

  • Traces système : enregistrées à l'aide de SystemTraceRequestBuilder, qui sont utiles pour l'analyse de la latence et le débogage des performances générales.

  • Empreintes de la mémoire : enregistrées à l'aide de JavaHeapDumpRequestBuilder, qui sont utiles pour la détection et l'optimisation des fuites de mémoire.

  • Profils de mémoire : enregistrés à l'aide de HeapProfileRequestBuilder, qui sont utiles pour l'optimisation de la mémoire.

  • Profils de pile d'appels : enregistrés à l'aide de StackSamplingRequestBuilder, qui sont utiles pour comprendre l'exécution de code et l'analyse de la latence.

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.1")
       implementation("androidx.core:core:1.19.0")
   }
   

Groovy

   dependencies {
       implementation 'androidx.tracing:tracing:2.0.1'
       implementation 'androidx.core:core:1.19.0'
   }
   

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 à partir d'un composable tout en gérant en toute sécurité 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 trace.
            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 heavyOperation() {
  // Computations you want to profile
}

void sampleRecordSystemTrace() {
  Executor mainExecutor = 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());
          }
        }
      };
  CancellationSignal stopSignal = new CancellationSignal();

  SystemTraceRequestBuilder requestBuilder = new SystemTraceRequestBuilder();
  requestBuilder.setCancellationSignal(stopSignal);
  requestBuilder.setTag("FOO");
  requestBuilder.setDurationMs(60000);
  requestBuilder.setBufferFillPolicy(BufferFillPolicy.RING_BUFFER);
  requestBuilder.setBufferSizeKb(32768);
  Profiling.requestProfiling(getApplicationContext(), requestBuilder.build(), mainExecutor,
      resultCallback);

  // Wait some time for profiling to start.

  Trace.beginSection("MyApp:HeavyOperation");
  heavyOperation();
  Trace.endSection();

  // Once the interesting code section is profiled, stop profile
  stopSignal.cancel();
}

L'exemple de code configure et gère la session de profilage en suivant les étapes suivantes :

  1. Configurer l'exécuteur Créez un Executor pour définir le thread qui recevra les résultats du profilage. Le profilage s'effectue en arrière-plan. L'utilisation d'un exécuteur de thread non UI permet d'éviter les erreurs « L'application ne répond pas » (ANR) si vous ajoutez ultérieurement un traitement au rappel.

  2. Gérer les résultats du profilage Créez un objet Consumer<ProfilingResult>. Le système utilise cet objet pour renvoyer les résultats du profilage de ProfilingManager à votre application.

  3. Créer la requête de profilage Créez un SystemTraceRequestBuilder pour configurer votre session de profilage. Ce compilateur vous permet de personnaliser les paramètres de trace ProfilingManager. La personnalisation du compilateur est facultative. Si vous ne le faites pas, le système utilise les paramètres par défaut.

    • Définir un tag Utilisez setTag() pour ajouter un tag au nom de la trace. Ce tag vous aide à identifier la trace.
    • Facultatif : définir la durée Utilisez setDurationMs() pour spécifier la durée du profilage en millisecondes. Par exemple, 60000 définit une trace de 60 secondes. La trace se termine automatiquement après la durée spécifiée si CancellationSignal n'est pas déclenché avant.
    • Choisir une règle de mémoire tampon Utilisez setBufferFillPolicy() pour définir le mode de stockage des données de trace. BufferFillPolicy.RING_BUFFER signifie que lorsque la mémoire tampon est pleine, les nouvelles données écrasent les données les plus anciennes, ce qui permet de conserver un enregistrement continu de l'activité récente.
    • Définir une taille de mémoire tampon Utilisez setBufferSizeKb() pour spécifier une taille de mémoire tampon pour le traçage, que vous pouvez utiliser pour contrôler la taille du fichier de trace de sortie.
  4. Facultatif : gérer le cycle de vie de la session Créez un CancellationSignal. Cet objet vous permet d'arrêter la session de profilage quand vous le souhaitez, ce qui vous donne un contrôle précis sur sa durée.

  5. Démarrer et recevoir les résultats Lorsque vous appelez requestProfiling(), ProfilingManager démarre une session de profilage en arrière-plan. Une fois le profilage terminé, il envoie le ProfilingResult à votre méthode resultCallback#accept. Si le profilage se termine correctement, le ProfilingResult fournit le chemin d'accès où la trace a été enregistrée sur votre appareil via ProfilingResult#getResultFilePath. Vous pouvez obtenir ce fichier par programmation ou, pour le profilage local, en exécutant adb pull <trace_path> à partir de votre ordinateur.

  6. Ajouter des points de trace personnalisés Vous pouvez ajouter des points de trace personnalisés dans le code de votre application. Dans l'exemple de code précédent, le trace("MyApp:HeavyOperation") { ... } bloc crée une tranche personnalisée dans le profil généré.