Писать тесты скорости JS не так легко, как кажется. Даже не касаясь вопросов кроссбраузерной совместимости, можно попасть во множество ловушек.

Именно поэтому я и сделал 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 обновляются еженедельно, исправляют баги и добавляют новые возможности, повышая точность тестов.