habrahabr

Пуленепробиваемые тесты JavaScript

  • воскресенье, 8 февраля 2015 г. в 02:15:42
http://habrahabr.ru/post/249969/

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