Perfetto — крутейший инструмент. Он покажет вам те проблемы с производительностью, которые не заметит другой профайлер.

Perfetto покажет, что процессор занят системными задачами, когда ваш поток готов работать. Подсветит, что GC блокирует UI на 50 миллисекунд. А ещё расскажет, что именно планировщик ядра выкидывает поток с CPU.

Привет! Меня зовут Андрей Гришанов. В этой статье я расскажу, что такое Perfetto и как использовать его максимально эффективно.

Вы узнаете, как записать трейс холодного старта приложения и написать SQL-запрос для поиска дропнутых кадров. А ещё я покажу, как автоматизировать весь процесс в bash-скрипт, который выдаёт готовый отчёт за 15 секунд.

Немного истории: что такое Perfetto и откуда он взялся

Perfetto — это не очередной инструмент профилирования, а полноценная экосистема трассировки производственного уровня. Google разработал её в 2017-2018 годах, чтобы заменить устаревшие systrace и chrome://tracing на Android и в браузере Chrome.

В отличие от предшественников Perfetto был спроектирован как система следующего поколения. Упор в ней делался на масштабирование — от локальной отладки на одном устройстве до анализа трейсов с тысяч устройств в тестовых лабораториях Google.

Perfetto стал доступен в Android 9, а уже в Android 11 стал стандартной системой трассировки по умолчанию. Если вы использовали System Tracing в Android Studio версии 2021 года или новее, под капотом уже работал Perfetto. 

Какую проблему решает Perfetto?

Мем, по которому можно понять, что мне уже много лет
Мем, по которому можно понять, что мне уже много лет

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

Да только ваше приложение работает не в вакууме. Оно конкурирует за ресурсы с десятками системных процессов, зависит от планировщика ядра Linux и может терять процессорное время, даже когда ваш код идеален.

Perfetto решает эту проблему, объединяя три источника данных на одной временной шкале:

  1. ftrace (ядро Linux) — когда ваш поток реально выкинули с процессора (событие sched_switch);

  2. atrace (Android Framework) — события measure, layout, draw от системы;

  3. Пользовательские трейсы — добавленные разработчиком трейсы бизнес-логики приложения.

Это как в чёрном ящике самолёта. Вы видите и действия пилота (кастомные трейсы), и показания датчиков системы: ветер, высоту, температуру двигателей (аtrace и ftrace). Вот как это может выглядеть:

Кастомные трейсы

Они же — пользовательские трейсы. Это незаменимые помощники в изучении проблем с производительностью.

Пока ftrace и atrace отражают системную картину, кастомные трейсы показывают, когда та или иная операция прошла внутри приложения: «тут загружаются данные из API, а тут — парсится JSON на 5000 элементов». Чтобы добавить их в приложение, нужна одна зависимость:

dependencies {
    implementation("androidx.tracing:tracing-ktx:1.3.0-alpha02")
}

Теперь оборачиваем нужные участки кода:

import androidx.tracing.trace

class MyViewModel {
   
  fun loadData() {
        trace("MyViewModel.loadData") {
            val data = repository.fetchData()
            processData(data)
        }
    }
    
    private fun processData(data: List<Item>) {
        trace("MyViewModel.processData") {
            data.map { transform(it) }
        }
    }
}

Готово! В трейсе вы увидите слайсы с точным временем выполнения каждого блока. Например, если processData занимает 150 мс, а Main Thread в это время проводит 80 мс в состоянии Runnable и ждёт CPU, понятно, что половину времени поток простоял из-за нехватки процессорного времени, а не из-за медленного кода.

Оверхед: в продакшене этот код ничего не стоит: ~3-5 наносекнуд на проверку, активна ли запись. Смело оборачивайте все тяжёлые операции в приложении!

Бонус: интеграция с Firebase Performance

Кстати, кастомные трейсы можно не только локально анализировать через Perfetto. Вы можете и отправлять их в Firebase для мониторинга продовских сборок через Firebase Performance SDK:

dependencies {
    implementation("com.google.firebase:firebase-perf:20.5.2")
}

Используйте тот же синтаксис trace(), и метрики автоматически попадут во вкладку Firebase Performance в консоли Firebase. Там вы увидите агрегированную статистику по всем пользователям: медианное время выполнения, 90-й перцентиль, количество срабатываний. Вот, например, как выглядит метрика startup_init_app_scope, которую я показывал на скриншоте ранее:

Это особенно полезно для отслеживания регрессий после релизов. Если медианное время loadData() выросло с 250 мс до 800 мс после обновления, вы узнаете об этом из дашборда, а не из гневных отзывов в Google Play.

Возвращаемся к главному: мы познако��ились со всеми трёмя источниками данных: ftrace от ядра, atrace от Android и кастомными трейсами. Теперь разберёмся, как их собрать в один файл трейса, и что он вообще из себя представляет.

Как это работает: анатомия трейса

Файл .perfetto-trace содержит два типа данных:

Duration Events (действия с длительностью):

  • метод onDraw выполнялся 16 мс;

  • слайс activityResume длился 300 мс;

  • GC работал 45 мс.

Instant Events (мгновенные снимки состояния):

  • частота CPU упала до 1.8 ГГц в момент времени T;

  • поток Main находится в состоянии Runnable (готов работать, но ждёт CPU);

  • температура процессора 45°C.

Perfetto не только показывает, что «UI лагает», но и из-за чего это происходит: «запустилась анимация (ваш код), но в эту же миллисекунду системный процесс kworker загрузил CPU на 80%, поэтому ваш поток провел 50 мс в состоянии Runnable вместо Running».

Три способа записать трейс

1. Android Studio Profiler

Самый простой способ для начала. Кликаете по кнопке записи, взаимодействуете с приложением, останавливаете. Файл можно открыть в Chrome в интерфейсе ui.perfetto.dev.

Плюсы:

  • не требует настройки — работает из коробки;

  • знакомый интерфейс для тех, кто уже использовал Android Studio;

  • файл автоматически сохраняется локально.

Минусы:

  • ограниченные настройки записи — не все категории ftrace доступны;

  • фиксированный размер буфера;

  • нужно подключение к компьютеру.

2. System Tracing App

Встроенное приложение в меню разработчика Android. Запускаете запись, тестируете приложение, останавливаете — файл сохраняется на устройстве.

Плюсы:

  • работает без компьютера — можно записать трейс на любом устройстве;

  • идеально подходит для воспроизведения проблем в полевых условиях: можете дать инструкцию пользователю, а он запишет трейс на своём лагающем устройстве;

  • больше настроек, чем в Android Studio Profiler.

Минусы:

  • менее удобен для систематического анализа;

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

Perfetto CLI — любовь с первого запроса

Perfetto CLI — это работа через терминал на компьютере с утилитой perfetto, которая находится на Android-устройстве (/system/bin/perfetto). Вы создаёте конфигурационный файл локально, загружаете его на устройство через adb push, а затем запускаете запись командой adb shell perfetto -c путь_к_конфигу.

Плюсы:

  • полный контроль над всеми параметрами записи;

  • воспроизводимые конфигурации для CI/CD;

  • можно запускать программно из скриптов;

  • точная настройка источников данных и размера буфера.

Минусы:

  • требует понимания конфигурационного формата;

  • более высокий порог входа.

Просто перечислить плюсы и минусы Perfetto CLI — проявить к нему неуважение. Если Android Studio Profiler — это наушники с преднастроенным звуком от производителя, то CLI — это наушники с 15-полосным эквалайзером. Это выбор настоящих ценителей гибкой настройки, CI/CD пайплайнов и выверенного контроля над тем, что и когда происходит.

Анатомия конфига: из чего он состоит

Конфигурационный файл описывается в формате protobuf text обычно с расширением .pbtxt. Его структура достаточно простая — два главных блока.

Блок 1: Буферы (buffers), где хранятся события в памяти

Perfetto использует кольцевые буферы: когда буфер заполняется, старые данные перезаписываются новыми. Это позволяет записывать длинные трейсы без риска переполнить память.

buffers: {
    size_kb: 65536               # 64 МБ
    fill_policy: RING_BUFFER     # Или DISCARD (остановить при заполнении)
}

Для холодного старта 64 МБ обычно достаточно. Если нужно записать 30+ секунд активного использования — увеличиваем до 128-256 МБ.

Блок 2: Источники данных (data_sources) — что именно записывать

Perfetto поддерживает десятки источников. Самые полезные для Android-разработчика:

  • linux.ftrace — события ядра Linux: переключения потоков, частоты CPU;

  • atrace категории — события Android Framework: AM, View system, Graphics;

  • linux.process_stats — статистика процессов: CPU, память;

  • android.heapprofd — профилирование native памяти.

Минималистичный конфиг для начала

Начнем с простого конфига для записи холодного старта. Создадим файл startup.pbtxt на ПК:

buffers: {
    size_kb: 65536
    fill_policy: RING_BUFFER
}

data_sources: {
    config {
        name: "linux.ftrace"
        ftrace_config {
            # События планировщика
            ftrace_events: "sched/sched_switch"
            # Частота процессора
            ftrace_events: "power/cpu_frequency"
            
            # Android Framework
            atrace_categories: "am"      # Activity Manager
            atrace_categories: "view"    # View system
            atrace_categories: "dalvik"  # ART VM
            
            # Только ваше приложение
            atrace_apps: "com.your.app"
        }
    }
}

duration_ms: 15000  # 15 секунд

Этот конфиг включает минимальный набор для анализа старта: планировщик, частоты CPU и основные категории Android. Perfetto запишет 15 секунд и автоматически остановится.

Как использовать:

# 1. Загружаем конфиг на устройство
adb push startup.pbtxt /data/local/tmp/

# 2. Запускаем запись
adb shell perfetto \
  -c /data/local/tmp/startup.pbtxt \
  -o /data/misc/perfetto-traces/trace.perfetto-trace

# 3. Скачиваем результат
adb pull /data/misc/perfetto-traces/trace.perfetto-trace

Perfetto сохранит файл, можно открывать на ui.perfetto.dev.

Когда нужно больше данных

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

Для глубокой диагностики лагов UI добавьте в ftrace_config:

ftrace_events: "sched/sched_wakeup"    # Когда потоки просыпаются
ftrace_events: "power/cpu_idle"        # Состояния сна CPU
atrace_categories: "gfx"               # SurfaceFlinger
atrace_categories: "input"             # Тапы/свайпы
atrace_categories: "sync"              # Синхронизация потоков

И увеличьте буфер до 128 МБ (size_kb: 131072). Так уместим больше событий.

Для профилирования памяти нужно добавить новый data_source и увеличить буфер до 256 МБ (size_kb: 262144), потому что heap dumps занимают много места:

data_sources: {
    config {
        name: "android.heapprofd"
        heapprofd_config {
            sampling_interval_bytes: 4096
            process_cmdline: "com.your.app"
            continuous_dump_config {
                dump_interval_ms: 10000  # Снимок каждые 10 сек
            }
        }
    }
}

Для мониторинга нагрузки процессов добавляем:

data_sources: {
    config {
        name: "linux.process_stats"
        process_stats_config {
            scan_all_processes_on_start: true
            proc_stats_poll_ms: 1000  # Опрос каждую секунду
        }
    }
}

Главное правило: не включайте всё подряд. Каждый источник увеличивает размер трейса и вносит оверхед. Лучше начать с минимального набора, а если проблема не определяется — расширьте зону поиска.

Короткий синтаксис для экспериментов

Если не хочется создавать файл для быстрой проверки, есть короткий синтаксис:

adb shell perfetto \
  -o /data/misc/perfetto-traces/trace.perfetto-trace \
  -t 20s \                                  # Длительность
  -b 64mb \                                 # Размер буфера
  --app com.your.app \                      # Ваше приложение
  sched freq am view gfx dalvik             # Категории через пробел

Это эквивалентно конфигу, но написано в одну строку. Удобно для одноразовых экспериментов.

Фоновая запись для длинных сценариев

Если нужно записать трейс, пока вы выполняете длинную последовательность действий:

# Запускаем в фоне (Perfetto выведет PID)
adb shell perfetto \
  -c /data/local/tmp/config.pbtxt \
  -o /data/misc/perfetto-traces/trace.perfetto-trace \
  --background

# Сохраняем PID, например: 12345
# ... выполняете действия в приложении ...

# Останавливаем по PID
adb shell kill -TERM 12345

# Ждем завершения записи
sleep 2

# Скачиваем
adb pull /data/misc/perfetto-traces/trace.perfetto-trace

Справочник. Полезные atrace категории:

Категория

Что показывает

Когда использовать

am

Activity Manager

Старт приложения, lifecycle

wm

Window Manager

Работа с окнами, layout

view

View system

measure/layout/draw

gfx

SurfaceFlinger

Композиция кадров

input

Input events

Лаги при тапах

dalvik

ART VM

GC, JIT-компиляция

database

SQLite

Медленные запросы к БД

network

Network stack

Сетевые запросы

camera

Camera HAL

Проблемы с камерой

bionic

C library

malloc, threading

Полный список: adb shell atrace --list_categories

Важные детали

  • На Android 9-10 (если у вас не Pixel): может потребоваться adb shell setprop persist.traced.enable 1 перед первым использованием.

  • Размер трейса: 1 минута полной записи ≈ 50-200 МБ в зависимости от активности.

  • Root не нужен: Perfetto работает на нерутованных устройствах с Android 9+.

Где класть конфиг: на Android 12+ можно использовать /data/misc/perfetto-configs/ для обхода некоторых SELinux ограничений

SQL-анализ: главная киллер-фича Perfetto

Perfetto запускает собственный SQL-движок trace_processor (документация) с синтаксисом, похожим на SQLite. Вместо того, чтобы часами скроллить таймлайн в поисках лага, вы задаёте вопрос базе данных.

Кейс 1. Точное время холодного старта

Вместо приблизительных оценок, считаем миллисекунды от bindApplication до первого activityResume:

SELECT
    (SELECT ts FROM slice WHERE name LIKE '%activityResume%' LIMIT 1) -
    (SELECT ts FROM slice WHERE name = 'bindApplication' LIMIT 1)
AS startup_ns;

Результат: 1234567890 наносекунд → делим на 1e6 → получаем время в миллисекундах.

Например, если использовать ui.perfetto.dev, то этот процесс будет выглядеть так:

Кейс 2. Дропнутые кадры (Jank)

Находим все кадры Choreographer#doFrame, отрисовка которых заняла больше 16,6 мс:

SELECT
      ts/1e9 as time_sec,
      dur/1e6 as frame_ms,
      name
  FROM slice
  WHERE name LIKE 'Choreographer#doFrame%'
    AND dur > 16.6e6
  ORDER BY dur DESC;

Получаете таблицу. Она показывает, на какой секунде произошёл лаг, и сколько он длился:

Кейс 3. Поток готов, но процессор занят

Самая частая причина «необъяснимых» лагов — состояние потока. В таблице thread_state видно разницу:

  • Running — поток реально исполняется на ядре;

  • Runnable — поток готов работать, но ждёт очереди (CPU занят);

  • Uninterruptible Sleep (D) — поток заблокирован (обычно I/O операции).

Запрос для поиска моментов, когда Main Thread долго ждал CPU:

SELECT
      ts/1e9 as time_sec,
      dur/1e6 as wait_ms, 
      state
  FROM thread_state -- Из таблицы thread_state (история состояний потоков) 
-- ПОДЗАПРОС: находит utid главного потока приложения
  WHERE utid = (
      SELECT t.utid
      FROM thread t
      JOIN process p USING(upid)-- Объединяем с таблицей process по upid
      WHERE p.name = 'ru.dodopizza.app.beta' -- Ищем процесс вашего приложения 
        AND t.name LIKE '%pizza.app.beta%'-- Ищем поток, имя которого содержит часть имени пакета

      LIMIT 1
  )
  AND state = 'R' -- Runnable
  AND dur > 10e6 -- Больше 10 мс
  ORDER BY dur DESC; -- Сортируем 

В UI-интерфейсе Perfetto это выглядит так:

Чтобы увидеть общую картину и оценить масштаб проблемы, стоит выполнить поиск SQL-запросом. Так вы получите отфильтрованный список всех проблем.

Если таких моментов много, оптимизировать ваш Kotlin-код бесполезно. Ищите, что загружает CPU. Это могут быть другие процессы или фоновые задачи системы.

Автоматизация: скрипт вместо ручной работы

Анализировать трейсы через веб-интерфейс удобно, если вы изучаете сложные баги. Если же вам нужно быстро проверить, не нагрузили ли вы GC доработками перед отправкой кода на ревью, ручной процесс вас утомит.

Однако, у Perfetto и тут есть отличное решение! Если вы всё ещё не начали им пользоваться, то сейчас я зайду с козырей.

Полный Bash-скрипт

https://gist.github.com/grishan0v/9b88c5a53e576f58534442f1ae1e8888

Не забудьте при запуске дать ему права на выполнение chmod +x

Что делает скрипт?

  1. Убивает приложение (am force-stop) для гарантии холодного старта.

  2. Запускает запись Perfetto в фоновом режиме на 15 секунд.

  3. Включает atrace для вашего package.

  4. Стартует приложение через monkey. Это встроенная в Android утилита для UI-тестирования. Команда monkey -p com.your.app -c android.intent.category.LAUNCHER 1 отправляет ровно одно событие: запуск главной Activity приложения. По сути, это программная имитация тапа пользователя по иконке приложения на рабочем столе.

  5. Скачивает трейс с устройства.

  6. Анализирует через trace_processor  SQL-запросы к файлу без браузера.

  7. Генерирует отчёт report.txt с готовыми цифрами.

Вместо 15 минут ручной работы — 15 секунд ожидания.

Шаг 1: Установка trace_processor

# Download prebuilts (Linux and Mac only)
curl -LO https://get.perfetto.dev/trace_processor
chmod +x trace_processor

Шаг 2: Как работает SQL-анализ в скрипте

Ключевой фрагмент — функция выполнения SQL через pipe:

TRACE_PROCESSOR="./trace_processor"
LOCAL_TRACE="trace.perfetto-trace"
REPORT_FILE="report.txt"

# Функция выполнения SQL
run_sql() {
    echo "$1" | "$TRACE_PROCESSOR" "$LOCAL_TRACE" 2>/dev/null
}

# Пример использования
echo "--- Анализ Jank-фреймов (>16.6мс) ---" >> $REPORT_FILE

SQL_JANK="SELECT 
    COUNT(*) as bad_frames 
FROM slice 
WHERE name LIKE 'Choreographer#doFrame%' 
AND dur > 16666666;"

run_sql "$SQL_JANK" >> $REPORT_FILE

Как это работает:

  1. Вы передаете SQL-запрос через echo в trace_processor.

  2. Утилита выполняет запрос и возвращает результат в формате CSV (comma-separated values)

    bad_frames
       54
  3. Этот CSV-вывод дописывается в конец файла report.txt (оператор >> в Bash).

Файл report.txt содержит обычный текст + CSV-таблицы от trace_processor. Вы можете открыть его в любом текстовом редакторе или импортировать в Excel/Google Sheets.

Что вы получаете на выходе

Итоговый вид файла report.txt ниже. Если процент janky_frames вырос с 2% до 5% после вашего коммита, — вы узнаете об этом мгновенно.

--- СТАТИСТИКА ФРЕЙМОВ ---
total_frames,janky_frames,janky_percent
1200,54,4.5

--- ТОП-5 САМЫХ МЕДЛЕННЫХ ФРЕЙМОВ ---
time_sec,duration_ms
4.231,82.10
5.105,45.20
7.891,38.50
...

--- ДОЛГИЕ GC (>20мс) ---
name,              duration_ms
CollectorType GC,  52.3
HeapTrim,          31.8

Интеграция в CI/CD

Следующий шаг — автоматическая проверка производительности в пайплайне. После прогона UI-тестов в CI:

  1. Скрипт записывает трейс.

  2. Анализирует его через SQL.

  3. Сравнивает метрики с эталоном (baseline).

  4. Если время старта увеличилось на 10%, билд помечается как нестабильный.

Упрощённый пример проверки в CI (GitHub Actions). Он позволит ловить регрессии до того, как они попадут в main:

- name: Performance Check
  run: |
    ./perfetto_record.sh
    
    STARTUP_MS=$(cat report.txt | grep "startup_ms" | cut -d',' -f2)
    BASELINE_MS=850
    
    if [ $STARTUP_MS -gt $((BASELINE_MS * 110 / 100)) ]; then
      echo "❌ Регрессия производительности: $STARTUP_MS мс (baseline: $BASELINE_MS мс)"
      exit 1
    fi

Бонус: AI-агенты для анализа трейсов

Рубрика «Заставляем ИИ делать работу за нас»: вместо написания SQL вручную, используем большие языковые модели (LLM) для генерации запросов.

Как это работает:

  1. Получаем схему базы данных Perfetto — список всех таблиц и их колонок. Это можно сделать командой:

    echo ".schema" | trace_processor trace.perfetto-trace

    Вывод будет примерно таким:

    CREATE TABLE slice(id INT, ts INT, dur INT, name TEXT, ...);
       CREATE TABLE thread(id INT, name TEXT, tid INT, ...);
       CREATE TABLE thread_state(id INT, ts INT, dur INT, state TEXT, ...);
  2. Передаём схему + задачу в LLM (например, ChatGPT, Claude, Gemini):

    Ты — эксперт по анализу Perfetto трейсов. Вот схема базы данных:
    {schema}
    Задача: Найди в трейсе все моменты, 
    где Main Thread был в состоянии Runnable (ждал CPU) дольше 50 мс. 
    Выведи SQL-запрос и объясни, что может быть причиной.
  3. LLM генерирует SQL и объясняет логику:

    SQL-запрос:
    
    SELECT 
        ts/1e9 as time_sec,
        dur/1e6 as wait_ms,
        (SELECT name FROM thread WHERE id = thread_state.utid) as thread_name
    FROM thread_state
    WHERE state = 'R'
    AND dur > 50000000
    AND thread_name = 'main'
    ORDER BY dur DESC;
    
    Объяснение: 
    
    Состояние Runnable означает, что поток готов выполняться, 
    но ждет освобождения CPU. Возможные причины:
    - Высокая нагрузка от других процессов
    - Фоновые задачи системы
    - Недостаточная мощность процессора на устройстве
  4. Выполняем SQL — здесь два варианта:

    Вариант A. Вручную через trace_processor: копируете SQL от LLM и запускаете сами. Полный контроль над процессом.

    echo "SELECT ... FROM thread_state ..." | 
    trace_processor trace.perfetto-trace

    Вы копируете SQL от LLM и запускаете сами. Полный контроль над процессом.

    Вариант Б. LLM сама выполняет запрос

    Современные LLM могут сами запустить trace_processor и получить результаты. Вы просто загружаете трейс и пишете:

    «Проанализируй трейс (путь к трейсу), найди все моменты, где Main Thread ждал CPU больше 50 мс, и объясни, что происходило. Используй trace_processor (путь к нему, если LLM запускается не в том же пакете)»

    LLM сама сгенерирует SQL, выполнит его через trace_processor и проанализирует результаты.

Это особенно полезно для быстрого старта поиска проблемы. Вместо того, чтобы вручную изучать схему таблиц и писать запросы методом проб и ошибок, вы сразу получаете рабочую гипотезу и SQL для её проверки. Подходит как для быстрого прототипирования сложных аналитических запросов, так и для первичной диагностики незнакомых проблем производительности.

Что дальше

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

С чего начать:

  1. Запишите System Trace проблемного сценария в вашем приложении.

  2. Откройте файл на ui.perfetto.dev.

  3. Попробуйте выполнить один SQL-запрос из этой статьи.

  4. Адаптируйте скрипт под свой проект и пользуйтесь по необходимости.

Полезные ссылки:

Если вы используете Perfetto, расскажите в комментариях, как он решает ваши проблемы и помогает в разработке? Как часто вы используете его? Стал ли Perfetto одним из этапов разработки фичей?

А на этом всё. Спасибо, что дочитали статью! Чтобы оставаться в курсе последних новостей нашей команды, подпишитесь на Telegram-канал Dodo Engineering. В нём много клёвого контента о нашей команде, продуктах и культуре!