
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 решает эту проблему, объединяя три источника данных на одной временной шкале:
ftrace (ядро Linux) — когда ваш поток реально выкинули с процессора (событие sched_switch);
atrace (Android Framework) — события measure, layout, draw от системы;
Пользовательские трейсы — добавленные разработчиком трейсы бизнес-логики приложения.
Это как в чёрном ящике самолёта. Вы видите и действия пилота (кастомные трейсы), и показания датчиков системы: ветер, высоту, температуру двигателей (а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. Загружаем конфиг на устройство # Используйте /data/misc/perfetto-configs/ вместо /data/local/tmp/ # если возникают проблемы с правами доступа на некоторых устройствах 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
Что делает скрипт?
Убивает приложение (
am force-stop) для гарантии холодного старта.Запускает запись Perfetto в фоновом режиме на 15 секунд.
Включает atrace для вашего
package.Стартует приложение через monkey. Это встроенная в Android утилита для UI-тестирования. Команда
monkey -pcom.your.app-c android.intent.category.LAUNCHER 1отправляет ровно одно событие: запуск главнойActivityприложения. По сути, это программная имитация тапа пользователя по иконке приложения на рабочем столе.Скачивает трейс с устройства.
Анализирует через
trace_processorSQL-запросы к файлу без браузера.Генерирует отчёт
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
Как это работает:
Вы передаете SQL-запрос через
echoвtrace_processor.Утилита выполняет запрос и возвращает результат в формате CSV (
comma-separated values)bad_frames 54Этот 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:
Скрипт записывает трейс.
Анализирует его через SQL.
Сравнивает метрики с эталоном (baseline).
Если время старта увеличилось на 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) для генерации запросов.
Как это работает:
Получаем схему базы данных 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, ...);Передаём схему + задачу в LLM (например, ChatGPT, Claude, Gemini):
Ты — эксперт по анализу Perfetto трейсов. Вот схема базы данных: {schema} Задача: Найди в трейсе все моменты, где Main Thread был в состоянии Runnable (ждал CPU) дольше 50 мс. Выведи SQL-запрос и объясни, что может быть причиной.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. Возможные причины: - Высокая нагрузка от других процессов - Фоновые задачи системы - Недостаточная мощность процессора на устройствеВыполняем 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 превращает профайлинг из шаманства в инженерную дисциплину. Вы оперируете фактами с точностью до наносекунд, а не догадками.
С чего начать:
Запишите System Trace проблемного сценария в вашем приложении.
Откройте файл на ui.perfetto.dev.
Попробуйте выполнить один SQL-запрос из этой статьи.
Адаптируйте скрипт под свой проект и пользуйтесь по необходимости.
Полезные ссылки:
Если вы используете Perfetto, расскажите в комментариях, как он решает ваши проблемы и помог��ет в разработке? Как часто вы используете его? Стал ли Perfetto одним из этапов разработки фичей?
А на этом всё. Спасибо, что дочитали статью! Чтобы оставаться в курсе последних новостей нашей команды, подпишитесь на Telegram-канал Dodo Engineering. В нём много клёвого контента о нашей команде, продуктах и культуре!
