App-gesteuertes Profiling

Auf dieser Seite wird beschrieben, wie Sie mit der ProfilingManager API einen System-Trace aufzeichnen.

ProfilingManager kann auch andere Profiltypen aufzeichnen. Dieser Vorgang ähnelt dem Aufzeichnen eines System-Trace, aber für jeden Typ wird ein anderer Builder verwendet. Die unterstützten Profile und ihre Builder sind:

  • System-Traces: Werden mit SystemTraceRequestBuilder aufgezeichnet und sind nützlich für die Latenzanalyse und das allgemeine Leistungs-Debugging.

  • Heap-Dumps: Werden mit JavaHeapDumpRequestBuilder aufgezeichnet und sind hilfreich für die Erkennung und Optimierung von Speicherlecks.

  • Heap-Profile: Werden mit HeapProfileRequestBuilder aufgezeichnet und sind nützlich für die Speicheroptimierung.

  • Aufrufstack-Profile: Werden mit StackSamplingRequestBuilder aufgezeichnet und sind nützlich, um die Codeausführung und Latenzanalyse zu verstehen.

Abhängigkeiten hinzufügen

Für eine optimale Nutzung der ProfilingManager API fügen Sie der Datei build.gradle.kts die folgenden Jetpack-Bibliotheken hinzu.

Kotlin

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

Groovy

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

System-Trace aufzeichnen

Nachdem Sie die erforderlichen Abhängigkeiten hinzugefügt haben, verwenden Sie den folgenden Code, um einen System-Trace aufzuzeichnen. In diesem Beispiel wird gezeigt, wie Sie eine Profilerstellungssitzung über eine zusammensetzbare Funktion starten und gleichzeitig rechenintensive Vorgänge sicher außerhalb des Hauptthreads verwalten.

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

Der Beispielcode richtet die Profilerstellungssitzung ein und verwaltet sie. Dazu werden die folgenden Schritte ausgeführt:

  1. Executor einrichten Erstellen Sie einen Executor, um den Thread zu definieren, der die Profilerstellungsergebnisse empfängt. Die Profilerstellung erfolgt im Hintergrund. Wenn Sie einen Executor verwenden, der nicht im UI-Thread ausgeführt wird, können Sie ANR-Fehler (App antwortet nicht) vermeiden, falls Sie dem Callback später weitere Verarbeitungsschritte hinzufügen.

  2. Profiling-Ergebnisse verarbeiten Erstellen Sie ein Consumer<ProfilingResult>-Objekt. Das System verwendet dieses Objekt, um Profilerstellungsergebnisse von ProfilingManager an Ihre App zurückzusenden.

  3. Erstellen Sie die Profilerstellungsanfrage. Erstellen Sie eine SystemTraceRequestBuilder, um Ihre Profilerstellungssitzung einzurichten. Mit diesem Builder können Sie die ProfilingManager-Trace-Einstellungen anpassen. Das Anpassen des Builders ist optional. Wenn Sie dies nicht tun, verwendet das System die Standardeinstellungen.

    • Tag definieren Verwenden Sie setTag(), um dem Trace-Namen ein Tag hinzuzufügen. Mit diesem Tag können Sie den Trace identifizieren.
    • Optional: Legen Sie die Dauer fest. Mit setDurationMs() geben Sie die Profildauer in Millisekunden an. Mit 60000 wird beispielsweise ein 60-Sekunden-Trace festgelegt. Der Trace endet automatisch nach der angegebenen Dauer, wenn CancellationSignal nicht vorher ausgelöst wird.
    • Wählen Sie eine Pufferrichtlinie aus. Mit setBufferFillPolicy() können Sie festlegen, wie Trace-Daten gespeichert werden. BufferFillPolicy.RING_BUFFER bedeutet, dass neue Daten die ältesten Daten überschreiben, wenn der Puffer voll ist. So wird eine kontinuierliche Aufzeichnung der letzten Aktivitäten beibehalten.
    • Legen Sie eine Puffergröße fest. Mit setBufferSizeKb() können Sie eine Puffergröße für das Tracing angeben, mit der Sie die Größe der Ausgabe-Trace-Datei steuern können.
  4. Optional: Sitzungslebenszyklus verwalten. Erstellen Sie einen CancellationSignal. Mit diesem Objekt können Sie die Profilerstellungssitzung jederzeit beenden und so die Dauer genau steuern.

  5. Starten und Ergebnisse erhalten. Wenn Sie requestProfiling() aufrufen, startet ProfilingManager im Hintergrund eine Profilerstellungssitzung. Nach Abschluss der Profilerstellung wird die ProfilingResult an die Methode resultCallback#accept gesendet. Wenn die Profilerstellung erfolgreich abgeschlossen wurde, gibt ProfilingResult über ProfilingResult#getResultFilePath den Pfad an, unter dem der Trace auf Ihrem Gerät gespeichert wurde. Sie können diese Datei programmatisch oder für die lokale Profilerstellung durch Ausführen von adb pull <trace_path> auf Ihrem Computer abrufen.

  6. Benutzerdefinierte Trace-Punkte hinzufügen Sie können im Code Ihrer Anwendung benutzerdefinierte Trace-Punkte hinzufügen. Im vorherigen Codebeispiel wird mit dem Block trace("MyApp:HeavyOperation") { ... } ein benutzerdefiniertes Slice im generierten Profil erstellt.