[Перевод] Как мы устраняли ошибку Chrome, скрывавшуюся в коде со времён совместимости с Windows XP

xa7jcf29funxx8mswa1b28_ae7i.jpeg

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

1%


Наши метрики показывают, что Chrome в среднем быстр, но временами может заметно притормаживать. Подобные страдания пользователей видны в 99-м перцентиле многих метрик, но невоспроизводимы, а поэтому с ними довольно сложно работать. Более глубокий анализ данных показывает, что «длинный хвост» производительности свойственен не 1% пользователей на медленных машинах, а множеству пользователей в 1% от общего времени.

Давайте поговорим об этом 1%.

На практике, 1% — это довольно большая цифра. Наша базовая метрика — это «jank», то есть заметная задержка между вводом пользователя и реакцией на него ПО. Chrome измеряет jank через каждые 30 секунд, поэтому Jank в 1% выборок конкретного пользователя означает, что он возникает каждые 50 минут. В такие моменты этот пользователь начинает чувствовать, что Chrome тормозит. Итак, наша проблема заключается в следующем: сможем ли мы найти и устранить первопричины всех случаев, когда Chrome ненадолго притормаживает у пользователей?

Подход к решению


Мы инженеры, поэтому при работе над оптимизациями привыкли к тому, что необходимо повышать производительность создаваемых нами компонентов. Однако последние три года анализа чрезвычайно сложной кодовой базы Chrome научили нас, что реальные проблемы часто возникают на стыке компонентов: проблемы производительности «длинного хвоста» множества несвязанных друг с другом функций имеют одинаковую системную первопричину. В таких случаях локальный анализ и оптимизации не позволяют достичь глобального оптимума. Приходится отбросить своё интуитивное понимание и исходить из собственного невежества; это позволяет нам копать глубже очевидного и находить фундаментальные причины благодаря неутомимому исследованию неизвестного.

Ищем невидимые баги


Как нам найти непредсказуемые и невоспроизводимые баги, которые не относятся к конкретному компоненту, а потому, по сути, невидимы?

Во-первых, нужно определить сценарий. Для этого мы делаем упор на заметном пользователю Jank, то есть метрике, позволяющей систематически обнаруживать моменты, в которые ощущается торможение Chrome.

Во-вторых, получать практичные отчёты о багах работающего браузера. Для этого мы используем инфраструктуру Chrome BackgroundTracing, генерирующую то, что мы называем «медленными отчётами» (Slow Reports). У подмножества пользователей-«канареек», давших согласие на передачу анонимизированных метрик, включена трассировка циклического буфера для изучения конкретных сценариев. Если значение интересующей нас метрики превышает заданное значение, трассируемый буфер записывается, анонимизируется и загружается на серверы Google.

Подобный отчёт о багах может выглядеть вот так:

6gqpsx9otlvum_yp9ufbqgvuamo.png

Отображение в chrome://tracing двухсекундного Jank на AutocompleteController: UpdateResult (); в остальном машина не имеет никаких проблем

Виновник найден! Ну что, значит, надо оптимизировать AutocompleteController? Нет! Мы ещё не знаем, почему так происходит: продолжаем считать, что мы находимся в неведении!

Усилив BackgroundTracing сэмплированием стека, мы смогли найти под тормозящими событиями AutoComplete повторяющийся стек:

RegEnumValueW
RegEnumValueWStub
base::win::RegistryValueIterator::Read()
gfx::`anonymous namespace\'::CachedFontLinkSettings::GetLinkedFonts
gfx::internal::LinkedFontsIterator::GetLinkedFonts()
gfx::internal::LinkedFontsIterator::NextFont(gfx::Font *)
gfx::GetFallbackFonts(gfx::Font const &)
gfx::RenderTextHarfBuzz::ShapeRuns(...)
gfx::RenderTextHarfBuzz::ItemizeAndShapeText(...)
gfx::RenderTextHarfBuzz::EnsureLayoutRunList()
gfx::RenderTextHarfBuzz::EnsureLayout()
gfx::RenderTextHarfBuzz::GetStringSizeF()
gfx::RenderTextHarfBuzz::GetStringSize()
OmniboxTextView::CalculatePreferredSize()
OmniboxTextView::ReapplyStyling()
OmniboxTextView::SetText...)
OmniboxResultView::Invalidate()
OmniboxResultView::SetMatch(AutocompleteMatch const &)
OmniboxPopupContentsView::UpdatePopupAppearance()
OmniboxPopupModel::OnResultChanged()
OmniboxEditModel::OnCurrentMatchChanged()
OmniboxController::OnResultChanged(bool)
AutocompleteController::UpdateResult(bool,bool)
AutocompleteController::Start(AutocompleteInput const &)
(...)

Ага! Значит, проблема не в Autocomplete. Ну что, оптимизируем GetFallbackFonts ()?! Но постойте — почему GetFallbackFonts () вообще вызывается?

И прежде чем мы начнём это выяснять, как нам понять, что это именно первопричина нашей общей проблемы производительности «длинного хвоста»? В конце концов, мы ведь изучили пока только одну трассировку…

Проблема измерений


Метрики сообщают нам, на какое количество пользователей повлияла проблема и насколько она серьёзна, но они не позволяют определить первопричину.

Slow Reports сообщают нам, в чём проблема у конкретного пользователя, но не количество таких пользователей. И хотя мы можем выполнять запросы к нашему корпусу трассировок Slow Report, они имеют неустранимые уникальные расхождения, из-за чего их нельзя полностью сопоставить с метриками. Например, поскольку Chrome сообщает только о первом случае низкой производительности за сессию, и только для пользователей канала Canary/Dev, то существуют расхождения и времени запуска, и популяции пользователей.

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

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

bapisoprh1ypsei8qaz_k4tdzsm.png

Поэтому мы научным способом пришли к наименее инженерному варианту: чтению набора трассировок Slow Report «глазами». Так мы достигли максимальной степени практического изучения высокоуровневой проблемы, метрики которой мы уже выразили количественно.

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

Fallback Fonts


Мы начали расследовать, почему вообще должен вызываться GetFallbackFonts (). В приведённом выше примере вызывающая сторона пытается определить размер в пикселях строки Юникода, рендерящейся указанным шрифтом.

Если её подстрока находится в блоке Юникода, который нельзя отрендерить указанным шрифтом, то используется GetFallbackFont () для выполнения запроса к системе, который должен возвращать рекомендуемый шрифт замены (fallback font). Если узнать шрифт не удаётся, GetFallbackFonts () вызывается для проверки всех связанных шрифтов и определения того шрифта, который отрендерит строку лучше всего; этот второй fallback намного медленнее.

GetFallbackFont () должен срабатывать всегда, то на практике не всё так просто. Надёжным способом его реализации в Windows является запрос к DirectWrite; однако DirectWrite был добавлен в Windows 7, когда Chrome продолжал поддерживать Windows XP. Поэтому логика GetFallbackFont () вынуждена использовать менее надёжную эвристику с использованием Uniscribe+GDI для сохранения работоспособности с обеими версиями ОС. Так как чаще всего всё работало без проблем, никто не замечал, что ситуацию нужно было изменить, когда Chrome в дальнейшем отказался от поддержки Windows XP. Благодаря новому инструментарию для исследования производительности «длинного хвоста» мы выяснили, что это стало основной причиной возникновения jank (ненужного вызова GetFallbackFonts ()).

Мы устранили эту ошибку, снизив количество вызовов GetFallbackFonts () в четыре раза.

ep5kedg6hu1_a3jggkmm__gn3j4.png

Но не до нуля: в наших Slow Reports продолжали встречаться примеры вышеупомянутой проблемы с AutoComplete. Так что продолжим копать. Сбой GetFallbackFont (), использующего DirectWrite, был неожиданным, но поскольку Slow Reports анонимизированы, в них не могут загружаться сгенерированные пользователем строки, а потому обнаружение проблем в таких элементах кодового пространства было сложной задачей. Совместно с нашими специалистами по конфиденциальности мы настроили систему, пропускающую блоки Юникода и скрипты текстовых блоков через HarfBuzz, чтобы гарантировать отсутствие утечек персональных данных пользователей.

Сага об эмодзи


После создания этой системы к нам начала поступать новая волна Slow Reports. Подавляющее большинство отчётов показывало, что в font fallback происходит сбой, когда у DirectWrite запрашивали найти шрифт для элемента кодового пространства (символа Юникода) из раздела «Различные символы и пиктограммы» (Miscellaneous Symbols and Pictographs). Мы написали локальный скрипт, проверяющий все элементы кодового пространства в этом блоке Юникода, и быстро выяснили, какие из них могут вызывать проблемы: U+1F3FB — U+1F3FF — это модификаторы, добавленные в Unicode 8.0, имеющие смысл только в сочетании с другим элементом кодового пространства. Например, U+1F9D7 (ksdf_saruo_s5slod7zxz6j_ko4.png) в сочетании с U+1F3FF даёт image. Ни один шрифт не может отрендерить U+1F3FF сам по себе, поэтому font fallback при запросе на поиск подходящего шрифта, проверив все связанные шрифты, вполне корректно выдаёт ошибку. Баг находится в логике сегментации Юникода на стороне браузера, некорректно разбивавшей эти элементы кодового пространства и требующей у DirectWrite рендерить их по отдельности, вместо того, чтобы сохранить их как единую графему.

Но постойте, разве Chrome не поддерживает современный Юникод?! Да, действительно, поддерживает, в Blink, который рендерит веб-контент. Но логику на стороне браузера не изменяли для поддержки современных эмодзи (с модификаторами), потому что она вообще не использовалась для отрисовки эмодзи. Эта легаси-логика сегментации превратилась в (невидимую) проблему только в 2018 году, когда UI браузера (полоса вкладок, панель закладок, omnibox, и т.п.) была модернизирована для поддержки Юникода.

Кроме того, логика кэширования не кэшировала ошибку, поэтому попытка отрендерить модификатор отдельно вызывала каждый раз сильный jank у пользователей, на машинах которых установлено много шрифтов. Забавно, что этот кэш был добавлен для амортизации вычислительных затрат на это непонятое разработчиками «узкое место», когда поддержка Юникода впервые была добавлена в UI браузера. Благодаря тому, что мы углубились в изучение фундаментальной реализации нашей логики шрифтов, а не остановились на слое API шрифтов, мы не только решили важную проблему производительности, но и устранили ошибку правильности отображения других эмодзи. Например, y0es0osnrdfwxym-ucwpgb3urou.png кодируется как U+1F3F3 (ztefihrhgzu6xmq_sj8sp-hzgv0.png) + U+1F308 (kejfmnyxy9xmivj7i5kxyykiquy.png); до устранения проблемы разделения графемы на части UI браузера некорректно рендерил эту графему как ztefihrhgzu6xmq_sj8sp-hzgv0.pngkejfmnyxy9xmivj7i5kxyykiquy.png.

А наш путь продолжается…


Наш путь проходит через различные компоненты Chrome, но всегда следует одному простому правилу: нужно предполагать собственное невежество и неустанно исследовать непредсказуемые и невоспроизводимые баги, не привязанные к конкретному компоненту. И хотя проблемы группового ранжирования решить практически невозможно (из-за проблемы измерений), исправление пяти самых часто встречающихся в любом инструменте неполадок и внимательное изучение «длинного хвоста» на практике всегда решало большинство проблем пользователей.

Благодаря такому подходу мы за последние два с половиной года снизили уровень заметного пользователю jank в десять раз, и повысили производительность «длинного хвоста» многих функций, попавшихся нам на глаза при этом процессе.

zf_lgfgbatwts4m-a16406avm2u.png

99-й перцентиль количества интервалов без реакции на действия пользователя за 100 мс в 30-секундной выборке

Источник данных для всей статистики: реальные данные, анонимно агрегированные с клиентов Chrome.


На правах рекламы


VDSina предлагает виртуальные серверы на Linux и Windows — выбирайте одну из предустановленных ОС, либо устанавливайте из своего образа.

8p3vz47nluspfyc0axlkx88gdua.png

© Habrahabr.ru