Понимание Systrace

systrace — это основной инструмент для анализа производительности Android-устройств. Однако на самом деле это оболочка для других инструментов. Это оболочка на стороне хоста для atrace , исполняемый файл на стороне устройства, который управляет трассировкой пользовательского пространства и настраивает ftrace , а также основной механизм трассировки в ядре Linux. systrace использует atrace для включения трассировки, затем считывает буфер ftrace и помещает все это в автономную программу просмотра HTML. (Хотя в более новых ядрах есть поддержка Linux Enhanced Berkeley Packet Filter (eBPF), следующая документация относится к ядру 3.18 (без eFPF), поскольку именно оно использовалось в Pixel/Pixel XL.)

systrace принадлежит командам Google Android и Google Chrome и является частью проекта Catapult с открытым исходным кодом. Помимо systrace, Catapult включает в себя и другие полезные утилиты. Например, ftrace имеет больше функций, чем могут быть непосредственно включены systrace или atrace, и содержит некоторые расширенные функции, которые имеют решающее значение для отладки проблем с производительностью. (Для этих функций требуется root-доступ и часто новое ядро.)

Запуск системной трассы

При отладке джиттера на Pixel/Pixel XL начните со следующей команды:

./systrace.py sched freq idle am wm gfx view sync binder_driver irq workq input -b 96000

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

Проходя через systrace, имейте в виду, что каждое событие запускается чем-то на ЦП .

Поскольку systrace построен поверх ftrace, а ftrace работает на ЦП, что-то на ЦП должно записывать буфер ftrace, который регистрирует аппаратные изменения. Это означает, что если вам интересно, почему экранное ограждение изменило состояние, вы можете увидеть, что выполнялось на ЦП в точной точке его перехода (что-то, работающее на ЦП, вызвало это изменение в журнале). Эта концепция лежит в основе анализа производительности с помощью systrace.

Пример: рабочая рама

В этом примере описывается системная трасса для обычного конвейера пользовательского интерфейса. Чтобы следовать примеру, загрузите ZIP-файл трассировок (который также включает другие трассировки, упомянутые в этом разделе), разархивируйте файл и откройте файл systrace_tutorial.html в браузере. Имейте в виду, что эта системная трасса представляет собой большой файл; если вы не используете systrace в своей повседневной работе, это, вероятно, гораздо более крупная трассировка с гораздо большим количеством информации, чем вы когда-либо видели в одной трассировке.

Для согласованной периодической рабочей нагрузки, такой как TouchLatency, конвейер пользовательского интерфейса содержит следующее:

  1. EventThread в SurfaceFlinger пробуждает поток пользовательского интерфейса приложения, сигнализируя о том, что пришло время отрисовать новый кадр.
  2. Приложение визуализирует кадр в потоке пользовательского интерфейса, RenderThread и hwuiTasks, используя ресурсы ЦП и ГП. Это основная часть ресурсов, затрачиваемых на пользовательский интерфейс.
  3. Приложение отправляет визуализированный кадр в SurfaceFlinger с помощью связующего, после чего SurfaceFlinger переходит в спящий режим.
  4. Второй EventThread в SurfaceFlinger пробуждает SurfaceFlinger для запуска композиции и отображения выходных данных. Если SurfaceFlinger определяет, что работы больше нет, он снова переходит в спящий режим.
  5. SurfaceFlinger обрабатывает композицию с помощью Hardware Composer (HWC)/Hardware Composer 2 (HWC2) или GL. Композиция HWC/HWC2 быстрее и потребляет меньше энергии, но имеет ограничения в зависимости от системы на кристалле (SoC). Обычно это занимает ~4-6 мс, но может совпадать с шагом 2, потому что приложения Android всегда имеют тройную буферизацию. (Хотя приложения всегда имеют тройную буферизацию, в SurfaceFlinger может быть только один ожидающий кадр, что делает его идентичным двойной буферизации.)
  6. SurfaceFlinger отправляет окончательный вывод для отображения с помощью драйвера поставщика и снова переходит в спящий режим, ожидая пробуждения EventThread.

Давайте пройдемся по кадру, начиная с 15409 мс:

Обычный конвейер пользовательского интерфейса с запущенным EventThread
Рис. 1. Обычный конвейер пользовательского интерфейса, запущен EventThread

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

  • серый . Спать.
  • Синий. Runnable (может работать, но планировщик еще не выбрал его для запуска).
  • Зеленый. Активно работает (планировщик думает, что он запущен).
  • Красный. Непрерывный сон (обычно сон на блокировке ядра). Может указывать на нагрузку ввода-вывода. Чрезвычайно полезно для отладки проблем с производительностью.
  • Апельсин. Непрерывный сон из-за нагрузки ввода-вывода.

Чтобы просмотреть причину непрерывного сна (доступную из точки sched_blocked_reason ), выберите красный фрагмент непрерывного сна.

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

Поток пользовательского интерфейса для TouchLatency
Рисунок 2. Поток пользовательского интерфейса для TouchLatency

На рис. 2 показано, что поток пользовательского интерфейса TouchLatency был разбужен tid 6843, что соответствует EventThread. Поток пользовательского интерфейса просыпается, визуализирует кадр и ставит его в очередь для использования SurfaceFlinger.

Поток пользовательского интерфейса пробуждается, визуализирует кадр и ставит его в очередь для использования SurfaceFlinger.
Рисунок 3. Поток пользовательского интерфейса пробуждается, визуализирует кадр и ставит его в очередь для использования SurfaceFlinger

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

Рисунок 4. Связующая транзакция

На рис. 4 показано, что через 15 423,65 мс Binder:6832_1 в SurfaceFlinger становится работоспособным из-за tid 9579, который является RenderThread TouchLatency. Вы также можете увидеть queueBuffer на обеих сторонах транзакции связывателя.

Во время queueBuffer на стороне SurfaceFlinger количество ожидающих кадров от TouchLatency увеличивается с 1 до 2.

Ожидающие кадры идут от 1 до 2
Рисунок 5. Ожидающие кадры идут от 1 до 2

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

Вскоре после этого основной поток SurfaceFlinger пробуждается вторым EventThread, чтобы он мог вывести на дисплей более старый ожидающий кадр:

Основной поток SurfaceFlinger пробуждается вторым EventThread
Рисунок 6. Основной поток SurfaceFlinger пробуждается вторым EventThread

SurfaceFlinger сначала фиксирует старый ожидающий буфер, что приводит к уменьшению количества ожидающих буферов с 2 до 1.

SurfaceFlinger сначала фиксируется на более старом ожидающем буфере
Рисунок 7. SurfaceFlinger сначала фиксируется на более старом ожидающем буфере

После фиксации буфера SurfaceFlinger настраивает композицию и отправляет последний кадр на дисплей. (Некоторые из этих разделов включены как часть точки mdss , поэтому они могут быть не включены в вашу SoC.)

SurfaceFlinger настраивает композицию и отправляет окончательный кадр
Рис. 8. SurfaceFlinger настраивает композицию и отправляет окончательный кадр

Затем mdss_fb0 пробуждается на ЦП 0. mdss_fb0 — это поток ядра конвейера отображения для вывода визуализированного кадра на дисплей. Мы можем видеть mdss_fb0 как отдельную строку в трассировке (прокрутите вниз, чтобы просмотреть).

mdss_fb0 просыпается на ЦП 0
Рисунок 9. mdss_fb0 пробуждается на ЦП 0

mdss_fb0 просыпается, работает ненадолго, переходит в непрерывный сон, затем снова просыпается.

Пример: нерабочий кадр

В этом примере описывается системная трасса, используемая для отладки джиттера Pixel/Pixel XL. Чтобы следовать примеру, загрузите zip-файл с трассировками (который включает в себя другие трассировки, упомянутые в этом разделе), разархивируйте файл и откройте файл systrace_tutorial.html в браузере.

Когда вы откроете системную трассу, вы увидите что-то вроде этого:

TouchLatency работает на Pixel XL с включенным большинством параметров
Рис. 10. TouchLatency в Pixel XL (большинство параметров включено, включая точки трассировки mdss и kgsl)

При поиске джанка проверьте строку FrameMissed в разделе SurfaceFlinger. FrameMissed — это улучшение качества жизни, предоставляемое HWC2. При просмотре systrace для других устройств строка FrameMissed может отсутствовать, если устройство не использует HWC2. В любом случае FrameMissed коррелирует с тем, что у SurfaceFlinger отсутствует одна из его чрезвычайно регулярных сред выполнения и неизменный счетчик ожидающих буферов для приложения ( com.prefabulated.touchlatency ) при вертикальной синхронизации.

Корреляция FrameMissed с SurfaceFlinger
Рисунок 11. Корреляция FrameMissed с SurfaceFlinger

На рис. 11 показан пропущенный кадр на 15598,29 мс. SurfaceFlinger ненадолго проснулся в интервале вертикальной синхронизации и снова заснул, не выполнив никакой работы, что означает, что SurfaceFlinger решил, что не стоит пытаться снова отправить кадр на дисплей. Почему?

Чтобы понять, как сломался конвейер для этого фрейма, сначала просмотрите приведенный выше пример рабочего фрейма, чтобы увидеть, как обычный конвейер пользовательского интерфейса отображается в systrace. Когда будете готовы, вернитесь к пропущенному кадру и работайте в обратном направлении. Обратите внимание, что SurfaceFlinger просыпается и сразу же переходит в спящий режим. При просмотре количества ожидающих кадров от TouchLatency есть два кадра (хороший ключ, помогающий понять, что происходит).

SurfaceFlinger просыпается и сразу засыпает
Рис. 12. SurfaceFlinger просыпается и тут же засыпает

Поскольку у нас есть фреймы в SurfaceFlinger, это не проблема приложения. Кроме того, SurfaceFlinger просыпается в нужное время, так что это не проблема SurfaceFlinger. Если SurfaceFlinger и приложение выглядят нормально, возможно, это проблема с драйвером.

Поскольку mdss и точки трассировки sync включены, мы можем получить информацию о барьерах (общих между драйвером дисплея и SurfaceFlinger), которые контролируют, когда кадры отправляются на дисплей. Эти ограничения перечислены в mdss_fb0_retire , что означает, когда кадр отображается на дисплее. Эти ограничения предоставляются как часть категории трассировки sync . Какие барьеры соответствуют конкретным событиям в SurfaceFlinger, зависит от вашего SOC и стека драйверов, поэтому обратитесь к поставщику SOC, чтобы понять значение категорий барьеров в ваших трассировках.

mdss_fb0_retire заборы
Рисунок 13. Заборы mdss_fb0_retire

На рис. 13 показан кадр, который отображался 33 мс, а не 16,7 мс, как ожидалось. В середине этого фрагмента этот кадр должен был быть заменен новым, но этого не произошло. Просмотрите предыдущий кадр и найдите что-нибудь.

Кадр, предшествующий сломанному кадру
Рис. 14. Кадр, предшествующий поврежденному кадру

На рис. 14 показан кадр длительностью 14,482 мс. Сломанный сегмент из двух кадров составил 33,6 мс, что примерно соответствует ожиданиям для двух кадров (мы рендерим с частотой 60 Гц, 16,7 мс на кадр, что близко). Но 14,482 мс вовсе не близко к 16,7 мс, что говорит о том, что что-то очень не так с конвейером дисплея.

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

Исследуйте конец забора
Рисунок 15. Исследуйте конец забора

Рабочая очередь содержит __vsync_retire_work_handler , который запускается при изменении границы. Просматривая исходный код ядра, вы можете увидеть, что это часть драйвера дисплея. Похоже, что он находится на критическом пути для конвейера отображения, поэтому он должен работать как можно быстрее. Его можно запустить на 70 человек или около того (небольшая задержка планирования), но это рабочая очередь, и ее расписание может быть неточным.

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

Предыдущий кадр
Рисунок 16. Предыдущий кадр

Выполняемая строка на kworker не видна, потому что средство просмотра делает ее белой, когда она выбрана, но статистика говорит об этом: 2,3 мс задержки планировщика для части критического пути конвейера отображения — это плохо . Прежде чем продолжить, исправьте задержку, переместив эту часть критического пути конвейера отображения из рабочей очереди (которая выполняется как поток SCHED_OTHER CFS) в выделенный поток SCHED_FIFO SCHED_FIFO. Эта функция требует временных гарантий, которые рабочие очереди не могут (и не предназначены) предоставить.

Это причина дергания? Трудно сказать окончательно. За исключением легко диагностируемых случаев, таких как конкуренция за блокировку ядра, приводящая к спячке критически важных для отображения потоков, трассировка обычно не указывает на проблему. Могло ли это дрожание быть причиной пропущенного кадра? Абсолютно. Время ограничения должно быть 16,7 мс, но оно совсем не близко к этому в кадрах, предшествующих пропущенному кадру. Учитывая, насколько тесно связан конвейер отображения, вполне возможно, что дрожание вокруг таймингов ограничения привело к пропущенному кадру.

В этом примере решение заключалось в преобразовании __vsync_retire_work_handler из рабочей очереди в выделенный kthread. Это привело к заметному улучшению джиттера и уменьшению рывков в тесте с прыгающим мячом. Последующие трассировки показывают временные ограничения, которые колеблются очень близко к 16,7 мс.