Интересный случай тестирования производительности setTimeout(0)

1656515295 interesnyj sluchaj testirovaniya proizvoditelnosti settimeout0

автор Нетта Бонди

(Для полного эффекта читайте хриплым голосом в окружении облака дыма)

1*ybjNWDE73_POdC0dIFOZ_Q
Изображение от Studio-Dee на Pixabay.

Все началось в серый осенний день. Небо было облачным, ветер дул, и мне это кто-то сказал setTimeout(0) создает в среднем 4 мс задержки. Они утверждали, что это время, которое нужно, чтобы извлечь обратный вызов из стека, в очередь обратных вызовов и снова в стек. Мне показалось, что это звучало по-рыбски (это то, что вы представляете меня черно-белым с сигарой во рту). Учитывая, что конвейер визуализации должен производиться каждые 16 мс, чтобы обеспечить плавную анимацию, 4 мс казались мне долгое время. Очень долго.

Несколько наивных тестов в devtools с console.time()подтвердил это. Средняя задержка в течение 20 запусков составила около 1,5 мс. Конечно, 20 запусков не является достаточным размером выборки, но теперь мне было что доказать. Я хотел провести тесты большего масштаба, чтобы получить более точный ответ. Тогда я мог бы, конечно, пойти и махнуть этим в лицо своему коллеге, чтобы доказать, что они не правы.

Почему иначе мы делаем то, что мы делаем?

1*VHoVJS_SNxeV0hAdKgxnig
Фотосессия Film Noir — Portland Lightist от Рэнди Кашка на flickr.

Традиционный метод

Сразу я очутился в горячей воде. Чтобы измерить, сколько времени это заняло setTimeout(0) для запуска мне нужна была функция, которая:

  • сделал снимок текущего времени
  • выполнено setTimeout
  • затем немедленно вышел, чтобы стек был очищен, а запланированный обратный вызов мог запуститься и вычислить разницу во времени
  • и мне нужно было, чтобы эта функция запускалась достаточно большое количество раз, чтобы вычисления были статистически значимы

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

0*nyT20-4_HmK0z2ir
Цикл выполняется 10 раз, и только затем обратные вызовы возвращаются в стек.

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

Конечно, я мог бы устроить это, как некоторые из этих никудышных детективов, написать функцию, которая выполняет то, что мне нужно, а затем скопировать и вставить ее 10 000 раз. Я бы узнал то, что хотел знать, но исполнение было бы далеко не утонченным. Если бы я собирался втереть это в лицо кому-нибудь другому, я бы лучше сделал это по-другому.

Потом это дошло до меня.

Революционный метод

Я мог бы воспользоваться веб-воркером.

Веб-работники работают в другом потоке. Итак, если я помещу setTimeout логику в веб-воркере я мог бы вызвать несколько раз. Каждый вызов создавал бы собственный контекст выполнения, вызывая setTimeout, и немедленно выйти из функции, чтобы выполнить обратный вызов. Я с нетерпением ждал поработать с веб-работниками.

Пора перейти на мой надежный Sublime Text.

Я просто начал тестировать воду. С этим кодом в main.js:

0*jbunaO2AqtWGW2PN

Несколько сантехник здесь, чтобы подготовиться к фактическому тесту, но сначала я просто хотел убедиться, что могу правильно общаться с веб-работником. Итак, это было первоначальное worker.js:

0*iAl6qOeph4ww82VV

И хотя это сработало как очарование — оно принесло результаты, которых я должен был ожидать, но не был:

1*5C_QAUCW5q3EDuc818jG1w
Это, конечно, асинхронное…

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

Возможно, это меня удивило, но это сработало, как ожидалось. Я мог бы продолжить тест.

То, что я хотел, это для веб-работников onmessage функция регистрации t0звоните setTimeout, а затем немедленно выйдите, чтобы не блокировать стек. Однако я мог бы добавить дополнительную функциональность в обратный вызов после того, как я установил значение t1. Я добавил свою postMessage в обратный вызов, чтобы он не блокировал стек:

0*TMgPjObYkeuTVx9T
worker.js

И вот main.js код:

0*HvWvrei9KcHE3PQt

У этой версии есть проблема.

Конечно, поскольку я новичок в веб-работниках, я поначалу не думал об этом. Но когда несколько запусков функции продолжали печататься 0я подумал, что что-то не так.

Когда я печатал суммы изнутри onmessage Я получил свой ответ. Основная функция работала синхронно и не ожидала возврата сообщения от воркера, поэтому вычисляла среднее значение до завершения работы веб-воркера.

Быстрое и грязное решение – добавить счетчик и произвести расчет только тогда, когда счетчик достигнет максимального значения. Так вот новое main.js:

0*9DWX6pzoYDWMSn7O

И вот результаты:

main(10): 0.1

main(100) : 1.41

main(1000) : 13.082

о Мой. Ну, это не здорово, не правда ли? Что здесь происходит?

0*dRmCZYxnq3k8o7_v

Я пожертвовал тестированием производительности, чтобы заглянуть внутрь. Теперь я веду журнал t0 и t1 когда они создаются, просто чтобы увидеть, что там происходит.

И результаты:

0*Rpu-snzwyLtCXJa0

Оказывается мое ожидание t1 рассчитывается сразу после t0 также был обманчив. Фактически тот факт, что ничего в веб-работах не синхронно, означает, что мои основные предположения относительно поведения моего кода больше не соответствуют действительности. Это слепая зона, которую трудно увидеть.

Не только это, но даже результаты, которые я получил main(10) и main(100)которые поначалу сделали меня очень счастливым и самодовольным, я не мог полагаться.

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

Метод учебника

Я был разочарован… действительно ли я не мог найти обычное решение JS, которое было бы элегантно и доказывало бы, что мой коллега неправ?

И тогда я понял, что я могу что-нибудь сделать, но мне это не понравится.

Я мог бы позвонить setTimeout рекурсивно.

1*7b2gfSohmz3wtHZg4Jw59w

Теперь, когда я позвоню main оно позвонит testRunner какие меры t0 а затем планирует обратный вызов. Обратный вызов затем выполняется немедленно, вычисляет t1 а затем звонит testRunner снова, пока не будет достигнуто нужное количество вызовов.

Результаты этого кода были особенно впечатляющими. Вот несколько распечаток main(10) и main(1000):

1*1uKttvNJZrOzoSLUBxua7Q

Результаты существенно отличаются при вызове функции в 1000 раз по сравнению с 10-кратным вызовом. Я пробовал это неоднократно и получил почти одинаковые результаты main(10) поступает через 3–4 мс и main(1000) вершина 5 мс.

Честно говоря, я не уверен, что здесь происходит. Я искал ответа, но не нашел никакого разумного объяснения. Если вы читаете это и имеете обоснованные предположения о том, что происходит, я буду рад услышать от вас в комментариях.

Проверенный и верный метод

Где-то в глубине души я всегда знал, что дойдет до этого… Вопиющие вещи приятны для тех, кто их может получить, но проверенные и правдивые всегда будут там. Хотя я пытался этого избежать, я всегда знал, что это вариант. setInterval.

1*v-WpNKwlsLf0Vz_GUhxuEw

Этот код выполняет трюк с несколько грубой силой. setInterval запускает функцию несколько раз, ожидая 50 мс между каждым запуском, чтобы убедиться, что стек очищен. Это неэлегантно, но проверяет то, что мне нужно.

И результаты тоже были многообещающими. Времена, кажется, соответствуют моим начальным ожиданиям – менее 1,5 мс.

1*bLfRyBhL2verNKh7buvhrg
1*PVVq-8FAzT_VZkYhnxTBKw
Подобные результаты на 10 000 пробежек

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

Хотите поиграть с этим кодом? проверьте это здесь:

Добавить комментарий

Ваш адрес email не будет опубликован.