Профилирование на основе приложений

В Android 15 (уровень API 35) и более поздних версиях профилирование на основе приложения позволяет явно запускать и останавливать поддерживаемые записи профиля в коде с помощью API ProfilingManager или конструкторов AndroidX Core (androidx.core:core 1.15.0 или более поздней версии). Используйте прямое профилирование, если хотите контролировать время профилирования или параметры сеанса, такие как длительность, политики буферизации и частота дискретизации, или ориентироваться на определенный путь пользователя или условие выполнения.

Добавьте зависимости

Чтобы использовать все возможности ProfilingManager API, добавьте в файл build.gradle.kts следующие библиотеки Jetpack:

Kotlin

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

Яркий

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

Как записать трассировку системы

Добавив необходимые зависимости, используйте следующий код, чтобы записать трассировку системы. В этом примере показано, как запустить сеанс профилирования, безопасно управляя тяжелыми операциями вне основного потока.

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

Образец кода настраивает сеанс профилирования и управляет им следующим образом:

  1. Настройте исполнитель. Создайте объект Executor без интерфейса, чтобы получать результаты профилирования вне основного потока.
  2. Обработка результатов профилирования. Создайте обратный вызов Consumer<ProfilingResult>, чтобы получить результат или ошибку от ProfilingManager.
  3. Создайте запрос на профилирование. Создайте объект SystemTraceRequestBuilder, чтобы настроить параметры сеанса (или пропустите этот шаг, чтобы использовать настройки по умолчанию):

    • setTag() – прикрепляет предоставленный вызывающим абонентом тег, который возвращается ProfilingResult.getTag и добавляется к названию выходного файла (до первых 20 букв и цифр в нижнем регистре и дефисов).
    • setDurationMs() – максимальная продолжительность записи в миллисекундах (например, 60000 для 60 секунд).
    • setBufferFillPolicy() – определяет, как буфер обрабатывает ограничения емкости: BufferFillPolicy.RING_BUFFER перезаписывает самые старые данные при заполнении, а BufferFillPolicy.DISCARD прекращает запись новых данных после заполнения буфера.
    • setBufferSizeKb(): задает размер буфера записи в килобайтах.
  4. Необязательно. Остановка записи раньше времени. Передайте CancellationSignal (stopSignal), чтобы остановить сеанс, как только целевая операция будет завершена. При этом записанные данные будут сохранены, а ProfilingResult будет доставлен в обратный вызов. Подробнее о том, как остановить запись…

  5. Начать и получить результаты. Вызовите requestProfiling(), чтобы начать запись в фоновом режиме. Когда сеанс завершается, resultCallback.accept() получает ProfilingResult:

    • Если вызов ERROR_NONE выполнен успешно, вызовите getResultFilePath(), чтобы получить путь к файлу на устройстве. Инструкции по извлечению файлов с помощью adb и их загрузке с помощью WorkManager приведены в статье Как извлекать и загружать профили.
    • Если произошел сбой, проверьте getErrorCode() и getErrorMessage(). Запросы могут не выполняться из-за ограничения частоты запросов, а также по другим причинам.
    • Если приложение завершит работу до того, как результат будет получен, система поставит его в очередь и доставит, когда приложение в следующий раз зарегистрирует глобальный прослушиватель с помощью registerForAllProfilingResults.
  6. Добавьте собственные точки трассировки. Оберните операции в trace("...") { ... } (или Trace.beginSection()/Trace.endSection()), чтобы пометить области временной шкалы.

Как остановить запись

Чтобы остановить запись сразу после завершения взаимодействия, не дожидаясь истечения срока действия setDurationMs(), передайте CancellationSignal в setCancellationSignal() или requestProfiling(). Если вы укажете оба параметра (CancellationSignal и setDurationMs()), сеанс завершится по достижении любого из них. Если вы не укажете ни один из параметров, система применит продолжительность по умолчанию.

Если вы позвоните по номеру cancel(), запись не будет удалена. Система останавливает запись, редактирует и сохраняет собранные к этому моменту данные, а затем отправляет ProfilingResult вашему прослушивателю с ERROR_NONE.

Как записывать трассировки системы с выборкой стека вызовов

На устройствах с Android 17 (уровень API 37) и более поздних версий (или Android 16 с обновленным основным модулем Profiling) ProfilingManager поддерживает сбор выборочных стеков вызовов вместе с трассировкой системы за один сеанс. Это позволяет просматривать события временной шкалы, такие как состояния потоков, фреймы и пользовательские срезы трассировки, вместе со стеками вызовов ЦП для методов без явного инструментирования трассировки.

В этом примере также показано, как вызвать ProfilingManager.requestProfiling напрямую без построителей Jetpack, указав один из поддерживаемых типов профилей (PROFILING_TYPE_SYSTEM_TRACE) и передав конфигурацию 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
);

Когда вы откроете полученный профиль в интерфейсе Perfetto, стеки вызовов появятся в виде отдельных дорожек Callstacks рядом с обычными фрагментами трассировки приложения. Выберите область дорожки стека вызовов, чтобы посмотреть флейм-граф стеков вызовов, выбранных для этого интервала времени, как показано на рисунке 1.

Интерфейс Perfetto, в котором показана трассировка системы с дорожками стека вызовов, фрагменты трассировки в RenderThread и флейм-граф стека вызовов для выбранной области.
Рисунок 1. Трассировка системы в сочетании с выборкой стека вызовов в Perfetto UI.

Как получить дамп кучи Java

Вы можете запросить снимок памяти Java вашего приложения с помощью JavaHeapDumpRequestBuilder (или PROFILING_TYPE_JAVA_HEAP_DUMP). В отличие от стандартных дампов кучи .hprof, полученных с помощью Debug.dumpHprofData, дампы кучи ProfilingManager предназначены для безопасного сбора данных с низкими накладными расходами в рабочей среде:

  • Сбор без зависания. Система создает копию процесса приложения при записи и собирает снимок кучи из клонированного процесса, поэтому приложение не зависает, пока дамп записывается на диск.
  • Только граф ссылок (без необработанных данных объектов). Вместо дампа необработанного содержимого памяти, как в файле .hprof, ProfilingManager записывает граф protobuf .perfetto-java-heap-dump, содержащий классы, размеры объектов, цепочки ссылок и корни сборки мусора, опуская примитивные значения, содержимое строк и полезную нагрузку массивов. Это защищает конфиденциальность пользователей и уменьшает размер файла. Подробную информацию вы найдете в разделе Чем дампы кучи ProfilingManager отличаются от дампов HPROF.

Используя те же настройки Context, Executor и resultCallback, что и в разделе Как записать трассировку системы, создайте запрос с помощью 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
);

Полное пошаговое руководство по анализу дампа кучи Java для диагностики утечки памяти можно найти в статье Пример из практики: отладка сбоя из-за нехватки памяти.

Дальнейшие действия

После записи профилей извлеките их локально с помощью adb или загрузите в серверную часть с помощью WorkManager.