Один не совсем обычный баг
Меня зовут Кирилл, я работаю в группе внутренних компонентов Яндекс.Браузера в Новосибирске. В один не совсем прекрасный день коллеги из тестирования Яндекс.Браузера воспроизвели проблему с проигрыванием видео через Flash Player. И поскольку именно наша группа отвечает за эту часть браузера (медиа, кодеки, вот это все), задача досталась мне. Баг, скажем так, не претендовал на оригинальность. Клик по кнопке Play приводил к черному прямоугольнику вместо корректного воспроизведения видео. Этот симптом я встречал и раньше, поэтому рассчитывал на достаточно быструю локализацию проблемы. Но я ошибался.
Буквально в первые же минуты удалось выяснить, что черный прямоугольник возникает не всегда, а только для flash-элементов с типом transparent, т.е. полупрозрачных. Отлично, уже есть за что зацепиться при отладке. Собираю debug-версию браузера, запускаю, бага нет. А это уже тревожный звонок. Расхождения в работе debug и release версий — это всегда очень весело. Поэтому решаюсь собрать еще и релизную версию. Собрал, запускаю, бага нет.
Задумался. В чем отличия моей релизной сборки от той, что собирает сервер? Сходу вспомнил про компоновку библиотек. Разработчики собирают браузер в режиме shared_library. Это увеличивает количество dll, но зато сильно экономит время компоновки. Распространяется же браузер, собранный в режиме static_library, при котором собирается лишь несколько больших dll. Выставляю флаг static_library, делаю полную сборку. Наблюдаю, как link.exe медленно съедает всю оперативную память, но нет, 16 ГБ RAM хватит всем, компоновка завершается без допинга в виде файла подкачки. Запускаю. Бага нет.
Серьезно задумался. Вспомнил, что сборочный сервер собирает релизный Яндекс.Браузер с флагом official, который немного меняет поведение (подробнее расскажем чуть позже). Собираю с этим флагом. Дрожащей рукой запускаю браузер. Вы уже угадали? Бага нет.
Тут я не на шутку встревожился и начал думать изо всех сил. Через некоторое время обратил внимание на то, что сервер собирает Яндекс.Браузер с помощью Visual Studio 2013. А я же использовал 2015 версию. Собираю в 2013 версии. Запускаю. Баг есть! Кто бы мог подумать, что я так буду радоваться ошибке.
Если вы сейчас подумали, что вся проблема заключалась только в версии VS, то ошибаетесь. Баг действительно не воспроизводился в debug-версии браузера. Опытным путем удалось установить, что для появления ошибки с черным прямоугольником браузер должен быть собран не только с помощью VS 2013, но и в статичной компоновке с флагом official. О причинах такого странного поведения вы узнаете чуть позже.
Следующие два дня были не менее интересными. В ходе отладки мне удалось понять, что сам плагин Flash Player отрабатывает свою задачу корректно: видео воспроизводится. Его интеграция с браузером вопросов также не вызывала. Результат его работы передавался для отрисовки, но по каким-то причинам на экране мы видели совсем другое. А это значит, что баг нужно было искать в той части браузера, которая отвечает за рендеринг. И здесь я передаю слово Вадиму.
Оптимизируй это
Как вы уже поняли, теперь на связи Вадим. Работаю я в группе разработки рендеринг-движка Яндекс.Браузера в Москве. Несколько слов о том, как вообще происходит отрисовка в Яндекс.Браузере или Chromium. Все, что вы видите в окне браузера, есть результат совмещения различных слоев (веб-страница, интерфейс), почти как в Photoshop. За работу с этими слоями отвечает компонент Compositor (или Chrome's Compositor == CC). А вот для отрисовки уже каждого слоя СС вызывает специальную опенсорсную библиотеку Skia.
Вместе с Кириллом мы поняли, что следы бага уходят в Skia. Оставалось понять, куда именно. К счастью, у меня была ценная подсказка. Почти в самом начале Кирилл её упомянул. Речь о том, что проблема возникает только в случае flash-элементов с прозрачностью. Чтобы отрисовать на экране такой элементы, браузеру необходимо совместить картинку видео с фоном. И для этого в Skia есть специальная функция SrcATop, отвечающая за блендинг. Несколько минут поиска, и вот я уже нашел багрепорт со схожей проблемой в Chrome, который окончательно развеял все сомнения.
Ура. Мы локализовали источник проблемы вплоть до конкретной функции. А теперь, внимание. Этот участок кода не содержал никаких ошибок. Совсем никаких. И работал он идеально для любых сборок кроме самой финальной, которая и отправляется пользователям. Причем только для Visual Studio 2013. И вот в этот момент я понял, почему Кирилл называл этот баг «веселым».
Начинаю разбираться в прочих отличиях, которые влияют на воспроизводимость ошибки. Пришло время вновь вспомнить про флаг official, который используются только в самом конце. Помимо всего прочего, этот флаг влияет на оптимизацию, а точнее на параметр /LTCG. Когда он указан, компилятор производит достаточно серьезную оптимизацию всей программы. На это уходит очень много времени, поэтому такие сборки просто так не собирают. Но как оптимизация может привести к ошибке? Чтобы понять это, нам понадобится небольшой флешбек.
В 2011 году проект Chromium стал настолько большим и сложным, что компоновщик Visual Studio 2010 однажды не смог слинковать его со всеми оптимизациями из-за нехватки ресурсов. Чтобы обойти проблему, разработчики решили по умолчанию оптимизировать все подпроекты (а их больше тысячи) не по скорости работы (/O2), а по размеру кода (/O1). И лишь для избранных и наиболее критичных, или для тех, чьи владельцы не проспали эту ситуацию, включили обратно /O2. Например, это сделали для CC и Skia. Вот только в 2013 году при рефакторинге в Skia оптимизацию случайно потеряли. И никто бы ничего не заметил, если бы еще через два года не случился еще один рефакторинг в Chromium, в результате которого часть кода сделали шаблонным и перенесли в header. И вот тут-то все и началось.
А началось вот что. Когда происходит сборка релизного браузера с флагом official, библиотеки, имеющие разные цели для оптимизации (по скорости, по размеру), оказываются в одной dll. Само по себе это не признак чего-то плохого. Например, в Visual Studio 2015 никаких проблем это не вызывает. Студия пыхтит час над оптимизацией и выдает вполне рабочий код. Но стоит нам заменить её 2013-й версией, и все ломается. Почему?
Функция SrcATop, которая отвечает за блендинг в Skia, принимает два параметра через регистры xmm0 и xmm1. И почти всегда она работает корректно. Но как мне удалось выяснить в ходе отладки, стоит добавить сюда VS 2013 и непростую оптимизацию, и функция вырождается до такой степени, что начинает возвращать в ответ содержимое первого регистра. Отсюда и появлялся неизменный черный фон вместо видео. Всему виной была неправильная кодогенерация в VS 2013.
Ускоряем веб-сёрфинг
При большом желании баг можно было «исправить» локально, немного подкрутив SrcATop. Но мне показалось неправильным, что у такого важного для отрисовки компонента, как Skia, отсутствует оптимизация по скорости. Поэтому я собрал новую сборку, в которой выставил для Skia оптимизацию по скорости. Баг, конечно же, пропал. Казалось бы, можно закрывать задачу и идти пить чай, но нет. Мне нужно было сделать еще кое-что.
Команда Яндекс.Браузера участвует в разработке Chromium уже не первый год. И это касается не только исправления ошибок. В свое время коллеги помогли с реализацией server push для HTTP/2 и со сборочной системой проекта для Windows. Поэтому и в этот раз я предложил решение проблемы и отправил на рассмотрение готовый коммит, который после небольшого обсуждения был принят.
Разработчикам из Chromium, так же как и мне, было интересно взглянуть на изменения в плане производительности браузера. Поэтому они прогнали целый комплекс performance-тестов. Практически все показатели для Windows подросли. Часть низкоуровневых тестов и вовсе показала улучшения в 2-3 раза. Интегральный FPS-тест для ключевых сайтов (иными словами, повседневный веб-сёрфинг) вырос на 6,5%. Отзывчивость на ввод улучшилась на 20-30%. В проекте Chromium далеко не каждый день случается оптимизация подобного уровня.
Учитывая, что VS 2010 уже давно не используется, я предложил попробовать включить оптимизацию по скорости для всего проекта. Тем более что обычная release-сборка (без флага official) всегда оптимизировалась по скорости целиком, и с тестированием никогда проблем не было. Но это уже совсем другая история.
P.S. Эта отдельно взятая проблема затронула лишь два офиса. На самом деле разработка Яндекс.Браузера ведется не только в Москве и Новосибирске. У нас есть команды еще и в Санкт-Петербурге, Екатеринбурге, Нижнем Новгороде, Минске и Киеве. И если вам было бы интересно к ним присоединиться, то заглядывайте на yandex.ru/jobs.
Комментарии (42)
Jeditobe
19.02.2016 17:20+2В какой версии хрома этот фикс есть?
BarakAdama
19.02.2016 17:27+2Фикс попал в Chromium 47.
luethus
20.02.2016 13:48+3Товарищи! Как раз начиная с 47, в Chromium сломан (по крайней мере на OS X) прозрачный режим, то есть рендеринг в прозрачном окне (alphaEnabled). Он используется в nwjs, Electron и Chrome Apps для создания прозрачных окон. Пример глючного рендеринга есть здесь: github.com/atom/electron/issues/4157
Так вот, может это вы нечаянно сломали? Мы на работе страдаем очень из-за этого.BarakAdama
20.02.2016 16:41+3Нет, OS X не трогали, потому что там нет Visual Studio. Но баги в Chromium появляются и так регулярно, поэтому скорее совпадение.
QtRoS
19.02.2016 17:47Пока читал в голове то и дело мелькали мысли "Отлично, старый добрый Хабр, статья с деталями и сюжетом, интересно", и это правда, но концовка немного подкачала — по сути всего лишь включили оптимизацию и всем похорошело.
P.S. А то, что в 2013 году имея такие бенчмарки взяли и прошляпили — это, безусловно, странно!
P.S.S. Невольно порадовался на тему того, что "твой самый странный баг все равно не переплюнули" :)
DjOnline
19.02.2016 19:52+1Есть ли ещё простор для ускорения? Например вручную переписать на Asm этот кусок кода, добавить SSE2-4/AVX, как это делают в x264 например. Попробовать скопимилировать с помощью Intel C Compiler.
Lof
20.02.2016 11:22+2Он уже на SSE (xmm это SSE регистр). А вообще у них уже все эти реализации есть: https://code.google.com/p/chromium/codesearch#chromium/src/third_party/skia/src/core/SkOpts.cpp&l=108
u007
19.02.2016 20:05Чтобы обойти проблему, разработчики решили пожертвовать скоростью? Это как понимать?
Antelle
20.02.2016 00:17Проект стал большим, с оптимизациями компилятор справляться перестал, простого решения не нашлось. Чтобы собрать, оптимизацию частично отключили. Немного более медленный, но рабочий код, всегда лучше несобирающегося вообще. Как-то так.
Lof
20.02.2016 11:26+3На самом деле, у них есть аргументы за оптимизацию по размеру кода.
1) Размер бинарника увеличивается на 25%, а значит пользователям больше качать.
2) Меньший по размеру код более cache-friendly.
В общем, они пока решили все не оптимизировать по скорости. Можешь с ними поспорить вот тут https://bugs.chromium.org/p/chromium/issues/detail?id=548457 :)
pavel_pimenov
20.02.2016 03:43+1Если не секрет, расскажите почему разработчики на своей локальной машине уже используют VC++2015
а на билд-сервере сборка релиза идет на VC++ 2013?fregate
20.02.2016 07:48Вот, например, изза таких ошибок. Можно собирать и в 2010, а писать в 2015.
Если у вас большая кодобаза, то оттестировать все на уровне чуть ли каждого байта — очень тяжелая задача.
alexstz
20.02.2016 08:16Такой пространный вопрос и немножко не в тему, но может знаете, почему на Mac (со встроенной графикой) Chrome работает заметно менее плавно, чем родной Safari? Что за хитрые оптимизации заставляют работать дефолтный браузер так быстро и непрожорливо (хотя насчёт последнего начиная с какой-то версии Chrome уже не так актуально)?
Lof
20.02.2016 11:34+3Боюсь, chrome всегда будет отставать от safari. Дело в том, что chrome имеет универсальную архитектуру, которая работает на множестве платформ.
Safari же заточен под OS X. Не даром они не смогли поддерживать его под Windows и закрыли проект.
Но есть одна настройка, которая увеличивает плавность скроллинга в chrome. Надо зайти на chrome://flags и включить GPU rasterization для всех слоев:
shamanchik
23.02.2016 21:41+1Chrome Версия 48.0.2564.116 (64-bit), MBP MD313, El Capitan
После включения стала тормозить прокрутка ленты ВКонтакте
ALPINE63rus
20.02.2016 09:26+1Я сейчас схлопочу минусов, но у меня остались вопросы:
Всему виной была неправильная кодогенерация в VS 2013.
Что, и всё? А подробности про вырождение той ошибочно-переоптимизированной функции? А вы писали разработчикам VS 2013 о проблеме? Уверены, что в VS 2015 бага уже нет? А может он только у вас перестал проявляться?
Вообще, краткое содержание статьи мне показалось вот таким: "Мы искали баг в коде, но оказалось, что у VS 2013 кривой компилятор. А ещё мы выставили /O2 и порадовались, что скорость возросла. Конец."BarakAdama
20.02.2016 11:14+3Почему VS ведёт себя именно так, ответить смогут только её разработчики. Баг с неправильной кодогенерацией функции SrcATop при разных целях для оптимизации был исправлен в 2015, потому что он пропал не только у нас, но и для всего Chromium (в том числе для иных ситуаций с блендингом).
Mixim333
20.02.2016 16:18Писал в VS2013 свой сериализатор, запустил в Debug-версии, подсунул файл в ~200Мб, через пару минут он успешно отработал, результат был ровно таким, какой я ожидал; собрал тот же самый код в Release-версии, дал тот же самый файл, посмотрел результат, а он кардинально отличается от того, что должно быть — WTF — снова перехожу в Debug и снова все отлично (условную компиляцию и прочие трюки не использую). Немного позже собрал этот же код в VS2015 и в Debug и в Release версиях — все отлично отработало! Так и не смог понять, что это был за баг
Так что такие проблемы случаются и на не очень больших и запутанных проектах
krak
21.02.2016 12:19+1Ребята, сделайте уже в Linux версии поддержку H.264. Chrome (из коробки), Vivaldi (с помощью доп.модулей) позволяют смотреть. А я, как дурак, пользуюсь ЯБ, а видео смотрю в другом. Служба поддержки только следующими релизами кормят.
Может хоть тут до разработчиков достучусь?
На всякий случай Ticket#15121617250381325.
Gorthauer87
24.02.2016 12:14+1Интересно, а кто-нибудь пробовал собирать при помощи gcc под windows? Насколько браузер хуже/лучше работать начинает?
c4simba
Если бага была в отрисовке слоев, то почему она не проявлялась в отрисовке других полупрозрачных компонентов?
Wedmer
Это уже вопрос к msvc2013, как я понял.
Lof
Не совсем понятно про какие компоненты идет речь.
Но если предположить, что про что-то простое типа css свойства opacity, то тут надо немного рассказать как хром рисует веб-странички.
Предположим, что у вас есть страничка с двумя статичными полупрозрачными изображениями.
Хром ( а реально WebKit ) заранее просчитает финальную картинку, которая получается при смешивании этих двух статичных картинок и браузер уже нарисует финальную картинку.
Однако, если что-то поменяется, надо будет перерисовать опять всю финальную картинку.
Чтобы этого избежать, придумали слои. JS разработчики могут на это явно влиять через will-change свойство css. А раньше для этого часто использовали transform z = 0.
В этом случае, WebKit отдаст браузеру две картинки, и он их смешает сам. С помощью skia.
В первом случае бага не будет. Потому что там будет использован код WebKit'а.
Во втором случае баг может быть.
А еще может все дело в том, что использовался не подверженный багу тип смешивания. Некоторые работали нормально :)
Fedcomp
На самом деле уже не webkit а blink.
Wedmer
Вы код хорошо изучили, прежде чем такое писать?
kos32
не читая код, можно объяснить, почему все ссылаются на WebKit, если форк произошел почти 3 года назад?
Gorthauer87
Думаю потому, что большая часть кода там всё-равно осталась вебкитовская.