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:
- Skonfiguruj wykonawcę: utwórz obiekt
Executorbez interfejsu, aby otrzymywać wyniki profilowania poza głównym wątkiem. - Obsługa wyników profilowania: utwórz wywołanie zwrotne
Consumer<ProfilingResult>, aby otrzymać ukończony wynik lub błąd zProfilingManager. 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 przezProfilingResult.getTagi 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.60000dla 60 sekund).setBufferFillPolicy(): określa, jak bufor obsługuje limity pojemności:BufferFillPolicy.RING_BUFFERzastępuje najstarsze dane, gdy bufor jest pełny, aBufferFillPolicy.DISCARDprzestaje rejestrować nowe dane, gdy bufor jest pełny.setBufferSizeKb(): ustawia rozmiar bufora nagrywania w kilobajtach.
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 dostarczenieProfilingResultdo wywołania zwrotnego. Więcej informacji znajdziesz w sekcji Zatrzymywanie aktywnego nagrywania.Rozpoczynanie i otrzymywanie wyników: wywołaj
requestProfiling(), aby rozpocząć nagrywanie w tle. Po zakończeniu sesjiresultCallback.accept()otrzymujeProfilingResult:- W przypadku powodzenia (
ERROR_NONE) wywołajgetResultFilePath(), aby uzyskać ścieżkę na urządzeniu. Instrukcje dotyczące pobierania plików za pomocąadblub przesyłania ich za pomocąWorkManagerznajdziesz w artykule Pobieranie i przesyłanie profili. - W przypadku niepowodzenia sprawdź
getErrorCode()igetErrorMessage(). Żą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.
- W przypadku powodzenia (
Dodawanie niestandardowych punktów śledzenia: opakuj operacje w
trace("...") { ... }(lubTrace.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.
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,ProfilingManagerrejestruje wykres protobuf.perfetto-java-heap-dumpzawierają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.