← Блог

Turbolog: устранение мерцания

Очистка консоли перед каждым кадром оставляла её видимо пустой в 70–81 % отрисовок DevTools. Дописывание кадров снизило эту долю до 1 %, целиком на первом кадре.

Adam

Каждый кадр Text.js — это один вызов console.log. До версии 0.4 каждому кадру предшествовал ещё и вызов console.clear(). Под нагрузкой между кадрами консоль на мгновение становилась пустой.

Измерение мерцания

Для измерения мы создали bench/devtools. Он запускает Chrome for Testing, открывает настоящий интерфейс DevTools как страницу и подключает к нему инструменты измерения:

  • При каждом requestAnimationFrame в интерфейсе DevTools он проверяет, есть ли в видимой области консоли полный кадр из 32 строк. Отрисовки без такого кадра считаются пустыми.
  • Затем он декодирует кадры Page.startScreencast, чтобы подтвердить результат на уровне пикселей.
  • Каждый кадр содержит FRAME:<id> · T:<epochMs> в строке 0, поэтому интерфейс может определить, какой кадр он показывает и с каким опозданием.

При вызове console.clear() перед каждым кадром панель 100×32 с частотой 15 кадров/с оставалась пустой в 70,6 % отрисовок DevTools. При 60 кадрах/с — в 81,1 %.

Причина

Компонент ConsoleView в DevTools обрабатывает эти два вызова по-разному:

  1. console.clear() вызывает consoleCleared(), который синхронно инвалидирует видимую область. Все сообщения исчезают в рамках той же задачи.
  2. console.log() вызывает addConsoleMessage(), который лишь планирует обновление через viewportThrottler, экземпляр Throttler с тайм-аутом 50 мс.

Поэтому при каждом кадре старый удаляется сразу, а новый отрисовывается позже. При 15 кадрах/с примерно 50 из каждых 66 мс ничего не отображается.

Дописывание кадров

Начиная с версии 0.4, Turbolog дописывает кадры. DevTools удерживает прокрутку у нижнего края консоли, поэтому новый кадр заменяет старый за одну отрисовку. Начальный span нулевой ширины с margin-top:100vh (занавес) выталкивает предыдущие кадры из видимой области и завершает собственную строку, поэтому ссылка DevTools на исходный код никогда не сужает строку 0.

Со временем консоль всё же нужно очищать. Очистка происходит только на первом кадре, после invalidate(), в состоянии покоя после накопления 100 кадров и при достижении жёсткого предела в 300 кадров.

Согласование темпа с ограничителем частоты

Отправка кадров быстрее, чем DevTools успевает их отрисовывать, ничего не ускоряет. Сообщение, пришедшее во время ожидания ограничителя частоты, перезапускает его, поэтому кадры теряются: при 60 кадрах/с DevTools отрисовал 67 из 295 отправленных кадров. Turbolog выдерживает между кадрами не менее 66 мс, около 15 кадров/с, и по-прежнему синхронно отрисовывает дискретный ввод, когда интервал это позволяет.

Устранение фазовой привязки

Очистка ещё и добавляла около 50 мс задержки каждому последующему кадру. После очистки DevTools вставляет служебную строку Console was cleared. Она занимает слот немедленного выполнения ограничителя частоты и отправляет следующий кадр на таймер в 50 мс, а при стабильном интервале в 66 мс каждый последующий кадр остаётся привязанным к этому таймеру.

Теперь рендерер один раз ждёт 110 мс после каждой очистки. После этого каждый кадр приходит более чем через 50 мс после завершения предыдущего обновления и обрабатывается DevTools немедленно. Медианная задержка отображения снизилась с 63 мс до 10,4 мс.

Результаты

bun bench/devtools/run.ts --strategy=native,clear --fps=15,60 --seconds=5, панель 100×32, настоящий интерфейс DevTools:

РежимКадров/сПустые отрисовкиОтрисовано / отправленоЗадержка p50
Очистка перед каждым кадром1570,6 %70 / 7560,4 мс
Дописывание (1.0)151,0 %74 / 7510,4 мс
Очистка перед каждым кадром6081,1 %45 / 3009,8 мс
Дописывание без ограничения частоты600,8 %67 / 29531,2 мс

Оставшийся 1 % приходится на первый кадр, который намеренно очищает консоль. Его пустые отрисовки содержат строку Console was cleared, а не пустую консоль.

Меньше свойств на span

Исходный код DevTools выявил ещё один источник затрат. DevTools копирует каждое отдельное свойство стиля %c в каждую обёртку строки, поэтому сокращённые записи font: и margin: разворачивались примерно в 33 свойства на span. Теперь span используют только отдельные свойства, около 7 на каждый, с цветами в шестнадцатеричной записи.

Режим дописывания вышел в Text.js 0.4, а исправление фазовой привязки выходит в 1.0. Ни то ни другое не требует изменений в коде приложения.