Умные тесты производительности своими руками
В прошлой статье я показал, как можно собрать свой Lighthouse. Сегодня пришла очередь применить получившийся трифорс в Performance-тестах, которые мы с командой успешно применяем для оптимизации и ускорения платформ Авито.
Это не так просто, как хотелось бы. С функциональными тестами всё прозрачно — тест либо проходит успешно, либо фэйлится. А в Perfomance-тесте у вас есть какая-то цифра, и непонятно — хорошая она или плохая. Если бы мы использовали инструмент типа Lighthouse, можно было выставить performance-бюджет, чтобы зафиксировать эту цифру на каком-то уровне. Но для динамической ситуации это не подходит. Расскажу, как это понять и использовать.
В нашей динамической ситуации происходят SPA-события с произвольной длительностью, страницы загружаются разными способами. Как подо все эти метрики определить бюджет? А если бизнес говорит о том, чтобы задеплоить фичу, которая переступает через этот бюджет, но должна быть на проде? Она вызывает просадку, и поможет только ручное изменение бюджета. Но вам придется это делать каждый раз, когда бизнес хочет что-то срочно катить на прод.
Остаётся одно — сравнивать два замера между собой. Причем ретроспективное сравнение, которое есть в Lighthouse–ci, позволит нам сравнивать замеры не с фиксированной точкой, а с блуждающей. Для этого нужно выставить пороговое значение в изменении показателей, например процент допустимого изменения по time to interactive и другим интересующим метрикам и вуаля.
Или это можно сделать с помощью системы контроля версии. Вы можете сравнивать ревизии между собой, понимая, что на одной всё хорошо, а другой — всё плохо. Можно получить разницу по производительности и по изменениям, сравнив релизы. Но все это сложно анализировать, потому что есть десяток фич, в которых менялась половина исходников, и найти причину просадки будет проблематично.
Вариант получше — сравнить какую-то develop-ветку при выкатке на стейджинг. В этом случае нужно измерить несколько merge commit`ов и получить разницу уже по ним. Но это тоже проблема. Просадка, например, уже помержилась, и придется как-то откатывать. К тому же сложно понять, кто досыпал этот merge commit, что в нём происходило и почему он вообще туда попал.
И самый нормальный вариант — сравнение какой-то фича-ветки и вашей актуальной develop-ветки. Если у вас мастер, то с его ревизиями. Для этого каждый разработчик делает замер той фичи, которую он пилит, чтобы сравнить ее с тем, что сейчас есть в коммите ответвления фича-ветки. На выходе он получает конкретную информацию для себя, с ней уже можно работать.
И что же сравнивать? Собственно те метрики, которые мы собрали до этого — это Navigation и User тайминги, User-centric метрики и всякие переходы SPA-шные, какие-то интеракции и так далее. Можно сравнивать как в абсолютном эквиваленте, так и в процентном. Дополнительно можно сделать тот самый эвристический велосипеда, который будет подсказывать разработчику, что лучше оптимизировать.
Допустим, нужно сравнить чанки, поскольку основная информация для оптимизации содержится именно в них. Сделать это непросто, потому что webpack каждый раз меняет хэши этих чанков. Здесь и поможет плагин для webpack под bundle-analyzer. Чанки можно сравнивать по набору и хэшу модулей, по их частичному соответствию, по синергии из этих двух методов, чтобы понимать, поменялся у нас чанк или нет.
Итак, тесты готовы. Вы уже можете замерять страницу, делать прогоны, сравнивать их между собой и сравниваете вашу фич-бранчу с мастером. И всё бы хорошо, но эти тесты будут безбожно флаковать, то есть вы не будете получать стабильных результатов. И это будут, наверное, самые флакующие тесты в вашем окружении. Вы не сможете им доверять ни в ту, ни в другую сторону.
Повышаем точность тестов
Я этот сложный путь начал года два назад и до сих пор занимаюсь стабилизацией, потому что постоянно что-то перестает быть стабильным. Мне помогает только матстат. Если делать один замер, то можно постоянно получать разные результаты, как в случае с Lighthouse. Для более стабильных результатов нужно делать 10—40 замеров и брать от них какие-то агрегаты: например, медианы, какие-то средние арифметические либо квартили.
Провалидировав их, можно понять, насколько можно им доверять. MDE (minimum detectable effect) вам поможет понять, насколько у вас минимально могут отклоняться две выборки между собой. То есть насколько близко эти выборки перестают быть точными. А Standard Deviation (среднеквадратичное отклонение) покажет вам разброс вашей выборки.
А дальше тема о Thresholds — как вместо Performance-бюджета использовать другие штуки для контроля. Можно сравнивать медианы между собой, получать некую дельту и на ее основе принимать решение о том, успешно ли прошел тест, если дельта больше трешхолда. Также можно вычислить процентный эквивалент, сравнивая те же медианы. Помимо этого можно применить uTest, в котором можно сравнить серии замеров, получая вероятность того, насколько они отличаются между собой.
Что ж, вы, возможно, выставите трешхолды и запустите в CI тесты. И все равно вы отловите огромное количество false-positive срабатываний, то есть срабатываний, когда просадка задетектировалась, но ее на самом деле нет. Например, MDE покажет, что вы минимально можете задетектить отклонение в 400 миллисекунд по какой-нибудь метрике (например first paint), которая имеет значение всего лишь полторы секунды.
Если у вас есть 10 страниц и 10 метрик на них, то понятно, что каждая метрика будет иметь разную стабильность (которая со временем тоже меняется) на каждой из 10 страниц. И вам придется индивидуально под каждую метрику на каждой странице выставлять этот Threshold, постоянно его контролируя. А так как стабильность может меняться со временем, у вас снова будет много false-positive срабатываний.
Что с этим делать? Вернемся к VCS. Если вы запустили профилирование на CI, то у вас, скорее всего, уже работают какие-то сравнения между ветками и вашим стейджингом или мастером. И также, возможно, вы запустили сравнение по ревизиям стеджинга. Если вы хотите получать огромное количество дельт, лучше это сделать.
Всё это вы можете использовать для построения динамических трешхолдов, которые сами будут приспосабливаться под стабильность вашей инфры, под стабильность вашего ресурса, и как-то контролировать этот процесс.
Далее мы чистим выборку из этих дельт между сравнениями и срезаем какие-то перцентили, где есть совсем дикие выбросы, например, отклонения по 400%. И теперьуже можно вычислить среднеквадратичное отклонение для дальнейшего использования в качестве трешхолда.
Как его использовать?
В методичке по матстату я нашел хорошие графики про нормальное распределение, и это можно применить к нашим дельтам между замерами. Сигма под графиком — это наше среднеквадратичное, которое мы вычислили с помощью дельт между замерами.
А по таблице можно понять, что если мы возьмем две сигмы, плюс-минус, то в этот промежуток будет попадать 95% всех дельт. И мы будем success«ить тест с вероятностью 95,5%. Вы также можете выбрать другой, более приятный вам, коэффициент у сигмы и использовать его для вероятностного контроля ваших тестов.
Мы получили автоматизированные Thresholds — и они завелись и работают. После этого вы уже не будете постоянно ловить false positive срабатывания. И тем не менее…
Где же стабильность?
Если вы посмотрите на ваше среднеквадратичное, то поймете, что стабильность — так себе, то есть ваша сигма — на уровне 50%. И вы будете детектить только 50%-ные отклонения по показателям. И как же добиться такого результата, когда у вас будет плюс-минус 1,5%?
Это будет второй частью повышения точности, только уже повышения точности наших замеров. Если еще точнее, то конкретного замера конкретной загрузки страницы. Чтобы именно выборка загрузок страниц была как можно более скученной и с минимальным разбросом.
Для этого есть несколько методов. В первую очередь, это золотой сервак — изолированный сервер, где ничего не крутится, кроме ваших Performance-тестов. Следующее — это мокирование сети. Вокруг сервера всё равно есть стохастическая среда, которая не помогает вам в своей стохастичности, а только ухудшает результаты. И последнее — детерминированность, то есть ваши странички должны постоянно загружаться идентичным образом.
С сервером все понятно. Остановлюсь на последних двух пунктах.
Мокирование сети
Я сравнил несколько инструментов: Squid, Web Page Replay, самописные решения, и отсутствие мокирования вообще. В последнем случае у нас есть среднеквадратичное на уровне 100% метрики — то есть мы вообще ничего задетектить не сможем и постоянно будет разброс замеров по всей их величине. Стабильность будет зависеть от вашего бэкенда и сети вокруг него. Сложности настройки нет — вы просто запускаете тесты на CI, и они работают. Натуральность низкая, потому что у вас есть сеть вокруг, деплои рядом, нагрузка на сервер от ваших функциональных тестов.
Следующий вариант — Web Page Replay от catapult-project — получше в точности, но стабильность так себе. Я удивился, когда его настроил и увидел огромное количество cache miss, то есть ресурсы не были найдены. В целом он очень плохо себя показал. Сложности в настройке тоже нет — просто две команды, которые запускают на запись странички и на воспроизведение этой странички. Но если у вас есть какой-то cache miss, когда вы пытались его загрузить в первый раз, то этот cache miss так и останется, и WPR отдаст 404. К сожалению, это плохо сказывается на стабильности. Натуральность низкая, так как он слишком синтетически эмулирует поведение бекенда.
Следующее — Request Interceptor + Node + mitmdump/mitmprox — самописное решение, которое может показать очень хороший результат с высокой стабильностью. Но всё зависит от того, как вы это напишите. Поэтому высокая сложность настройки, но, наверное, это самое лучшее решение по стабильности. Натуральность так себе, но тоже зависит от реализации. Я пробовал написать что-то похожее на NodeJS с Request Interceptor, но были довольно сложно добиться полной эмуляции сети. Если использовать какой-то кэширующий прокси, типа митмдам, перехватывать сжатый трафик и потом его отдавать, то можно сделать что-то более натуральное.
И последнее — Squid — это то, что мы используем в Авито. Это очень древний проект кэш-прокси, но у него есть большое community и хорошая поддержка. И реально это самый зеленый в таблице вариант. Единственный минус — это сложность настройки.
Так мы нивелировали влияние сети, замокировав все запросы через Squid.
Детерминированность
Здесь есть один фактор, из-за которого метрики могут скакать на 50%. Это — реклама. Она постоянно вызывает стохастические вариации, которые сложно детектировать. Это все лучше отсеять и оставить конкретный код продуктовой функциональности и профилировать его.
Для контроля запросов в Puppeteer есть Request Interceptor, и вы можете по какому-то паттерну заабортить какой-то запрос — отменить его либо продолжить. С помощью этого можно отфильтровать всю рекламу и повысить детерминированность вашей странички, ограничив влияние сети.
Request Interceptor
const isAd = (url) => url && /ad-url/.test(url);
const mockAd = async (page) => {
page.on('request', async (interceptedRequest) => {
if (isAd(interceptedRequest.url())) {
await interceptedRequest.abort();
} else {
await interceptedRequest.continue();
}
});
};
Итак, мы собрали всё кристражи вместе — и можно вступать в светлое будущее перфрманса, и, если всё это засетапить, оно реально будет таким. Потому что, если вы будете следить именно в контуре CI/CD за вашей производительностью, то, скорее всего, ваши просадки до прода не доедут.
Пример
Хочу показать Overlooker — проект, который используется внутри Авито. Это ядро нашего профилировщика, и он позволяет замерять производительность и сравнивать ее. Но пока там нет UI. Интерфейс там довольно простой. Есть несколько методов на экспорте, вы их можете использовать. Также есть дока с описанием того, как это всё применять. В планах по нему сделать UI, точнее, его заопенсорсить. И прикрутить поддержку mongoDB для сохранения результатов, чтобы к ним потом обращаться и эксплуатировать их.
Overlooker умеет замерять сразу несколько страниц — вы можете ему указать массив из этих страниц или сценариев. Поддерживает все имеющиеся на фронтенде метрики: User-Centric, Lighthouse Score, тайминги, Element тайминги и т.п.
Если вы будете использовать плагин для webpack, то получите подробное описание содержимого страницы, и в том числе вычислить страницы, на которых были изменения. Последнее полезно, чтобы профилировать только ту страницу, которая изменилась, так как замер производительности — это довольно дорогая операция, она съедает много ресурсов.
Резюме
Теперь вы сможете освободить разработчика от профилирования. Если вы всё это засетапите и автоматизируете, ваши разработчики уже не будут тратить огромное количество времени на весь этот описанный flow, чтобы задетектировать где-то просадку. Вы сможете поднять инфраструктуру для performance-тестов, стабилизировать ее, настроить. И вы сможете сохранить производительность, оставить ее на том уровне, на котором она у вас есть. Или даже постепенно улучшать. Это очень критично.
Потому что Google просто ухудшает ранжирование в поисковой выдаче, если у вас плохой перфоманс. И это то, что легко убеждает бизнес, лучше, чем A/B-тестирование с деградацией производимости.
Сколько вычислительных ресурсов на это всё надо? Для наших двух основных продуктов — мобильной версия сайта и десктопной — отправляется около 200 полуреквестов в день. Для их профилирования мы используем 4 сервера по 70 ядер. Но у нас происходит всего 40 замеров и 12 страниц и мы используем наши мощности не на полную катушку.
Сами тесты сейчас проходят за пять минут (если есть diff), раньше это занимало полчаса. И это было, конечно, очень жестко. Но сейчас наши performance-тесты проходят быстрее, чем многие e2e и интеграционные. И наши тесты не блокируют мерж Pull Request«а, они носят информационный характер, чтобы разработчики могли принять решения самостоятельно или обратиться за консультацией.
Видео моего выступления на Frontend Live 2020.
Встречаемся на FrontendConf 2021 в Москве или онлайн по всему миру 11–12 октября. Билеты, описание докладов, расписание и другая необходимая информация уже на сайте конференции.