Profilowanie oparte na aplikacjach

Na Androidzie 15 (API na poziomie 35) i nowszych profilowanie oparte na aplikacji umożliwia aplikacji jawne rozpoczynanie i zatrzymywanie obsługiwanych nagrań profilu w kodzie za pomocą interfejsu API ProfilingManager lub kreatorów AndroidX Core (androidx.core:core 1.15.0 lub nowszej). Używaj profilowania bezpośredniego, gdy chcesz kontrolować czas profilowania lub parametry sesji, takie jak czas trwania, zasady buforowania i częstotliwość próbkowania, lub kierować reklamy na konkretną ścieżkę użytkownika lub warunek środowiska wykonawczego.

Dodawanie zależności

Aby interfejs API ProfilingManager działał jak najlepiej, dodaj do pliku build.gradle.kts te biblioteki Jetpack.

Kotlin

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

Dynamiczny

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

Rejestrowanie śledzenia systemu

Po dodaniu wymaganych zależności użyj tego kodu, aby zarejestrować śledzenie systemu. Ten przykład pokazuje, jak rozpocząć sesję profilowania, bezpiecznie zarządzając ciężkimi operacjami poza wątkiem głównym.

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

Przykładowy kod konfiguruje sesję profilowania i zarządza nią w tych krokach:

  1. Skonfiguruj wykonawcę: utwórz obiekt Executor bez interfejsu, aby otrzymywać wyniki profilowania poza głównym wątkiem.
  2. Obsługa wyników profilowania: utwórz wywołanie zwrotne Consumer<ProfilingResult>, aby otrzymać ukończony wynik lub błąd z ProfilingManager.
  3. Utwórz żądanie profilowania: utwórz obiekt SystemTraceRequestBuilder, aby dostosować ustawienia sesji (lub pomiń dostosowywanie, aby użyć ustawień domyślnych systemu):

    • setTag(): dołącza tag dostarczony przez rozmówcę, który jest zwracany przez ProfilingResult.getTag i dołączany do nazwy pliku wyjściowego (do pierwszych 20 małych znaków alfanumerycznych i łączników).
    • setDurationMs(): ustawia maksymalny czas nagrywania w milisekundach (np. 60000 dla 60 sekund).
    • setBufferFillPolicy(): określa, jak bufor obsługuje limity pojemności: BufferFillPolicy.RING_BUFFER zastępuje najstarsze dane, gdy bufor jest pełny, a BufferFillPolicy.DISCARD przestaje rejestrować nowe dane, gdy bufor jest pełny.
    • setBufferSizeKb(): ustawia rozmiar bufora nagrywania w kilobajtach.
  4. Opcjonalnie: zatrzymaj nagrywanie wcześniej: przekaż CancellationSignal (stopSignal), aby zatrzymać sesję, gdy tylko zakończy się operacja docelowa. Spowoduje to zapisanie dotychczas zarejestrowanych danych i dostarczenie ProfilingResult do wywołania zwrotnego. Więcej informacji znajdziesz w sekcji Zatrzymywanie aktywnego nagrywania.

  5. Rozpoczynanie i otrzymywanie wyników: wywołaj requestProfiling(), aby rozpocząć nagrywanie w tle. Po zakończeniu sesji resultCallback.accept() otrzymuje ProfilingResult:

    • W przypadku powodzenia (ERROR_NONE) wywołaj getResultFilePath(), aby uzyskać ścieżkę na urządzeniu. Instrukcje dotyczące pobierania plików za pomocą adb lub przesyłania ich za pomocą WorkManager znajdziesz w artykule Pobieranie i przesyłanie profili.
    • W przypadku niepowodzenia sprawdź getErrorCode() i getErrorMessage(). Żądania mogą nie zostać zrealizowane z powodu ograniczenia liczby żądań, a także z kilku innych powodów.
    • Jeśli aplikacja zostanie zamknięta przed dostarczeniem wyniku, system umieści go w kolejce i dostarczy, gdy aplikacja zarejestruje globalny obiekt nasłuchujący za pomocą metody registerForAllProfilingResults.
  6. Dodawanie niestandardowych punktów śledzenia: opakuj operacje w trace("...") { ... } (lub Trace.beginSection() / Trace.endSection()), aby oznaczyć etykietami regiony osi czasu.

Zatrzymywanie aktywnego nagrywania

Aby zatrzymać przechwytywanie natychmiast po zakończeniu interakcji, zamiast czekać na wygaśnięcie setDurationMs(), przekaż CancellationSignal do setCancellationSignal() lub requestProfiling(). Jeśli określisz zarówno CancellationSignal, jak i setDurationMs(), sesja zakończy się, gdy nastąpi pierwsze z tych zdarzeń. Jeśli nie określisz żadnego z nich, system zastosuje domyślny czas trwania.

Wywołanie metody cancel() nie powoduje odrzucenia nagrania. System zatrzymuje przechwytywanie, ukrywa i zapisuje dane zebrane do tego momentu, a następnie dostarcza ProfilingResult do detektora za pomocą ERROR_NONE.

Rejestrowanie śledzenia systemu z próbkowaniem stosu wywołań

Na urządzeniach z Androidem 17 (poziom interfejsu API 37) lub nowszym (albo z Androidem 16 ze zaktualizowanym modułem głównym Profiling) ProfilingManager obsługuje zbieranie próbkowanych stosów wywołań wraz ze śledzeniem systemu w jednej sesji. Dzięki temu możesz wyświetlać zdarzenia na osi czasu, takie jak stany wątków, klatki i niestandardowe fragmenty śledzenia, wraz ze stosami wywołań procesora dla metod bez wyraźnego instrumentowania śledzenia.

Ten przykład pokazuje też, jak wywołać ProfilingManager.requestProfiling bezpośrednio bez kreatorów Jetpack, określając jeden z obsługiwanych typów profili (PROFILING_TYPE_SYSTEM_TRACE) i przekazując konfigurację 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
);

Gdy otworzysz wynikowy profil w interfejsie Perfetto, próbkowane stosy wywołań pojawią się jako osobne ścieżki Callstacks obok zwykłych fragmentów śladu aplikacji. Wybierz region ścieżki stosu wywołań, aby wyświetlić wykres płomieniowy próbkowanych stosów wywołań dla tego zakresu czasu, jak pokazano na rysunku 1.

Interfejs Perfetto pokazujący śledzenie systemu z próbkowanymi ścieżkami stosu wywołań, fragmentami śledzenia w wątku RenderThread i wykresem płomieniowym stosu wywołań dla wybranego obszaru.
Rysunek 1. Śledzenie systemu połączone z próbkowaniem stosu wywołań w interfejsie Perfetto.

Przechwytywanie zrzutu sterty Java

Możesz poprosić o zrzut pamięci Java aplikacji za pomocą JavaHeapDumpRequestBuilder (lub PROFILING_TYPE_JAVA_HEAP_DUMP). W przeciwieństwie do standardowych zrzutów sterty .hprof przechwytywanych za pomocą Debug.dumpHprofData zrzuty sterty ProfilingManager są przeznaczone do bezpiecznego zbierania danych w środowisku produkcyjnym przy niewielkim obciążeniu:

  • Zbieranie bez zawieszania: system tworzy klon procesu aplikacji typu copy-on-write i pobiera zrzut sterty ze sklonowanego procesu, dzięki czemu aplikacja nie zawiesza się podczas zapisywania zrzutu na dysku.
  • Tylko wykres referencyjny (bez nieprzetworzonych danych obiektów): zamiast zrzucać nieprzetworzoną zawartość pamięci, np. plik .hprof, ProfilingManager rejestruje wykres protobuf .perfetto-java-heap-dump zawierający klasy, rozmiary obiektów, łańcuchy referencyjne i korzenie odzyskiwania pamięci (GC), pomijając wartości podstawowe, zawartość ciągów znaków i ładunki tablic. Chroni to prywatność użytkowników i zmniejsza rozmiar pliku. Pełne zestawienie znajdziesz w sekcji Czym zrzuty sterty ProfilingManager różnią się od zrzutów HPROF.

Używając tych samych ustawień Context, Executor i resultCallback, które zostały pokazane w sekcji Rejestrowanie śladu systemu, utwórz żądanie za pomocą 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
);

Pełny opis analizy zrzutu sterty Java w celu zdiagnozowania wycieku pamięci znajdziesz w studium przypadku dotyczącym debugowania awarii OOM.

Dalsze kroki

Po przechwyceniu profili pobierz je lokalnie za pomocą adb lub prześlij do backendu za pomocą WorkManager.