Писать тесты скорости JS не так легко, как кажется. Даже не касаясь вопросов кроссбраузерной совместимости, можно попасть во множество ловушек.
Именно поэтому я и сделал jsPerf. Простой веб-интерфейс для того, чтобы каждый мог создавать и делиться тестами, и проверять быстродействие различных фрагментов кода. Ни о чём не нужно беспокоиться – просто вводите код, быстродействие которого необходимо измерить, и jsPerf создаст для вас новую задачу по тестированию, которую вы затем сможете запустить на разных устройствах и в разных браузерах.
За кулисами jsPerf сначала использовал библиотеку на JSLitmus, которую я обозвал Benchmark.js. Со временем она обрастала новыми возможностями, и недавно Джон-Дэвид Дальтон переписал всё с нуля.
Эта статья проливает свет на разные каверзные ситуации, которые могут случиться при разработке тестов JS.
Есть несколько способов запустить тест части JS-кода для проверки на быстродействие. Самый распространённый вариант, шаблон А:
Тестируемый код размещается в цикле, который выполняется заданное количество раз (6). После этого дата старта вычитается из даты окончания. Такой шаблон используют тестировочные фреймворки SlickSpeed, Taskspeed, SunSpider и Kraken.
При постоянном повышении быстродействия устройств и браузеров, тесты, использующие фиксированное количество повторений, всё чаще выдают 0 ms как результат работы, что нам не нужно.
Второй подход – посчитать, сколько операций совершается за фиксированное время. Плюс: не нужно выбирать количество итераций.
Выполняет код примерно секунду, т.е. пока totalTime не превысит 1000 ms.
Шаблон B используется в Dromaeo и V8 Benchmark Suite.
Из-за сборки мусора, оптимизаций движка и других фоновых процессов время выполнения одного и того же кода может меняться. Поэтому тест желательно запускать много раз и усреднять результаты. V8 Suite запускает тесты только один раз. Dromaeo – по пять раз, но иногда этого недостаточно. Например, уменьшить минимальное время выполнения теста с 1000 до 50 ms, чтобы больше времени оставалось на повторенные запуски.
JSLitmus комбинирует два шаблона. Он использует шаблон А для прогона теста в цикле n раз, но циклы адаптируются и увеличивают n во время выполнения, пока не наберётся минимальное время выполнения теста – т.е. как в шаблоне В.
JSLitmus избегает проблем шаблона А, но от проблем шаблона В не уходит. Для калибровки выбираются 3 самых быстрых повторения теста, которые вычитаются из результатов остальных. К сожалению, «лучший из трёх» — статистически не лучший метод. Даже если прогнать тесты много раз и вычесть калибровочное среднее из среднего результата, увеличившаяся погрешность полученного результата съест всю калибровку.
Проблемы предыдущих шаблонов можно исключить через компиляцию функций и развёртку циклов.
Но и здесь есть недостатки. Компиляция функций увеличивает используемую память и замедляет работу. При повторении теста несколько миллионов раз вы создаёте очень длинную строку и компилируете гигантскую функцию.
Ещё одна проблема с развёрткой цикла – тест может организовать выход через return в начале работы. Нет смысла компилировать миллион строк, если функция делает возврат на третьей строчке. Нужно отслеживать эти моменты и пользоваться шаблоном А в таких случаях.
В Benchmark.js используется другая технология. Можно сказать, что она включает лучшие стороны всех этих шаблонов. Мы не развёртываем циклы для экономии памяти. Чтоб уменьшить факторы, влияющие на точность, и разрешить тестам работать с локальными методами и переменными, мы извлекаем для каждого теста тело функции. К примеру:
После этого мы запускаем извлечённый код в цикле while (шаблон А), повторяем до тех пор, пока не пройдёт заданное время (шаблон В), повторяем всё вместе столько раз, чтобы получить статистически значимые результаты.
В некоторых комбинациях ОС и браузера таймеры могут работать неверно по разным причинам. Например, при загрузке Windows XP время прерывания обычно составляет 10-15 мс. То есть, каждые 10 мс ОС получает прерывание от системного таймера. Некоторые старые браузеры (IE, Firefox 2) полагаются на таймер ОС, то есть, например, вызов Date().getTime() получает данные непосредственно от операционки. И если таймер обновляется только каждые 10-15 мс, это приводит к накоплению неточностей измерения.
Однако, это можно обойти. В JS можно получить минимальную единицу измерения времени. После этого нужно рассчитать время работы теста так, чтобы погрешность составляла не более 1%. Для получения погрешности нужно поделить эту минимальную единицу пополам. Например, мы используем IE6 на Windows XP и минимальная единица – 15 мс. Погрешность составляет 15 ms / 2 = 7.5 ms. Чтобы эта погрешность составляла не более 1% от времени измерения, поделим её на 0.01: 7.5 / 0.01 = 750 ms.
При запуске с параметром --enable-benchmarking flag, Chrome и Chromium дают доступ к методу chrome.Interval, который позволяет использовать таймер высокого разрешения вплоть до микросекунд. При работе над Benchmark.js Джон-Дэвид Дальтон встретил в Java наносекундный таймер, и сделал доступ к нему из JS через небольшой java-applet.
Используя таймер высокого разрешения, можно задавать меньшее время теста, что даёт меньше ошибок в результате.
Запущенный аддон Firebug отключает встроенную компиляцию по системе just-in-time, поэтому все тесты выполняются в интерпретаторе. Они будут работать там гораздо медленнее, чем обычно. Не забывайте отключать Firebug перед тестами.
То же, хотя и в меньшей степени, касается Web Inspector и Opera’s Dragonfly. Закрывайте их перед запуском тестов, чтобы они не влияли на результаты.
Тесты, использующие циклы, подвержены различным багам браузеров – пример был продемонстрирован в IE9 с его функцией удаления «мёртвого кода». Баги в движке Mozilla TraceMonkey или кеширование результатов querySelectorAll в Opera 11 тоже могут помешать получению правильных результатов. Нужно иметь их в виду.
В статье Джона Резига описано, почему большинство тестов не выдают статистически значимые результаты. Короче говоря, нужно всегда оценивать величину ошибки каждого результата и уменьшать её всеми возможными способами.
Тестируйте скрипты на реальных разных версиях браузеров. Не полагайтесь, например, на режимы совместимости в IE. Также, IE вплоть до 8-й версии ограничивал работу скрипта 5 миллионами инструкций. Если ваша система быстрая, то скрипт может выполнить их и за полсекунды. В этом случае вы получите сообщение “Script Warning” в браузере. Тогда придётся подредактировать количество разрешённых операций в реестре. Или воспользоваться программкой, исправляющей это ограничение. К счастью, в IE9 его уже убрали
Заключение
Выполняете ли вы несколько тестов, пишете ли свой набор тестов или даже библиотеку – в вопросе тестирования JS есть много скрытых моментов. Benchmark.js и jsPerf обновляются еженедельно, исправляют баги и добавляют новые возможности, повышая точность тестов.
Именно поэтому я и сделал jsPerf. Простой веб-интерфейс для того, чтобы каждый мог создавать и делиться тестами, и проверять быстродействие различных фрагментов кода. Ни о чём не нужно беспокоиться – просто вводите код, быстродействие которого необходимо измерить, и jsPerf создаст для вас новую задачу по тестированию, которую вы затем сможете запустить на разных устройствах и в разных браузерах.
За кулисами jsPerf сначала использовал библиотеку на JSLitmus, которую я обозвал Benchmark.js. Со временем она обрастала новыми возможностями, и недавно Джон-Дэвид Дальтон переписал всё с нуля.
Эта статья проливает свет на разные каверзные ситуации, которые могут случиться при разработке тестов JS.
Шаблоны тестов
Есть несколько способов запустить тест части JS-кода для проверки на быстродействие. Самый распространённый вариант, шаблон А:
var totalTime,
start = new Date,
iterations = 6;
while (iterations--) {
// Здесь идёт фрагмент кода
}
// totalTime → количество миллисекунд, потребовавшихся на шестикратное выполнение кода
totalTime = new Date - start;
Тестируемый код размещается в цикле, который выполняется заданное количество раз (6). После этого дата старта вычитается из даты окончания. Такой шаблон используют тестировочные фреймворки SlickSpeed, Taskspeed, SunSpider и Kraken.
Проблемы
При постоянном повышении быстродействия устройств и браузеров, тесты, использующие фиксированное количество повторений, всё чаще выдают 0 ms как результат работы, что нам не нужно.
Шаблон B
Второй подход – посчитать, сколько операций совершается за фиксированное время. Плюс: не нужно выбирать количество итераций.
var hz,
period,
startTime = new Date,
runs = 0;
do {
// Здесь идёт фрагмент кода
runs++;
totalTime = new Date - startTime;
} while (totalTime < 1000);
// преобразуем ms в секунды
totalTime /= 1000;
// period → сколько времени занимает одна операция
period = totalTime / runs;
// hz → количество операций в секунду
hz = 1 / period;
// или можно записать короче
// hz = (runs * 1000) / totalTime;
Выполняет код примерно секунду, т.е. пока totalTime не превысит 1000 ms.
Шаблон B используется в Dromaeo и V8 Benchmark Suite.
Проблемы
Из-за сборки мусора, оптимизаций движка и других фоновых процессов время выполнения одного и того же кода может меняться. Поэтому тест желательно запускать много раз и усреднять результаты. V8 Suite запускает тесты только один раз. Dromaeo – по пять раз, но иногда этого недостаточно. Например, уменьшить минимальное время выполнения теста с 1000 до 50 ms, чтобы больше времени оставалось на повторенные запуски.
Шаблон С
JSLitmus комбинирует два шаблона. Он использует шаблон А для прогона теста в цикле n раз, но циклы адаптируются и увеличивают n во время выполнения, пока не наберётся минимальное время выполнения теста – т.е. как в шаблоне В.
Проблемы
JSLitmus избегает проблем шаблона А, но от проблем шаблона В не уходит. Для калибровки выбираются 3 самых быстрых повторения теста, которые вычитаются из результатов остальных. К сожалению, «лучший из трёх» — статистически не лучший метод. Даже если прогнать тесты много раз и вычесть калибровочное среднее из среднего результата, увеличившаяся погрешность полученного результата съест всю калибровку.
Шаблон D
Проблемы предыдущих шаблонов можно исключить через компиляцию функций и развёртку циклов.
function test() {
x == y;
}
while (iterations--) {
test();
}
// …скомпилируется в →
var hz,
startTime = new Date;
x == y;
x == y;
x == y;
x == y;
x == y;
// …
hz = (runs * 1000) / (new Date - startTime);
Проблемы
Но и здесь есть недостатки. Компиляция функций увеличивает используемую память и замедляет работу. При повторении теста несколько миллионов раз вы создаёте очень длинную строку и компилируете гигантскую функцию.
Ещё одна проблема с развёрткой цикла – тест может организовать выход через return в начале работы. Нет смысла компилировать миллион строк, если функция делает возврат на третьей строчке. Нужно отслеживать эти моменты и пользоваться шаблоном А в таких случаях.
Извлечение тела функции
В Benchmark.js используется другая технология. Можно сказать, что она включает лучшие стороны всех этих шаблонов. Мы не развёртываем циклы для экономии памяти. Чтоб уменьшить факторы, влияющие на точность, и разрешить тестам работать с локальными методами и переменными, мы извлекаем для каждого теста тело функции. К примеру:
var x = 1,
y = '1';
function test() {
x == y;
}
while (iterations--) {
test();
}
// …скомпилируется в →
var x = 1,
y = '1';
while (iterations--) {
x == y;
}
После этого мы запускаем извлечённый код в цикле while (шаблон А), повторяем до тех пор, пока не пройдёт заданное время (шаблон В), повторяем всё вместе столько раз, чтобы получить статистически значимые результаты.
На что нужно обратить внимание
��е совсем верная работа таймера
В некоторых комбинациях ОС и браузера таймеры могут работать неверно по разным причинам. Например, при загрузке Windows XP время прерывания обычно составляет 10-15 мс. То есть, каждые 10 мс ОС получает прерывание от системного таймера. Некоторые старые браузеры (IE, Firefox 2) полагаются на таймер ОС, то есть, например, вызов Date().getTime() получает данные непосредственно от операционки. И если таймер обновляется только каждые 10-15 мс, это приводит к накоплению неточностей измерения.
Однако, это можно обойти. В JS можно получить минимальную единицу измерения времени. После этого нужно рассчитать время работы теста так, чтобы погрешность составляла не более 1%. Для получения погрешности нужно поделить эту минимальную единицу пополам. Например, мы используем IE6 на Windows XP и минимальная единица – 15 мс. Погрешность составляет 15 ms / 2 = 7.5 ms. Чтобы эта погрешность составляла не более 1% от времени измерения, поделим её на 0.01: 7.5 / 0.01 = 750 ms.
Другие таймеры
При запуске с параметром --enable-benchmarking flag, Chrome и Chromium дают доступ к методу chrome.Interval, который позволяет использовать таймер высокого разрешения вплоть до микросекунд. При работе над Benchmark.js Джон-Дэвид Дальтон встретил в Java наносекундный таймер, и сделал доступ к нему из JS через небольшой java-applet.
Используя таймер высокого разрешения, можно задавать меньшее время теста, что даёт меньше ошибок в результате.
Firebug отключает JIT в Firefox
Запущенный аддон Firebug отключает встроенную компиляцию по системе just-in-time, поэтому все тесты выполняются в интерпретаторе. Они будут работать там гораздо медленнее, чем обычно. Не забывайте отключать Firebug перед тестами.
То же, хотя и в меньшей степени, касается Web Inspector и Opera’s Dragonfly. Закрывайте их перед запуском тестов, чтобы они не влияли на результаты.
Фичи и баги браузеров
Тесты, использующие циклы, подвержены различным багам браузеров – пример был продемонстрирован в IE9 с его функцией удаления «мёртвого кода». Баги в движке Mozilla TraceMonkey или кеширование результатов querySelectorAll в Opera 11 тоже могут помешать получению правильных результатов. Нужно иметь их в виду.
Статистическая значимость
В статье Джона Резига описано, почему большинство тестов не выдают статистически значимые результаты. Короче говоря, нужно всегда оценивать величину ошибки каждого результата и уменьшать её всеми возможными способами.
Кросс-браузерное тестирование
Тестируйте скрипты на реальных разных версиях браузеров. Не полагайтесь, например, на режимы совместимости в IE. Также, IE вплоть до 8-й версии ограничивал работу скрипта 5 миллионами инструкций. Если ваша система быстрая, то скрипт может выполнить их и за полсекунды. В этом случае вы получите сообщение “Script Warning” в браузере. Тогда придётся подредактировать количество разрешённых операций в реестре. Или воспользоваться программкой, исправляющей это ограничение. К счастью, в IE9 его уже убрали
Заключение
Выполняете ли вы несколько тестов, пишете ли свой набор тестов или даже библиотеку – в вопросе тестирования JS есть много скрытых моментов. Benchmark.js и jsPerf обновляются еженедельно, исправляют баги и добавляют новые возможности, повышая точность тестов.
