Джиттер – это случайное поведение системы, которое не позволяет выполнять заметную работу. На этой странице рассказывается, как выявлять и устранять проблемы с рывками, связанные с дрожанием.
Задержка планировщика потоков приложения
Задержка планировщика – самый очевидный симптом дрожания. Процесс, который должен быть запущен, становится доступным для запуска, но не запускается в течение значительного времени. Значимость задержки зависит от контекста. Пример:
- Вспомогательный поток в приложении можно задержать на несколько миллисекунд без каких-либо проблем.
- Поток UI приложения может выдерживать джиттер в 1–2 мс.
- Если потоки ядра драйвера, работающие как SCHED_FIFO, могут быть запущены в течение 500 мкс до запуска, это может привести к проблемам.
В systrace время выполнения можно определить по синей полосе, предшествующей выполняемому сегменту потока. Время выполнения также можно определить по продолжительности времени между событием sched_wakeup для потока и событием sched_switch, которое сигнализирует о начале выполнения потока.
Слишком длинные цепочки писем
Потоки UI приложения, которые слишком долго находятся в состоянии готовности к выполнению, могут вызвать проблемы. У потоков более низкого уровня с длительным временем выполнения обычно другие причины, но попытка свести время выполнения потока UI к нулю может потребовать устранения тех же проблем, которые приводят к длительному времени выполнения потоков более низкого уровня. Чтобы уменьшить задержки:
- Используйте cpusets, как описано в разделе Термическое регулирование.
- Увеличьте значение CONFIG_HZ.
- Ранее на платформах ARM и ARM64 по умолчанию было установлено значение 100. Однако это историческая случайность, и для интерактивных устройств это значение не подходит. CONFIG_HZ=100 означает, что джиффи длится 10 мс, а балансировка нагрузки между процессорами может занимать 20 мс (два джиффи). Это может привести к значительным задержкам в работе системы.
- На новых устройствах (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.
Благодаря этим двум изменениям время выполнения потока UI на устройстве должно значительно улучшиться.
Используйте sys.use_fifo_ui
Вы можете попытаться свести время выполнения потока UI к нулю, задав для свойства sys.use_fifo_ui значение 1.
Предупреждение. Не используйте этот параметр в конфигурациях с разнородными ЦП, если у вас нет планировщика реального времени с учетом емкости.
В настоящее время НИ ОДИН ИЗ ДОСТУПНЫХ ПЛАНИРОВЩИКОВ RT НЕ УЧИТЫВАЕТ ОГРАНИЧЕНИЯ ПРОИЗВОДИТЕЛЬНОСТИ. Мы работаем над аналогичным инструментом для EAS, но он пока недоступен. Планировщик реального времени по умолчанию основан исключительно на приоритетах реального времени и на том, есть ли у ЦП поток реального времени с равным или более высоким приоритетом.
В результате планировщик RT по умолчанию с радостью перенесет относительно долго работающий поток UI с высокочастотного большого ядра на маленькое ядро с минимальной частотой, если на том же большом ядре проснется поток FIFO kthread с более высоким приоритетом. Это приведет к значительному снижению производительности. Поскольку этот вариант ещё не использовался на устройствах Android, свяжитесь с командой Android Performance, чтобы проверить его.
Если параметр sys.use_fifo_ui включен, ActivityManager отслеживает поток UI и поток рендеринга (два наиболее важных потока UI) верхнего приложения и делает эти потоки SCHED_FIFO вместо SCHED_OTHER. Это позволяет устранить дрожание в потоках отрисовки и интерфейса. В трассировках, собранных с включенным параметром, время выполнения составляет микросекунды, а не миллисекунды.
Однако из-за того, что балансировщик нагрузки RT не учитывал емкость, производительность при запуске приложения снизилась на 30 %, поскольку поток UI, отвечающий за запуск приложения, был перенесен с золотого ядра Kryo с частотой 2,1 ГГц на серебряное ядро Kryo с частотой 1,5 ГГц. При использовании балансировщика нагрузки с учетом емкости мы наблюдаем эквивалентную производительность при выполнении массовых операций и снижение времени формирования кадра на 10–15 % (95-й и 99-й процентили) во многих наших тестах интерфейса.
Прерывание трафика
Поскольку платформы 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 вытеснение запрещено. Кроме того, softirq можно запускать в разных местах ядра и в пользовательском процессе. Если активность softirq достаточно высока, пользовательские процессы перестанут запускать softirq, а ksoftirqd будет запускать softirq и балансировать нагрузку. Обычно это не проблема. Однако один очень длинный softirq может нанести системе серьезный ущерб.
Программные прерывания можно увидеть в разделе irq трассировки, поэтому их легко обнаружить, если проблему можно воспроизвести во время трассировки. Поскольку softirq может выполняться в пользовательском процессе, неисправность softirq может проявляться как дополнительное время выполнения в пользовательском процессе без очевидной причины. Если вы видите это, проверьте раздел irq, чтобы узнать, не вызваны ли проблемы softirq.
Драйверы, которые слишком долго не разрешают вытеснение или запросы на прерывание
Если отключить прерывания на слишком долгое время (десятки миллисекунд), изображение будет дергаться. Как правило, временное зависание проявляется в том, что поток становится готовым к выполнению, но не выполняется на определенном ЦП, даже если его приоритет значительно выше (или он имеет тип SCHED_FIFO), чем у другого потока.
Примечания
- Если выполняемый поток – SCHED_FIFO, а выполняющийся – SCHED_OTHER, то у выполняющегося потока отключено вытеснение или прерывания.
- Если приоритет потока, готового к выполнению (100), значительно выше приоритета выполняемого потока (120), то, скорее всего, у выполняемого потока отключено вытеснение или прерывания, если поток, готовый к выполнению, не запускается в течение двух тиков.
- Если у готового к выполнению и выполняющегося потоков одинаковый приоритет, то, скорее всего, у выполняющегося потока отключено вытеснение или прерывания, если готовый к выполнению поток не запускается в течение 20 мс.
Помните, что запуск обработчика прерываний не позволяет обслуживать другие прерывания, что также отключает вытеснение.
Ещё один способ выявить проблемные регионы – использовать трассировщик preemptirqsoff (см. раздел Использование динамической трассировки ftrace). Этот трассировщик позволяет получить более подробную информацию о первопричине непрерываемого региона (например, названия функций), но для его включения требуется более инвазивное вмешательство. Хотя это может повлиять на производительность, попробовать стоит.
Неправильное использование рабочих очередей
Обработчикам прерываний часто приходится выполнять задачи, которые могут выполняться вне контекста прерывания, что позволяет распределять задачи между разными потоками в ядре. Разработчик драйвера может заметить, что в ядре есть очень удобная общесистемная функция асинхронных задач, которая называется очередью задач, и использовать ее для работы, связанной с прерываниями.
Однако для этой задачи очереди рабочих процессов почти всегда не подходят, поскольку они всегда имеют тип SCHED_OTHER. Многие аппаратные прерывания находятся на критическом пути производительности и должны выполняться немедленно. Нет никаких гарантий, что очередь будет выполнена. Каждый раз, когда мы видели рабочую очередь в критическом пути производительности, она была источником спорадических задержек, независимо от устройства. На устройстве Pixel с флагманским процессором мы обнаружили, что при высокой нагрузке на устройство одна рабочая очередь может задерживаться до 7 мс в зависимости от поведения планировщика и других процессов, выполняемых в системе.
Вместо рабочей очереди драйверы, которым нужно обрабатывать прерывания в отдельном потоке, должны создавать собственный поток ядра с планировщиком SCHED_FIFO. Чтобы узнать, как это сделать с помощью функций kthread_work, ознакомьтесь с этим исправлением.
Конфликт блокировок фреймворка
Конфликт блокировки фреймворка может быть источником рывков и других проблем с производительностью. Обычно это вызвано блокировкой ActivityManagerService, но может наблюдаться и при других блокировках. Например, блокировка PowerManagerService может повлиять на производительность экрана. Если вы видите это на своем устройстве, то исправить проблему нельзя, поскольку она связана с архитектурой фреймворка. Однако если вы изменяете код, который выполняется внутри system_server, важно не удерживать блокировки в течение длительного времени, особенно блокировку ActivityManagerService.
Конфликт блокировок брошюратора
Исторически у связующего была единая глобальная блокировка. Если поток, выполняющий транзакцию связывателя, был вытеснен во время удержания блокировки, ни один другой поток не сможет выполнить транзакцию связывателя, пока исходный поток не снимет блокировку. Это плохо, поскольку конфликты binder могут заблокировать все в системе, включая отправку обновлений интерфейса на дисплей (потоки UI взаимодействуют с SurfaceFlinger через binder).
В Android 6.0 было добавлено несколько исправлений, которые улучшают это поведение, отключая вытеснение при удержании блокировки Binder. Это было безопасно только потому, что блокировка binder должна удерживаться в течение нескольких микросекунд фактического времени выполнения. Это значительно повысило производительность в ситуациях, когда не было конфликтов, и предотвратило конфликты, поскольку большинство переключений планировщика не выполнялось, пока удерживалась блокировка Binder. Однако вытеснение нельзя было отключить на все время удержания блокировки связующего ПО, а значит, оно было включено для функций, которые могли перейти в спящий режим (например, copy_from_user), что могло привести к тому же вытеснению, что и в исходном случае. Когда мы отправили исправления вышестоящим разработчикам, они сразу же сообщили нам, что это худшая идея в истории. (Мы согласились с ними, но также не могли не признать эффективность исправлений в предотвращении рывков.)
конфликт дескрипторов файлов в процессе;
Это редкий случай. Скорее всего, задержки вызваны не этим.
Однако если в процессе несколько потоков записывают данные в один и тот же дескриптор файла, возможен конфликт. Единственный раз, когда мы столкнулись с этим при запуске Pixel, был во время теста, в котором низкоприоритетные потоки пытались занять все процессорное время, пока в том же процессе выполнялся один высокоприоритетный поток. Все потоки записывали данные в дескриптор файла маркера трассировки, и высокоприоритетный поток мог быть заблокирован в дескрипторе файла маркера трассировки, если низкоприоритетный поток удерживал блокировку дескриптора файла, а затем был вытеснен. Когда трассировка была отключена для низкоприоритетных потоков, проблем с производительностью не было.
Нам не удалось воспроизвести эту ситуацию в других случаях, но мы считаем важным упомянуть ее как возможную причину проблем с производительностью при трассировке.
Ненужные переходы ЦП в режим ожидания
При работе с IPC, особенно с многопроцессными конвейерами, часто наблюдаются следующие варианты поведения во время выполнения:
- Поток A выполняется на ЦП 1.
- Поток A пробуждает поток B.
- Поток B начинает выполняться на ЦП 2.
- Поток А сразу переходит в спящий режим и будет разбужен потоком Б, когда тот закончит свою работу.
Чаще всего избыточные расходы возникают на этапе между шагами 2 и 3. Если ЦП 2 простаивает, его необходимо перевести в активное состояние, прежде чем поток B сможет запуститься. В зависимости от SoC и глубины простоя это может занять десятки микросекунд, прежде чем поток B начнет выполняться. Если фактическое время выполнения каждой стороны IPC достаточно близко к накладным расходам, общая производительность этого конвейера может быть значительно снижена из-за переходов ЦП в режим ожидания. Чаще всего в Android это происходит при транзакциях связывателя, и многие сервисы, использующие связыватель, в конечном итоге выглядят так, как описано выше.
Сначала используйте функцию wake_up_interruptible_sync() в драйверах ядра и поддерживайте ее в любом пользовательском планировщике. Это требование, а не рекомендация. Binder уже использует эту функцию, и она помогает избежать ненужных переходов в режим ожидания ЦП при синхронных транзакциях Binder.
Во-вторых, убедитесь, что время перехода cpuidle реалистично и регулятор cpuidle учитывает его правильно. Если SOC постоянно переходит из глубокого режима ожидания в активный режим и обратно, то переход в глубокий режим ожидания не позволит сэкономить энергию.
Журналы
Ведение журнала требует ресурсов ЦП и памяти, поэтому не спамьте в буфер журнала. Запись в журнал требует циклов процессора в приложении (напрямую) и в демоне журнала. Перед отправкой устройства удалите все журналы отладки.
Проблемы с Google I/O
Операции ввода-вывода часто являются источником дрожания. Если поток обращается к файлу, сопоставленному с памятью, а страницы нет в кеше страниц, происходит ошибка и страница считывается с диска. Это блокирует поток (обычно на 10 мс и более), и если это происходит на критическом пути отрисовки интерфейса, может привести к рывкам. Причин операций ввода-вывода слишком много, чтобы обсуждать их здесь, но при попытке улучшить поведение ввода-вывода проверьте следующие места:
- PinnerService. Добавленный в Android 7.0, PinnerService позволяет фреймворку блокировать некоторые файлы в кеше страниц. Это освобождает память для использования любым другим процессом, но если есть файлы, которые, как известно заранее, используются регулярно, может быть эффективно заблокировать эти файлы.
На устройствах 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. Самое главное, встроенное шифрование уменьшает дрожание, связанное с операциями I/O, особенно по сравнению с шифрованием на основе ЦП. Поскольку запросы к кешу страниц часто находятся на критическом пути отрисовки интерфейса, шифрование на основе ЦП создает дополнительную нагрузку на ЦП на критическом пути, что приводит к большему джиттеру, чем просто запрос I/O.
У аппаратных движков шифрования на основе DMA есть похожая проблема, поскольку ядро должно тратить циклы на управление этой работой, даже если есть другая важная работа, которую можно выполнить. Мы настоятельно рекомендуем поставщикам систем на кристалле, разрабатывающим новое оборудование, включить поддержку встроенного шифрования.
Агрессивная упаковка небольших задач
Некоторые планировщики поддерживают упаковку небольших задач на отдельные ядра ЦП, чтобы снизить энергопотребление за счет более длительного простоя большего количества ЦП. Хотя это хорошо сказывается на пропускной способности и энергопотреблении, это может катастрофически повлиять на задержку. В критическом пути отрисовки интерфейса есть несколько коротких потоков, которые можно считать небольшими. Если эти потоки будут задержаны при переносе на другие процессоры, это приведет к рывкам. Рекомендуем использовать упаковку небольших задач очень осторожно.
Частое переключение страниц в кеше
Устройство, на котором недостаточно свободной памяти, может внезапно начать работать очень медленно при выполнении продолжительной операции, например при открытии нового приложения. Трассировка приложения может показать, что оно постоянно блокируется в I/O во время определенного запуска, даже если обычно этого не происходит. Обычно это признак того, что кеш страниц переполнен, особенно на устройствах с небольшим объемом памяти.
Один из способов определить это – выполнить трассировку системы с помощью тега pagecache и передать ее скрипту по адресу system/extras/pagecache/pagecache.py. pagecache.py преобразует отдельные запросы на сопоставление файлов в кеше страниц в агрегированную статистику по файлам. Если вы обнаружите, что из файла было прочитано больше байтов, чем его общий размер на диске, вы определенно столкнулись с перегрузкой кеша страниц.
Это означает, что рабочий набор, необходимый для вашей рабочей нагрузки (обычно это одно приложение плюс system_server), больше объема памяти, доступного для кеша страниц на вашем устройстве. В результате, когда одна часть рабочей нагрузки получает необходимые данные из кеша страниц, другая часть, которая будет использоваться в ближайшем будущем, удаляется и должна быть получена снова, что приводит к повторному возникновению проблемы до тех пор, пока загрузка не будет завершена. Это основная причина проблем с производительностью, когда на устройстве недостаточно памяти.
Нет надежного способа устранить эту проблему, но есть несколько способов улучшить ситуацию на определенном устройстве.
- Используйте меньше памяти в постоянных процессах. Чем меньше памяти используют постоянные процессы, тем больше памяти доступно для приложений и кэша страниц.
- Проверьте, какие области памяти выделены для устройства, чтобы убедиться, что вы не удаляете память из ОС без необходимости. Бывали случаи, когда разделы, используемые для отладки, случайно оставались в конфигурациях ядра, потребляя десятки мегабайт памяти. Это может помочь избежать переполнения кеша страниц, особенно на устройствах с небольшим объемом памяти.
- Если вы видите, что в system_server происходит пробуксовка кеша страниц критически важных файлов, закрепите их. Это увеличит нагрузку на память в других местах, но может изменить поведение настолько, чтобы избежать пробуксовки.
- Перенастройте lowmemorykiller, чтобы освободить больше памяти. Пороговые значения lowmemorykiller основаны как на абсолютном объеме свободной памяти, так и на кеше страниц, поэтому увеличение порогового значения, при котором процессы на заданном уровне oom_adj завершаются, может привести к улучшению поведения за счет увеличения количества завершений фоновых приложений.
- Попробуйте использовать ZRAM. Мы используем ZRAM на устройствах Pixel, даже если у них 4 ГБ ОЗУ, поскольку это может помочь с редко используемыми грязными страницами.