Новости ChatGPT

Perfetto в Android-разработке: когда профайлера уже недостаточно

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. В нём много клёвого контента о нашей команде, продуктах и культуре!