App-gesteuertes Profiling

Unter Android 15 (API-Level 35) und höher können Sie mit der app-gesteuerten Profilerstellung unterstützte Profilaufzeichnungen im Code explizit starten und beenden. Verwenden Sie dazu die ProfilingManager API oder AndroidX Core-Builder (androidx.core:core 1.15.0 oder höher). Verwenden Sie direktes Profiling, wenn Sie den Zeitpunkt der Profilerstellung oder Sitzungsparameter wie Dauer, Pufferrichtlinien und Abtastraten steuern oder eine bestimmte User Journey oder Laufzeitbedingung anvisieren möchten.

Abhängigkeiten hinzufügen

Für eine optimale Nutzung der ProfilingManager API fügen Sie Ihrer build.gradle.kts-Datei 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-ktx: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 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 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.
}

Im Beispielcode wird die Profilerstellungssitzung in den folgenden Schritten eingerichtet und verwaltet:

  1. Executor einrichten: Erstellen Sie einen Executor ohne Benutzeroberfläche, um Profilerstellungsergebnisse außerhalb des Hauptthreads zu erhalten.
  2. Profilerstellungsergebnisse verarbeiten: Erstellen Sie einen Consumer<ProfilingResult>-Callback, um das abgeschlossene Ergebnis oder den Fehler von ProfilingManager zu erhalten.
  3. Profiling-Anfrage erstellen: Erstellen Sie ein SystemTraceRequestBuilder, um die Sitzungseinstellungen anzupassen (oder lassen Sie die Anpassungen weg, um die Systemstandards zu verwenden):

    • setTag(): Hängt ein vom Aufrufer bereitgestelltes Tag an, das von ProfilingResult.getTag zurückgegeben und an den Namen der Ausgabedatei angehängt wird (bis zu den ersten 20 alphanumerischen Kleinbuchstaben und Bindestrichen).
    • setDurationMs(): Legt die maximale Aufnahmedauer in Millisekunden fest (z. B. 60000 für 60 Sekunden).
    • setBufferFillPolicy(): Definiert, wie der Zwischenspeicher mit Kapazitätsgrenzen umgeht: BufferFillPolicy.RING_BUFFER überschreibt die ältesten Daten, wenn der Zwischenspeicher voll ist, während BufferFillPolicy.DISCARD die Aufzeichnung neuer Daten beendet, sobald der Zwischenspeicher voll ist.
    • setBufferSizeKb(): Legt die Größe des Aufzeichnungspuffers in Kilobyte fest.
  4. Optional: Aufzeichnung vorzeitig beenden: Übergeben Sie ein CancellationSignal (stopSignal), um die Sitzung zu beenden, sobald der Zielvorgang abgeschlossen ist. Dadurch werden die bis dahin aufgezeichneten Daten gespeichert und die ProfilingResult an Ihren Callback übergeben. Weitere Informationen finden Sie unter Aktive Aufzeichnung beenden.

  5. Ergebnisse starten und empfangen: Rufen Sie requestProfiling() auf, um die Aufnahme im Hintergrund zu starten. Wenn die Sitzung endet, erhält resultCallback.accept() einen ProfilingResult:

    • Rufen Sie bei Erfolg (ERROR_NONE) getResultFilePath() auf, um den Pfad auf dem Gerät abzurufen. Eine Anleitung zum Abrufen von Dateien mit adb oder zum Hochladen mit WorkManager finden Sie unter Profile abrufen und hochladen.
    • Prüfen Sie bei einem Fehler getErrorCode() und getErrorMessage(). Anfragen können aufgrund von Ratenbegrenzungen sowie aus verschiedenen anderen Gründen fehlschlagen.
    • Wenn Ihre App beendet wird, bevor das Ergebnis geliefert wird, stellt das System das Ergebnis in die Warteschlange und liefert es, wenn Ihre App das nächste Mal einen globalen Listener mit registerForAllProfilingResults registriert.
  6. Benutzerdefinierte Trace-Punkte hinzufügen: Schließen Sie Vorgänge in trace("...") { ... } (oder Trace.beginSection() / Trace.endSection()) ein, um Bereiche der Zeitachse zu kennzeichnen.

Aktive Aufnahme beenden

Wenn Sie eine Erfassung beenden möchten, sobald eine Interaktion abgeschlossen ist, anstatt auf das Ablaufen von setDurationMs() zu warten, übergeben Sie CancellationSignal an setCancellationSignal() oder requestProfiling(). Wenn Sie sowohl CancellationSignal als auch setDurationMs() angeben, wird die Sitzung beendet, sobald eine der beiden Bedingungen erfüllt ist. Wenn Sie keine der beiden angeben, wendet das System eine Standarddauer an.

Wenn du cancel() aufrufst, wird die Aufnahme nicht verworfen. Das System beendet die Erfassung, entfernt die bis zu diesem Zeitpunkt erfassten Daten und speichert sie. Anschließend wird die ProfilingResult mit ERROR_NONE an Ihren Listener gesendet.

System-Traces mit Aufrufstapel-Stichproben aufzeichnen

Auf Geräten mit Android 17 (API-Level 37) und höher (oder Android 16 mit einem aktualisierten Profiling-Mainline-Modul) unterstützt ProfilingManager das Erfassen von Stichproben-Aufrufstacks zusammen mit einem System-Trace in einer einzigen Sitzung. So können Sie Zeitachsenereignisse wie Thread-Zustände, Frames und benutzerdefinierte Trace-Slices zusammen mit CPU-Aufrufstacks für Methoden ohne explizite Trace-Instrumentierung sehen.

In diesem Beispiel wird auch gezeigt, wie Sie ProfilingManager.requestProfiling direkt ohne Jetpack-Builder aufrufen können. Dazu geben Sie einen der unterstützten Profiltypen (PROFILING_TYPE_SYSTEM_TRACE) an und übergeben eine Konfiguration 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
);

Wenn Sie das resultierende Profil in der Perfetto-Benutzeroberfläche öffnen, werden die Stichproben-Aufrufstacks als dedizierte Aufrufstack-Tracks neben den regulären Trace-Slices Ihrer App angezeigt. Wählen Sie einen Bereich eines Aufrufstack-Tracks aus, um ein Flame-Diagramm der Stichproben-Aufrufstacks für diesen Zeitraum aufzurufen (siehe Abbildung 1).

Perfetto-UI mit einem System-Trace mit Stichproben-Aufrufstapel-Tracks, Trace-Slices im RenderThread und einem Aufrufstapel-Flame-Diagramm für den ausgewählten Bereich.
Abbildung 1. Ein System-Trace kombiniert mit einer Aufrufstapel-Stichprobe in der Perfetto-Benutzeroberfläche.

Java-Heap-Dump erfassen

Sie können mit JavaHeapDumpRequestBuilder (oder PROFILING_TYPE_JAVA_HEAP_DUMP) einen Snapshot des Java-Arbeitsspeichers Ihrer Anwendung anfordern. Im Gegensatz zu standardmäßigen .hprof-Heap-Dumps, die mit Debug.dumpHprofData erfasst werden, sind ProfilingManager-Heap-Dumps für eine sichere Erfassung mit geringem Aufwand in der Produktion konzipiert:

  • Nicht einfrierende Erfassung: Das System erstellt einen Copy-on-Write-Klon des Prozesses Ihrer App und erfasst den Heap-Snapshot aus dem geklonten Prozess. So friert Ihre App nicht ein, während der Dump auf die Festplatte geschrieben wird.
  • Nur Referenzdiagramm (keine Rohdaten von Objekten): Anstatt Rohspeicherinhalte wie eine .hprof-Datei zu sichern, zeichnet ProfilingManager ein .perfetto-java-heap-dump-Protobuf-Diagramm auf, das Klassen, Objektgrößen, Referenzketten und Garbage Collection-Wurzeln (GC) enthält, während primitive Werte, Stringinhalte und Array-Nutzlasten weggelassen werden. Dies schützt den Datenschutz der Nutzer und reduziert die Dateigröße. Eine vollständige Übersicht finden Sie unter Unterschiede zwischen ProfilingManager-Heap-Dumps und HPROF-Dumps.

Verwenden Sie dieselbe Einrichtung für Context, Executor und resultCallback, die unter System-Trace aufzeichnen gezeigt wird, und erstellen Sie eine Anfrage mit 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
);

Eine vollständige End-to-End-Anleitung zur Analyse eines Java-Heap-Dumps zur Diagnose eines Speicherlecks finden Sie unter Fallstudie: OOM-Absturz beheben.

Nächste Schritte

Nachdem Sie Profile erfasst haben, rufen Sie sie lokal mit adb ab oder laden Sie sie mit WorkManager in Ihr Backend hoch.