Выявление джанка, связанного с джиттером

Джиттер — это случайное поведение системы, препятствующее выполнению ощутимой работы. На этой странице описывается, как выявлять и устранять проблемы с дрожанием, связанные с джиттером.

Задержка планировщика потоков приложений

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

  • Случайный вспомогательный поток в приложении, вероятно, может без проблем задерживаться на много миллисекунд.
  • Поток пользовательского интерфейса приложения может выдерживать дрожание в 1–2 мс.
  • Драйвер kthreads, работающий как SCHED_FIFO, может вызвать проблемы, если они могут выполняться в течение 500 мкс перед запуском.

Время выполнения можно определить в systrace по синей полосе, предшествующей выполняемому сегменту потока. Время выполнения также можно определить по промежутку времени между событием sched_wakeup для потока и событием sched_switch , сигнализирующим о начале выполнения потока.

Потоки, которые выполняются слишком долго

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

  1. Используйте процессорные наборы, как описано в разделе Термическое регулирование .
  2. Увеличьте значение CONFIG_HZ.
    • Исторически сложилось так, что на платформах arm и arm64 было установлено значение 100. Однако это историческая случайность, и это не лучший вариант для интерактивных устройств. CONFIG_HZ=100 означает, что длительность jiffy составляет 10 мс, что означает, что балансировка нагрузки между процессорами может занять 20 мс (два jiffy). Это может значительно способствовать зависанию загруженной системы.
    • Последние устройства (Nexus 5X, Nexus 6P, Pixel и Pixel XL) поставляются с CONFIG_HZ=300. Это должно иметь незначительные затраты энергии при значительном улучшении времени работы. Если вы видите значительное увеличение энергопотребления или проблемы с производительностью после изменения CONFIG_HZ, вероятно, один из ваших драйверов использует таймер, основанный на необработанных мигах вместо миллисекунд и конвертирующих в миги. Обычно это легко исправить (см. патч , исправляющий проблемы с таймером kgsl на Nexus 5X и 6P при преобразовании в CONFIG_HZ=300).
    • Наконец, мы поэкспериментировали с CONFIG_HZ=1000 на Nexus/Pixel и обнаружили, что он обеспечивает заметное снижение производительности и энергопотребления из-за уменьшения нагрузки на RCU.

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

Использование sys.use_fifo_ui

Вы можете попытаться свести время выполнения потока пользовательского интерфейса к нулю, установив для свойства sys.use_fifo_ui значение 1.

Предупреждение . Не используйте этот параметр в гетерогенных конфигурациях ЦП, если у вас нет планировщика RT с учетом емкости. И, на данный момент, НИ ОДИН ИЗ ПОСТАВЛЯЕМЫХ В НАСТОЯЩЕЕ ВРЕМЯ ПЛАНИРОВЩИКОВ RT НЕ ПОДДЕРЖИВАЕТ ЕМКОСТЬ . Мы работаем над одним для EAS, но он пока недоступен. Планировщик RT по умолчанию основан исключительно на приоритетах RT и на том, есть ли у ЦП поток RT с таким же или более высоким приоритетом.

В результате планировщик RT по умолчанию с радостью переместит ваш относительно продолжительный поток пользовательского интерфейса с высокочастотного большого ядра на маленькое ядро ​​с минимальной частотой, если произойдет пробуждение k-потока FIFO с более высоким приоритетом на том же большом ядре. Это приведет к значительному снижению производительности . Поскольку эта опция еще не использовалась на выпускаемом Android-устройстве, если вы хотите ее использовать, свяжитесь с командой разработчиков Android, чтобы помочь вам проверить ее.

Когда sys.use_fifo_ui включен, ActivityManager отслеживает поток пользовательского интерфейса и RenderThread (два наиболее важных для пользовательского интерфейса потока) основного приложения и делает эти потоки SCHED_FIFO вместо SCHED_OTHER. Это эффективно устраняет дрожание пользовательского интерфейса и RenderThreads; трассировки, которые мы собрали с включенной этой опцией, показывают время выполнения порядка микросекунд, а не миллисекунд.

Однако, поскольку балансировщик нагрузки RT не учитывал емкость, производительность запуска приложений снизилась на 30 %, поскольку поток пользовательского интерфейса, отвечающий за запуск приложения, был перемещен с ядра Gold Kryo с частотой 2,1 ГГц на ядро ​​Kryo Silver с частотой 1,5 ГГц. . С балансировщиком нагрузки RT с учетом емкости мы видим эквивалентную производительность при массовых операциях и сокращение времени кадра 95-го и 99-го процентилей на 10-15% во многих наших тестах пользовательского интерфейса.

Прерывание трафика

Поскольку платформы ARM по умолчанию доставляют прерывания только ЦП 0, мы рекомендуем использовать балансировщик IRQ (irqbalance или msm_irqbalance на платформах Qualcomm).

Во время разработки Pixel мы видели сбои, которые могли быть напрямую связаны с перегрузкой ЦП 0 прерываниями. Например, если поток mdss_fb0 был запланирован на ЦП 0, вероятность рывка была гораздо выше из-за прерывания, которое инициируется дисплеем почти непосредственно перед сканированием. mdss_fb0 был бы в середине своей работы с очень сжатыми сроками, а затем потерял бы некоторое время обработчику прерывания MDSS. Сначала мы попытались исправить это, установив привязку ЦП потока mdss_fb0 к ЦП 1-3, чтобы избежать конкуренции с прерыванием, но потом поняли, что еще не включили msm_irqbalance. С включенным msm_irqbalance jank заметно улучшился, даже когда и mdss_fb0, и прерывание MDSS находились на одном и том же ЦП из-за уменьшения конкуренции со стороны других прерываний.

Это можно определить в systrace, просмотрев раздел sched и раздел irq. Раздел sched показывает, что было запланировано, но перекрывающаяся область в разделе irq означает, что в это время выполняется прерывание, а не обычно запланированный процесс. Если вы видите значительные промежутки времени, занимаемые во время прерывания, ваши варианты включают в себя:

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

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

Длинные отложенные прерывания

Во время работы softirq отключает вытеснение. softirqs также могут запускаться во многих местах ядра и выполняться внутри пользовательского процесса. Если активности softirq достаточно, пользовательские процессы перестанут запускать softirq, а ksoftirqd просыпается для запуска softirq и балансировки нагрузки. Обычно это нормально. Однако одно очень длинное прерывание может нанести вред системе.


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

Драйверы оставляют вытеснение или IRQ отключены слишком долго

Отключение вытеснения или прерываний на слишком долгое время (десятки миллисекунд) приводит к рывкам. Как правило, джанк проявляется в том, что поток становится работоспособным, но не работает на конкретном ЦП, даже если работоспособный поток имеет значительно более высокий приоритет (или SCHED_FIFO), чем другой поток.

Некоторые рекомендации:

  • Если исполняемый поток — SCHED_FIFO, а работающий поток — SCHED_OTHER, у работающего потока отключены вытеснение или прерывания.
  • Если исполняемый поток имеет значительно более высокий приоритет (100), чем работающий поток (120), у выполняющегося потока, вероятно, отключено вытеснение или прерывания, если исполняемый поток не запускается в течение двух мгновений.
  • Если исполняемый поток и работающий поток имеют одинаковый приоритет, работающий поток, вероятно, имеет вытеснение или прерывания отключены, если исполняемый поток не запускается в течение 20 мс.

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


Еще один вариант выявления проблемных регионов — с помощью трассировщика preemptirqsoff (см. Использование динамического ftrace ). Этот трассировщик может дать гораздо более полное представление об основной причине непрерываемой области (например, именах функций), но для включения требуется более инвазивная работа. Хотя это может иметь большее влияние на производительность, его определенно стоит попробовать.

Неправильное использование рабочих очередей

Обработчикам прерываний часто приходится выполнять работу, которая может выполняться вне контекста прерывания, что позволяет распределять работу между различными потоками в ядре. Разработчик драйвера может заметить, что ядро ​​имеет очень удобную общесистемную функциональность асинхронных задач, называемую рабочими очередями , и может использовать ее для работы, связанной с прерываниями.

Однако рабочие очереди почти всегда являются неправильным решением этой проблемы, потому что они всегда SCHED_OTHER. Многие аппаратные прерывания находятся на критическом пути производительности и должны запускаться немедленно. Рабочие очереди не имеют никаких гарантий относительно того, когда они будут запущены. Каждый раз, когда мы видели рабочую очередь на критическом пути производительности, она становилась источником спорадических помех, независимо от устройства. На Pixel с флагманским процессором мы увидели, что одна рабочая очередь может задерживаться до 7 мс, если устройство находится под нагрузкой, в зависимости от поведения планировщика и других вещей, работающих в системе.

Вместо рабочей очереди драйверы, которым необходимо обрабатывать работу, подобную прерыванию, внутри отдельного потока, должны создавать свой собственный k-поток SCHED_FIFO. Чтобы сделать это с помощью функций kthread_work, обратитесь к этому патчу .

Конфликт за блокировку фреймворка

Конфликт за блокировку фреймворка может быть источником помех или других проблем с производительностью. Обычно это вызвано блокировкой ActivityManagerService, но ее можно увидеть и в других блокировках. Например, блокировка PowerManagerService может повлиять на производительность экрана. Если вы видите это на своем устройстве, хорошего решения нет, потому что его можно улучшить только за счет архитектурных улучшений фреймворка. Однако, если вы изменяете код, который выполняется внутри system_server, очень важно избегать удержания блокировок в течение длительного времени, особенно блокировки ActivityManagerService.

Конфликт за блокировку связующего

Исторически сложилось так, что связующее устройство имело единую глобальную блокировку. Если поток, выполняющий транзакцию связывателя, был вытеснен при удержании блокировки, ни один другой поток не сможет выполнить транзакцию связывателя, пока исходный поток не освободит блокировку. Это плохо; Конфликт связывателя может заблокировать все в системе, включая отправку обновлений пользовательского интерфейса на дисплей (потоки пользовательского интерфейса взаимодействуют с SurfaceFlinger через связыватель).

Android 6.0 включает несколько исправлений для улучшения этого поведения путем отключения вытеснения при удержании блокировки привязки. Это было безопасно только потому, что блокировку связывателя нужно было удерживать в течение нескольких микросекунд фактического времени выполнения. Это значительно повысило производительность в неконкурентных ситуациях и предотвратило конкуренцию, предотвратив большинство переключений планировщика, пока удерживалась блокировка привязки. Однако вытеснение нельзя было отключить на все время выполнения удержания блокировки связывателя, а это означает, что вытеснение было включено для функций, которые могли спать (например, copy_from_user), что могло привести к тому же вытеснению, что и в исходном случае. Когда мы разослали патчи вверх по течению, нам тут же сказали, что это худшая идея в истории. (Мы согласились с ними, но мы также не могли поспорить с эффективностью исправлений для предотвращения зависаний.)

Конфликт fd внутри процесса

Это редкость. Ваш рывок, вероятно, не вызван этим.

Тем не менее, если у вас есть несколько потоков в процессе, записывающем один и тот же fd, можно увидеть конкуренцию за этот fd, однако единственный раз, когда мы видели это во время запуска Pixel, — это во время теста, когда потоки с низким приоритетом пытались занять весь ЦП. время, пока один высокоприоритетный поток выполнялся в том же процессе. Все потоки выполняли запись в маркер трассировки fd, и поток с высоким приоритетом мог быть заблокирован на маркере трассировки fd, если поток с низким приоритетом удерживал блокировку fd и затем был вытеснен. Когда трассировка была отключена для потоков с низким приоритетом, проблем с производительностью не возникало.

Нам не удалось воспроизвести это в какой-либо другой ситуации, но это стоит указать как потенциальную причину проблем с производительностью при трассировке.

Ненужные переходы бездействия ЦП

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

  1. Поток A работает на ЦП 1.
  2. Поток A пробуждает поток B.
  3. Поток B начинает работать на ЦП 2.
  4. Поток A немедленно засыпает, чтобы быть разбуженным потоком B, когда поток B завершит свою текущую работу.

Общий источник накладных расходов находится между шагами 2 и 3. Если ЦП 2 простаивает, его необходимо вернуть в активное состояние, прежде чем поток B сможет запуститься. В зависимости от SOC и глубины простоя это может занять несколько десятков микросекунд, прежде чем поток B начнет работать. Если фактическое время выполнения каждой стороны IPC достаточно близко к накладным расходам, общая производительность этого конвейера может быть значительно снижена из-за переходов бездействия ЦП. Наиболее распространенное место, где Android сталкивается с этим, связано с транзакциями связывателя, и многие сервисы, использующие связыватель, в конечном итоге выглядят как ситуация, описанная выше.

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

Во-вторых, убедитесь, что время перехода вашего процессора реалистично, и регулятор процессора правильно его учитывает. Если ваш SOC постоянно находится в состоянии глубокого бездействия и выходит из него, вы не сэкономите энергию, перейдя в состояние глубокого бездействия.

логирование

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

Проблемы с вводом/выводом

Операции ввода-вывода являются распространенными источниками джиттера. Если поток обращается к отображаемому в память файлу, а страницы нет в кэше страниц, происходит сбой и чтение страницы с диска. Это блокирует поток (обычно на 10+ мс) и, если это происходит на критическом пути рендеринга пользовательского интерфейса, может привести к зависанию. Существует слишком много причин операций ввода-вывода, чтобы обсуждать их здесь, но проверьте следующие места, пытаясь улучшить поведение ввода-вывода:

  • ПиннерСервис . PinnerService, добавленный в Android 7.0, позволяет платформе блокировать некоторые файлы в кэше страниц. Это удаляет память для использования любым другим процессом, но если есть некоторые файлы, о которых заранее известно, что они используются регулярно, может быть эффективно заблокировать эти файлы.

    На устройствах Pixel и Nexus 6P под управлением Android 7.0 мы заблокировали четыре файла:
    • /system/framework/arm64/boot-framework.oat
    • /система/фреймворк/овес/arm64/services.odex
    • /система/каркас/arm64/boot.oat
    • /system/framework/arm64/boot-core-libart.oat
    Эти файлы постоянно используются большинством приложений и system_server, поэтому их не следует выгружать. В частности, мы обнаружили, что если какие-либо из них будут выгружены, они будут выгружены обратно и вызовут зависание при переключении с тяжеловесного приложения.
  • Шифрование . Другая возможная причина проблем с вводом-выводом. Мы считаем, что встроенное шифрование обеспечивает наилучшую производительность по сравнению с шифрованием на основе ЦП или использованием аппаратного блока, доступного через DMA. Самое главное, встроенное шифрование снижает дрожание, связанное с вводом-выводом, особенно по сравнению с шифрованием на основе ЦП. Поскольку выборка в кэш страницы часто находится на критическом пути рендеринга пользовательского интерфейса, шифрование на основе ЦП создает дополнительную нагрузку на ЦП на критическом пути, что добавляет больше дрожания, чем просто выборка ввода-вывода.

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

Агрессивная упаковка небольших задач

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

Переполнение кеша страниц

Устройство без достаточного количества свободной памяти может внезапно стать очень медленным при выполнении длительной операции, такой как открытие нового приложения. Трассировка приложения может показать, что оно постоянно блокируется при вводе-выводе во время определенного запуска, даже если оно часто не блокируется при вводе-выводе. Обычно это признак перегрузки кеша страниц, особенно на устройствах с меньшим объемом памяти.

Один из способов определить это — взять системную трассировку с помощью тега pagecache и передать эту трассировку сценарию в system/extras/pagecache/pagecache.py . pagecache.py переводит отдельные запросы на отображение файлов в кеш страниц в совокупную статистику по файлам. Если вы обнаружите, что было прочитано больше байтов файла, чем общий размер этого файла на диске, вы определенно столкнулись с перегрузкой кеша страниц.

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

Не существует надежного способа исправить перегрузку кэша страниц, но есть несколько способов улучшить это на данном устройстве.

  • Используйте меньше памяти в постоянных процессах. Чем меньше памяти используется постоянными процессами, тем больше памяти доступно приложениям и кэшу страниц.
  • Проверьте вырезки, которые у вас есть для вашего устройства, чтобы убедиться, что вы не удаляете память из ОС без необходимости. Мы видели ситуации, когда вырезки, используемые для отладки, случайно оставлялись в поставляемых конфигурациях ядра, потребляя десятки мегабайт памяти. Это может иметь значение между попаданием в кэш страниц и нет, особенно на устройствах с меньшим объемом памяти.
  • Если вы видите перегрузку кеша страниц в system_server для критических файлов, подумайте о том, чтобы закрепить эти файлы. Это увеличит нагрузку на память в другом месте, но может изменить поведение в достаточной степени, чтобы избежать перегрузки.
  • Перенастройте lowmemorykiller, чтобы попытаться освободить больше памяти. Пороги lowmemorykiller основаны как на абсолютной свободной памяти, так и на кэше страниц, поэтому увеличение порога, при котором процессы с заданным уровнем oom_adj уничтожаются, может привести к улучшению поведения за счет увеличения числа случаев смерти фоновых приложений.
  • Попробуйте использовать ZRAM. Мы используем ZRAM на Pixel, хотя у Pixel 4 ГБ, потому что это может помочь с редко используемыми грязными страницами.