Куленепробивні тести JavaScript

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

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.