Джиттер — это случайное поведение системы, которое препятствует выполнению заметной работы. На этой странице описывается, как выявлять и устранять проблемы, связанные с джиттером.
Задержка планировщика потоков приложения
Задержка планировщика является наиболее очевидным признаком дрожания: процесс, который должен быть запущен, становится работоспособным, но не выполняется в течение значительного периода времени. Значение задержки варьируется в зависимости от контекста. Например:
- Случайный вспомогательный поток в приложении, вероятно, может быть задержан на несколько миллисекунд без каких-либо проблем.
- Поток пользовательского интерфейса приложения может выдерживать дрожание в 1–2 мс.
- Драйвер kthreads, работающий как SCHED_FIFO, может вызвать проблемы, если он может работать в течение 500 мкс перед запуском.
Время выполнения можно определить в systrace по синей полосе, предшествующей работающему сегменту потока. Время выполнения также можно определить по промежутку времени между событием sched_wakeup
для потока и событием sched_switch
, которое сигнализирует о начале выполнения потока.
Темы, которые выполняются слишком долго
Потоки пользовательского интерфейса приложения, которые выполняются слишком долго, могут вызвать проблемы. Потоки нижнего уровня с длительным временем выполнения обычно имеют разные причины, но попытка свести время выполнения потока пользовательского интерфейса к нулю может потребовать исправления некоторых из тех же проблем, которые приводят к увеличению времени выполнения потоков более низкого уровня. Чтобы минимизировать задержки:
- Используйте процессоры, как описано в разделе «Тепловое регулирование» .
- Увеличьте значение CONFIG_HZ.
- Исторически на платформах Arm и Arm64 значение было установлено равным 100. Однако это историческая случайность, и ее нельзя использовать для интерактивных устройств. CONFIG_HZ=100 означает, что длительность одного мига составляет 10 мс, а это означает, что балансировка нагрузки между процессорами может занять 20 мс (два мига). Это может существенно способствовать зависанию загруженной системы.
- Последние устройства (Nexus 5X, Nexus 6P, Pixel и Pixel XL) поставлялись с CONFIG_HZ=300. Это должно иметь незначительные затраты на электроэнергию, но при этом значительно улучшить время работы. Если после изменения CONFIG_HZ вы видите значительное увеличение энергопотребления или проблемы с производительностью, вероятно, один из ваших драйверов использует таймер, основанный на необработанных джиффах, а не на миллисекундах, и конвертирует их в джиффи. Обычно это легко исправить (см. патч , исправляющий проблемы с таймером кгсл на 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 %, поскольку поток пользовательского интерфейса, отвечающий за запуск приложения, был перенесен с золотого ядра Kryo с частотой 2,1 ГГц на серебряное ядро Kryo с частотой 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 джанк заметно улучшился, даже когда и mdss_fb0, и прерывание MDSS находились на одном процессоре из-за уменьшения конкуренции со стороны других прерываний.
Это можно определить в systrace, просмотрев раздел sched, а также раздел irq. В разделе sched показано, что было запланировано, но перекрывающаяся область в разделе irq означает, что в это время вместо обычного запланированного процесса выполняется прерывание. Если вы видите, что во время прерывания затрачиваются значительные отрезки времени, ваши варианты включают в себя:
- Сделайте обработчик прерываний быстрее.
- В первую очередь предотвратите прерывание.
- Измените частоту прерывания, чтобы она не совпадала по фазе с другой регулярной работой, которой оно может мешать (если это обычное прерывание).
- Установите привязку прерывания к процессору напрямую и предотвратите его балансировку.
- Установите привязку процессора к потоку, которому мешает прерывание, чтобы избежать прерывания.
- Положитесь на балансировщик прерываний, чтобы переместить прерывание на менее загруженный процессор.
Установка привязки к ЦП обычно не рекомендуется, но может быть полезна в определенных случаях. В общем, слишком сложно предсказать состояние системы для большинства распространенных прерываний, но если у вас есть очень специфический набор условий, который запускает определенные прерывания, когда система более ограничена, чем обычно (например, VR), явная привязка ЦП может быть хорошим решением.
Длинные мягкие прерывания
Пока работает softirq, оно отключает вытеснение. Softirqs также может запускаться во многих местах ядра и запускаться внутри пользовательского процесса. Если активности softirq достаточно, пользовательские процессы прекратят выполнение softirq, а ksoftirqd просыпается для запуска softirq и балансировки нагрузки. Обычно это нормально. Однако одно очень длинное SoftIRQ может нанести ущерб системе.
Softirqs видны в разделе irq трассировки, поэтому их легко обнаружить, если проблема может быть воспроизведена во время трассировки. Поскольку softirq может выполняться внутри пользовательского процесса, плохой softirq может также проявляться как дополнительное время выполнения внутри пользовательского процесса без видимой причины. Если вы это видите, проверьте раздел прерываний, чтобы узнать, виноваты ли программные прерывания.
Драйверы слишком долго отключают приоритетное прерывание или прерывания
Отключение вытеснения или прерываний на слишком долгое время (десятки миллисекунд) приводит к зависаниям. Обычно затор проявляется в том, что поток становится работоспособным, но не работает на конкретном процессоре, даже если работоспособный поток имеет значительно более высокий приоритет (или 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), что могло вызвать то же вытеснение, что и в исходном случае. Когда мы отправили патчи в исходную версию, нам сразу сказали, что это худшая идея в истории. (Мы согласились с ними, но мы также не могли спорить с эффективностью исправлений в предотвращении зависаний.)
устранение разногласий внутри процесса
Это редкость. Вероятно, ваш рывок вызван не этим.
Тем не менее, если у вас есть несколько потоков в процессе, записывающих один и тот же файловый диск, можно увидеть конфликт на этом файловом диске, однако единственный раз, когда мы видели это во время запуска Pixel, — это во время теста, когда потоки с низким приоритетом пытались занять весь процессор. время, пока в одном процессе выполнялся один поток с высоким приоритетом. Все потоки записывали данные в маркер трассировки fd, и поток с высоким приоритетом мог быть заблокирован на маркере трассировки fd, если поток с низким приоритетом удерживал блокировку fd и затем был вытеснен. Когда трассировка была отключена для потоков с низким приоритетом, проблем с производительностью не возникало.
Нам не удалось воспроизвести это ни в одной другой ситуации, но на это стоит указать как на потенциальную причину проблем с производительностью во время трассировки.
Ненужные переходы бездействия ЦП
При работе с IPC, особенно с многопроцессными конвейерами, часто можно увидеть вариации следующего поведения во время выполнения:
- Поток A выполняется на процессоре 1.
- Поток А пробуждает поток Б.
- Поток B начинает работать на ЦП 2.
- Поток 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
- /system/framework/oat/arm64/services.odex
- /system/framework/arm64/boot.oat
- /system/framework/arm64/boot-core-libart.oat
- Шифрование . Другая возможная причина проблем ввода-вывода. Мы обнаружили, что встроенное шифрование обеспечивает лучшую производительность по сравнению с шифрованием на базе ЦП или использованием аппаратного блока, доступного через DMA. Самое главное, что встроенное шифрование снижает дрожание, связанное с вводом-выводом, особенно по сравнению с шифрованием на базе ЦП. Поскольку выборка в кэш страниц часто находится на критическом пути рендеринга пользовательского интерфейса, шифрование на базе ЦП создает дополнительную нагрузку на ЦП на критическом пути, что приводит к большему дрожанию, чем просто выборка ввода-вывода.
Механизмы аппаратного шифрования на основе DMA сталкиваются с аналогичной проблемой, поскольку ядру приходится тратить циклы на управление этой работой, даже если доступна для выполнения другая важная работа. Мы настоятельно рекомендуем любому поставщику SOC, создающему новое оборудование, включать поддержку встроенного шифрования.
Агрессивная упаковка мелких задач
Некоторые планировщики предлагают поддержку упаковки небольших задач в одно ядро ЦП, чтобы попытаться снизить энергопотребление за счет более длительного бездействия большего количества ЦП. Хотя это хорошо влияет на пропускную способность и энергопотребление, это может привести к катастрофическим последствиям для задержек. На критическом пути рендеринга пользовательского интерфейса есть несколько кратковременных потоков, которые можно считать небольшими; если эти потоки задерживаются из-за медленной миграции на другие процессоры, это приведет к зависанию. Мы рекомендуем использовать упаковку небольших задач очень консервативно.
Перегрузка кэша страниц
Устройство без достаточного количества свободной памяти может внезапно стать чрезвычайно медленным при выполнении длительной операции, например при открытии нового приложения. Трассировка приложения может показать, что оно постоянно блокируется при вводе-выводе во время определенного запуска, даже если оно часто не блокируется при вводе-выводе. Обычно это признак перегрузки страничного кэша, особенно на устройствах с меньшим объемом памяти.
Один из способов определить это — взять системную трассировку с помощью тега pagecache и передать эту трассировку сценарию по адресу system/extras/pagecache/pagecache.py
. pagecache.py преобразует отдельные запросы на отображение файлов в кэш страниц в совокупную статистику по файлам. Если вы обнаружите, что было прочитано больше байтов файла, чем общий размер этого файла на диске, вы определенно столкнулись с перегрузкой страничного кэша.
Это означает, что рабочий набор, необходимый для вашей рабочей нагрузки (обычно одно приложение плюс system_server), превышает объем памяти, доступной для кэша страниц на вашем устройстве. В результате, когда одна часть рабочей нагрузки получает необходимые ей данные в кэше страниц, другая часть, которая будет использоваться в ближайшем будущем, будет вытеснена, и ее придется извлекать снова, в результате чего проблема возникнет снова, пока загрузка не завершится. завершилось. Это основная причина проблем с производительностью, когда на устройстве недостаточно памяти.
Не существует надежного способа исправить перегрузку кэша страниц, но есть несколько способов попытаться улучшить эту ситуацию на конкретном устройстве.
- Используйте меньше памяти в постоянных процессах. Чем меньше памяти используется постоянными процессами, тем больше памяти доступно приложениям и кэшу страниц.
- Проверьте все ограничения, предусмотренные для вашего устройства, чтобы убедиться, что вы не удаляете память из ОС без необходимости. Мы видели ситуации, когда вырезки, используемые для отладки, случайно оставлялись в поставляемых конфигурациях ядра, занимая десятки мегабайт памяти. Это может сыграть решающую роль в перегрузке кэша страниц или нет, особенно на устройствах с меньшим объемом памяти.
- Если вы видите сбои в кэше страниц в system_server для важных файлов, рассмотрите возможность закрепления этих файлов. Это увеличит нагрузку на память в других местах, но может изменить поведение в достаточной степени, чтобы избежать сбоев.
- Перенастройте lowmemorykiller, чтобы попытаться сохранить больше памяти свободной. Пороговые значения lowmemorykiller основаны как на абсолютно свободной памяти, так и на страничном кеше, поэтому увеличение порога, при котором процессы на данном уровне oom_adj завершаются, может привести к улучшению поведения за счет увеличения смертности фоновых приложений.
- Попробуйте использовать ZRAM. Мы используем ZRAM на Pixel, хотя у Pixel 4 ГБ, потому что это может помочь с редко используемыми грязными страницами.