Метрики производительности для исследования невероятно быстрых веб-приложений

Image 17.10.2019

Есть одно высказывание: «Что ты не можешь измерить, то ты не можешь улучшить». Автор статьи, перевод которой мы сегодня публикуем, работает в компании Superhuman. Он говорит, что эта компания занимается разработкой самого быстрого в мире почтового клиента. Здесь речь пойдёт о том, что такое «быстро», и о том, как создавать инструменты для измерения производительности невероятно быстрых веб-приложений.

Мы, в стремлении улучшить нашу разработку, потратили очень много времени на измерение её скорости. И, как оказалось, метрики производительности — это показатели, которые на удивление сложны для понимания и применения.

С одной стороны — сложно проектировать метрики, которые точно описывают те ощущения, которые пользователь испытывает, работая с системой. С другой стороны — непросто создавать метрики, которые точны настолько, что их анализ позволяет принимать обоснованные решения. В результате многие команды разработчиков не могут доверять собираемым ими данным о производительности их проектов.

Даже если у разработчиков есть достоверные и точные метрики, пользоваться ими нелегко. Как определить понятие «быстро»? Как найти баланс между скоростью и непротиворечивостью? Как научиться оперативно обнаруживать ухудшения производительности или научиться оценивать воздействие оптимизаций на систему?

Здесь мы хотим поделиться некоторыми соображениями, касающимися разработки средств анализа производительности веб-приложений.

1. Использование правильных «часов»

В JavaScript имеются два механизма для получения временных меток: performance.now() и new Date().

Чем они различаются? Для нас принципиальными являются следующие два различия:

Метод performance.now() гораздо точнее. Точность конструкции new Date() — ± 1 мс, в то время как точность performance.now() — это уже ± 100 мкс (да, речь идёт именно о микросекундах!).
Значения, возвращаемые методом performance.now(), всегда возрастают с постоянной скоростью и не зависят от системного времени. Этот метод просто отмеряет промежутки времени, не ориентируясь на системное время. А на new Date() системное время влияет. Если переставить системные часы, то изменится и то, что возвратит new Date (), а это испортит данные мониторинга производительности.

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

2. Проверка активности приложения

Если вы, измеряя производительность веб-приложения, переключитесь с его вкладки на какую-то другую — это нарушит процесс сбора данных. Почему? Дело в том, что браузер ограничивает приложения, находящиеся в фоновых вкладках.

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

Компьютер переводится в режим сна.
Приложение выполняется в фоновой вкладке браузера.

Возникновение обеих этих ситуаций — не редкость. У нас, к счастью, есть два варианта их решения.

Во-первых, мы можем просто игнорировать искажённые метрики, отбрасывая результаты измерений, слишком сильно отличающиеся от неких разумных значений. Например, код, вызываемый при нажатии на кнопку, просто не может выполняться в течение 15 минут! Возможно, это — единственное, что вам нужно для того, чтобы справиться с двумя вышеописанными проблемами.

Во-вторых, можно воспользоваться свойством document.hidden и событием visibilitychange. Событие visibilitychange вызывается тогда, когда пользователь переключается с интересующей нас вкладки браузера на другую вкладку или возвращается на интересующую нас вкладку. Оно вызывается тогда, когда окно браузера сворачивается или разворачивается, когда компьютер начинает работу, выходя из режима сна. Другими словами, это именно то, что нам нужно. Кроме того, до тех пор, пока вкладка находится в фоновом режиме, свойство document.hidden равно true.

Вот простой пример, демонстрирующий использование свойства document.hidden и события visibilitychange.

let lastVisibilityChange = 0
window.addEventListener('visibilitychange', () => {
  lastVisibilityChange = performance.now()
})
// не логируйте никаких метрик, собранных до последнего изменения видимости страницы,
// или метрик, собираемых на странице, находящейся на фоновой вкладке
if (metric.start < lastVisibilityChange || document.hidden) return

Как видите, некоторые данные мы отбрасываем, но это хорошо. Дело в том, что это данные, относящиеся к тем периодам работы программы, когда она не может полноценно пользоваться ресурсами системы.

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

3. Поиск показателя, который позволяет наилучшим образом зафиксировать время начала события

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

Если пользователь нажимает на кнопку во время выполнения некоего кода — программа не узнает об этом до тех пор, пока выполнение этого кода не завершится. Например, если приложение потратило 1000 мс в непрерывном цикле, а пользователь нажал кнопку Escape через 100 мс после начала цикла, событие не будет зарегистрировано ещё в течение 900 мс.

Это может сильно исказить метрики. Если нам нужна точность в измерении того, как именно пользователь воспринимает работу с программой, то это — огромная проблема!

К счастью, решить эту проблему не так уж и сложно. Если речь идёт о текущем событии, то мы можем, вместо использования performance.now() (времени, когда мы увидели событие), воспользоваться window.event.timeStamp (время, когда было создано событие).

Временная метка события устанавливается главным процессом браузера. Так как этот процесс не блокируется тогда, когда заблокирован цикл событий JS, event.timeStamp даёт нам гораздо более ценные сведения о том, когда событие было на самом деле запущено.

Тут надо отметить, что и этот механизм не идеален. Так, между моментом, когда нажата физическая кнопка, и моментом, когда соответствующее событие прибывает в Chrome, проходит 9-15 мс неучтённого времени (вот превосходная статья, из которой можно узнать о том, почему это происходит).

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

В результате, если говорить о нахождении временной метки начала события, то показатель event.timeStamp выглядит тут наиболее адекватно.

Как лучше всего оценить момент завершения события?

4. Выключение таймера в requestAnimationFrame()

Из особенностей устройства цикла событий в JavaScript вытекает ещё одно следствие: некий код, не имеющий отношения к вашему коду, может выполняться после него, но до того, как браузер отобразит на экране обновлённый вариант страницы.

Автор:
Просмотров: 88

Комментарии

Ничего не найдено.