Turbolog: устранение мерцания
Очистка консоли перед каждым кадром оставляла её видимо пустой в 70–81 % отрисовок DevTools. Дописывание кадров снизило эту долю до 1 %, целиком на первом кадре.
Каждый кадр 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 обрабатывает эти два вызова по-разному:
console.clear()вызываетconsoleCleared(), который синхронно инвалидирует видимую область. Все сообщения исчезают в рамках той же задачи.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 |
|---|---|---|---|---|
| Очистка перед каждым кадром | 15 | 70,6 % | 70 / 75 | 60,4 мс |
| Дописывание (1.0) | 15 | 1,0 % | 74 / 75 | 10,4 мс |
| Очистка перед каждым кадром | 60 | 81,1 % | 45 / 300 | 9,8 мс |
| Дописывание без ограничения частоты | 60 | 0,8 % | 67 / 295 | 31,2 мс |
Оставшийся 1 % приходится на первый кадр, который намеренно очищает консоль. Его пустые отрисовки содержат строку Console was cleared, а не пустую консоль.
Меньше свойств на span
Исходный код DevTools выявил ещё один источник затрат. DevTools копирует каждое отдельное свойство стиля %c в каждую обёртку строки, поэтому сокращённые записи font: и margin: разворачивались примерно в 33 свойства на span. Теперь span используют только отдельные свойства, около 7 на каждый, с цветами в шестнадцатеричной записи.
Режим дописывания вышел в Text.js 0.4, а исправление фазовой привязки выходит в 1.0. Ни то ни другое не требует изменений в коде приложения.