Профилирование на основе триггеров

ProfilingManager поддерживает создание профилей на основе системных триггеров. Система управляет процессом записи и предоставляет полученный профиль вашему приложению.

Триггеры привязаны к критически важным для производительности событиям. Зарегистрированные в системе профили предоставляют подробную отладочную информацию для критически важных пользовательских сценариев (CUJ), связанных с этими триггерами.

Сбор исторических данных

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

Например, длительная операция в потоке пользовательского интерфейса вызывает ошибку « Приложение не отвечает» (ANR) . К тому моменту, когда система обнаружит ANR и сообщит об этом приложению, операция может уже завершиться. Запуск профилирования в этот момент пропускает фактическую блокирующую работу.

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

Почему следует использовать захват по триггеру?

Основная причина использования триггеров профилирования — сбор данных о непредсказуемых событиях, когда приложение не может начать запись вручную до их наступления. Триггеры профилирования могут использоваться для:

  • Отладка проблем с производительностью: диагностика ошибок ANR, утечек памяти и других проблем со стабильностью.
  • Оптимизируйте ключевые пользовательские сценарии: анализируйте и улучшайте процессы, например, при запуске приложения.
  • Понимание поведения пользователей: получение информации о событиях, например, о выходе пользователя из приложения по его собственной инициативе.

Настройте триггер

Приведенный ниже код демонстрирует, как зарегистрироваться для триггера TRIGGER_TYPE_APP_FULLY_DRAWN и применить к нему ограничение скорости запросов.

Котлин

fun recordWithTrigger() {
    val profilingManager = applicationContext.getSystemService(ProfilingManager::class.java)

    val triggers = ArrayList<ProfilingTrigger>()

    val triggerBuilder = ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_APP_FULLY_DRAWN)
        .setRateLimitingPeriodHours(1)

    triggers.add(triggerBuilder.build())

    val mainExecutor: Executor = Executors.newSingleThreadExecutor()

    val resultCallback = Consumer<ProfilingResult> { profilingResult ->
        if (profilingResult.errorCode == ProfilingResult.ERROR_NONE) {
            Log.d(
                "ProfileTest",
                "Received profiling result file=" + profilingResult.resultFilePath
            )
            setupProfileUploadWorker(profilingResult.resultFilePath)
        } else {
            Log.e(
                "ProfileTest",
                "Profiling failed errorcode=" + profilingResult.errorCode + " errormsg=" + profilingResult.errorMessage
            )
        }
    }

    profilingManager.registerForAllProfilingResults(mainExecutor, resultCallback)
    profilingManager.addProfilingTriggers(triggers)

Java

public void recordWithTrigger() {
  ProfilingManager profilingManager = getApplicationContext().getSystemService(
      ProfilingManager.class);
  List<ProfilingTrigger> triggers = new ArrayList<>();
  ProfilingTrigger.Builder triggerBuilder = new ProfilingTrigger.Builder(
      ProfilingTrigger.TRIGGER_TYPE_APP_FULLY_DRAWN);
  triggerBuilder.setRateLimitingPeriodHours(1);
  triggers.add(triggerBuilder.build());

  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());
          }
        }
      };
  profilingManager.registerForAllProfilingResults(mainExecutor, resultCallback);
  profilingManager.addProfilingTriggers(triggers);

Код выполняет следующие шаги:

  1. Получить менеджера : Получает службу ProfilingManager .
  2. Определите триггер : Создает триггер ProfilingTrigger для TRIGGER_TYPE_APP_FULLY_DRAWN . Это событие происходит, когда приложение сообщает о завершении запуска и переходе в интерактивный режим.
  3. Установить ограничения скорости : Применяет ограничение скорости в 1 час к этому конкретному триггеру ( setRateLimitingPeriodHours(1) ). Это предотвращает запись приложением более одного профиля запуска в час.
  4. Зарегистрировать слушатель : вызывает функцию registerForAllProfilingResults для определения функции обратного вызова, обрабатывающей результат. Эта функция обратного вызова получает путь к сохраненному профилю через getResultFilePath() .
  5. Добавить триггеры : регистрирует список триггеров в ProfilingManager с помощью addProfilingTriggers .
  6. Событие срабатывания : вызывает функцию reportFullyDrawn() , которая генерирует событие TRIGGER_TYPE_APP_FULLY_DRAWN для системы, запуская сбор данных профилирования при условии, что фоновая трассировка системы выполнялась и имелась квота ограничения скорости. Этот необязательный шаг демонстрирует сквозной процесс, поскольку ваше приложение должно вызвать reportFullyDrawn() для этого триггера.

Извлечь трассировку

Система сохраняет профили, созданные по триггерам, в том же каталоге, что и другие профили. Имя файла для трассировок, созданных по триггерам, имеет следующий формат:

profile_trigger_<profile_type_code>_<datetime>.<profile-type-name>

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

adb pull /data/user/0/com.example.sampleapp/files/profiling/profile_trigger_1_2025-05-06-14-12-40.perfetto-trace

Подробную информацию о визуализации этих данных см. в разделе «Получение и анализ данных профилирования» .

Как работает трассировка фона

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

После сохранения профиля система уведомляет ваше приложение, используя функцию обратного вызова, предоставленную методу registerForAllProfilingResults . Эта функция обратного вызова предоставляет путь к сохраненному профилю, к которому можно получить доступ, вызвав метод ProfilingResult#getResultFilePath() .

Диаграмма, демонстрирующая принцип работы фоновых снимков трассировки с использованием кольцевого буфера, захватывающего данные до срабатывания триггерного события.
Рисунок 1 : Принцип работы снимков фоновой трассировки.

Чтобы уменьшить влияние на производительность устройства и время работы от батареи, система не выполняет фоновые трассировки непрерывно. Вместо этого она использует метод выборки. Система случайным образом запускает фоновую трассировку в течение заданного временного интервала (с минимальной и максимальной продолжительностью). Случайное распределение этих трассировок улучшает охват срабатывания триггеров.

Профили, запускаемые системой, имеют заданный системой максимальный размер, поэтому они используют кольцевой буфер. Как только буфер заполняется, новые данные трассировки перезаписывают самые старые. Как показано на рисунке 1, захваченная трассировка может не охватывать всю продолжительность фоновой записи, если буфер заполняется; вместо этого она представляет собой самую последнюю активность, предшествовавшую запуску триггера.

Внедрить ограничение скорости, зависящее от конкретного триггера.

Часто срабатывающие триггеры могут быстро исчерпать квоту ограничителя скорости вашего приложения. Для лучшего понимания работы ограничителя скорости мы рекомендуем ознакомиться с разделом «Как работает ограничитель скорости» . Чтобы предотвратить исчерпание квоты одним типом триггера, вы можете реализовать ограничение скорости для каждого триггера отдельно.

ProfilingManager поддерживает ограничение скорости запросов, определяемое конкретным приложением и запускаемое при помощи триггера. Это позволяет добавить еще один уровень регулирования скорости запросов в зависимости от времени, в дополнение к существующему ограничителю. Используйте API setRateLimitingPeriodHours , чтобы установить определенное время ожидания для триггера. После истечения этого времени вы можете запустить его снова.

Отладка запускает локально

Поскольку фоновые трассировки запускаются в случайное время, отладка триггеров локально затруднена. Чтобы принудительно запустить фоновую трассировку для тестирования, используйте следующую команду ADB:

adb shell device_config put profiling_testing system_triggered_profiling.testing_package_name <com.example.myapp>

Эта команда заставляет систему запустить непрерывную фоновую трассировку для указанного пакета, что позволяет каждому триггеру собирать профилирование, если это разрешает ограничитель скорости.

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