[Перевод] Измеряем задержку от клавиатуры до фотона с помощью оптического датчика
Для измерения времени отклика или задержки (latency) на компьютерах и в интерфейсах я давным-давно использую приложение Is It Snappy с высокоскоростной камерой iPhone для подсчёта кадров между нажатием клавиши и изменением экрана. Однако проблема заключается в определении на глаз точных кадров для тайминга, что раздражает при выполнении множества тестов. Это также затрудняет измерение вариабельности результатов. Я уже упростил эти тесты, добавив в прошивку клавиатуры режим, который изменяет цвет светодиода после отправки события по USB, но это лишь немного повышает скорость и точность. Хотелось бы чего-то получше.
Поэтому я пошел по стопам моего друга Рафа и сделал аппаратный тестер задержки, который посылает события клавиатуры, а затем с помощью оптического датчика измеряет время изменения экрана! Это было довольно легко, и в этой статье я расскажу о некоторых результатах, а также о сложностях качественного тестирования задержки и как сделать собственный тестер.
Мой тестер сделан на базе датчика освещённости с Amazon. Он установлен на регулируемый штатив, подключённый к микроконтроллеру Teensy LC, который нажимает 'а' и ждёт, когда изменится уровень освещённости, удаляет букву, а потом продолжает собирать образцы, пока удерживается кнопка. При коротком нажатии кнопки он выводит красивую гистограмму задержки, которая выглядит следующим образом:
lat i= 60.3 +/- 9.3, a= 60, d= 59 (n= 65,q= 41) | 239_ |
Эта строка показывает среднюю задержку вставок (i=
), удалений (d=
) и их вместе взятых (a=
), стандартное отклонение времени вставки (+/-
), количество измерений (n=
) и качество (q=
), а также небольшую ascii-гистограмму, где каждый символ представляет собой фрагмент (бакет) в 10 мс, а цифры от 1 до 9 пропорционально представляют собой, насколько заполнен этот бакет. Символ _
означает бакет с по крайней мере одним образцом, но его недостаточно до заполнения одной девятой бакета, так что это хвост задержек с малым количеством случайных образцов. Вот как это выглядит (на фотографии мониторы в портретном режиме, но все тесты делались в ландшафтном):
Я также сделал так, что при повторном нажатии кнопки выводятся все результаты измерений, например, [35, 35, 33, 44]
, так что можно сгенерировать график:
Начну с моего любимого набора результатов:
Sublime Text, macOS, distraction-free full-screen mode on two 4k monitors: lat i= 35.3 +/- 4.7, a= 36, d= 36 (n= 67,q= 99) | 193 | Dell P2415Q top lat i= 52.9 +/- 5.0, a= 53, d= 54 (n= 66,q= 45) | _391 | Dell P2415Q bottom lat i= 65.1 +/- 5.0, a= 64, d= 63 (n=109,q=111) | _292 | HP Z27 top lat i= 79.7 +/- 5.0, a= 80, d= 80 (n= 98,q=114) | 89_ | HP Z27 bottom
Здесь есть на что посмотреть:
- Прежде всего, мне нравится, что однострочный формат гистограммы фиксированной ширины позволяет поместить все результаты рядышком в текстовом файле и помечать для сравнения.
- Мы видим ожидаемую разницу в 16 мс между временем отклика вверху и внизу экрана из-за времени сканирования строк для кадров на 60 Гц.
- Стандартное отклонение не сильно отличается от 4,6 мс от равномерного распределения результатов с временем обновления экрана 16 мс.
- У HP Z27 время отклика примерно на 30 мс больше, чем у Dell P2415Q! И это измерение до самого начала отображения символа. Я почти уверен, что на Z27 полное отображение также занимает больше времени. То есть на Z27 в редакторе Sublime почти половина всей задержки — это лишняя задержка с монитора!
Все измерения в остальной части статьи сделаны на моём Dell P2415Q. У обоих мониторов время отклика установлено на «быстрый», у Z27 есть ещё более быстрая настройка времени отклика, но она влияет только на время перехода с начала до конца отображения (transition time) и вводит неприглядные призрачные следы, не помогая уменьшить начальную задержку.
В большинстве известных способов качественно измерить задержку на самом деле сложнее, чем вы думаете. Чтобы получить реалистичные результаты, я приложил больше усилий, чем большинство экспериментаторов, но всё в первые несколько раз потерпел неудачу, так что пришлось исправлять ошибки.
Главная причина использования аппаратного тестера заключается в том, что существует множество неполных и ложных способов измерения сквозной задержки.
Есть известная и действительно отличная статья под названием «Печатаем с удовольствием» с хорошим анализом и красивыми графиками. Там сравнивается задержка различных текстовых редакторов на разных операционных системах. Но автор реализовал измерения через имитацию входных событий и скрапинг экрана с помощью системных API. Я сам не делал повторных измерений, поэтому не могу указать на что-то конкретно неправильное, но с этим подходом много потенциальных проблем. Например, проверка буфера экрана на CPU может неоправданно штрафовать приложения, которые рендерятся на GPU, из-за копий буфера окна в некоторых случаях, когда может cработать захват. Имитация входных данных может пойти по другому пути, чем реальные нажатия клавиш. Несмотря на это, даже в случае действительно качественных измерений и объективных результатов (а мы не можем действительно быть в этом уверены, не проверив их на сквозном тесте), эти результаты не говорят нам о полном времени отклика от начала до конца, что важно для реального опыта.
USB-опрос на 1000 Гц
Один из источников задержки, которую испытывают пользователи, но которую не измеряет мой тестер, — это задержка клавиатуры. Многие клавиатуры могут увеличить замеренное мной время отклика в два раза (как моя прошлая клавиатура) из-за 8-миллисекундных интервалов опроса USB, низкой скорости сканирования сетки клавиатуры, медленной прошивки и спорного механического дизайна.
Чтобы построить тестер задержки с низкой дисперсией для эмуляции клавиатуры нельзя просто взять любой микроконтроллер — обычно там по умолчанию реализован опрос с частотой 125 Гц. Мой микроконтроллер Teensy LC — один из немногих, кто по умолчанию использует 1000 Гц.
Обеспечение хорошего уровня сигнала
В первое время, когда я сконструировал свой тестер для времени отклика, я никак не измерял силу сигнала. В конце концов, я запутался: в одном и том же приложении, на том же экране в немного разных сценариях получались совершенно разные результаты. Я провёл некоторое тестирование и выяснил, что иногда на маленьких шрифтах с плохим размещении датчиков сигнал совсем слабенький, поэтому датчик срабатывает только на окончании вывода символа, хотя должен срабатывать в начале (это уже его собственный сложный субъективный выбор).
Я знал, что причина должна быть во времени полного перехода с начала до конца отображения, потому что ещё до написания прошивки я игрался с простым опросом оптического датчика каждую миллисекунду, используя для вывода результатов плоттер Arduino. Просто набирал и удалял символы, чтобы посмотреть на сигнал. Можете заметить, что в некоторых комбинациях оптического датчика и монитора полный переход занимает почти 100 мс. Но по видеосъёмке с Is It Snappy создаётся впечатление, что на мониторе Z27 полный переход занимает всего около 20 мс.
Для исправления я добавил измерение силы сигнала от пика к пику после полного перехода для проверки, что получаю адекватное разрешение измерений для своего порога в пять шагов к началу перехода. Это цифра, которую вы видите после q=
. Как выяснилось, очень важно установить большой размер шрифта и высокую яркость экрана.
Сильный разброс от маленьких различий
Вполне возможно, что кажущиеся незначительными различия в объекте измерений приведут к серьёзным различиям задержки. Например, я хотел посмотреть, есть ли существенная разница между временем отклика Sublime и VSCode при выделении текста в маленьком файле по сравнению с большим файлом со сложной грамматикой выделения и всплывающим окном автозаполнения. Конечно, разница есть. Но когда я заметил некоторый разброс значений, то провёл ещё несколько тестов и обнаружил, что время отклика сильно отличается между вводом 'а' в пустой строке и вводом 'а' после предыдущего 'а' ('аа').
Вот результаты создания новой строки после 3469-й строки в большом файле parser.rs на 6199 строк. Все результаты получены после аналогичного позиционирования датчика ближе к нижней части монитора Dell.
lat i= 40.2 +/- 4.1, a= 40, d= 39 (n= 38,q= 90) | _89 | sublime small .txt lat i= 41.2 +/- 6.9, a= 41, d= 42 (n= 54,q= 92) | 992 | sublime aa parser.rs lat i= 43.6 +/- 6.1, a= 43, d= 42 (n= 48,q=100) | 492 | lat i= 52.2 +/- 6.0, a= 52, d= 52 (n= 26,q=100) | 49 | lat i= 44.3 +/- 5.6, a= 43, d= 42 (n= 45,q=100) | 391 | lat i= 42.7 +/- 7.6, a= 42, d= 42 (n= 46,q=100) | _491 | lat i= 48.1 +/- 6.8, a= 49, d= 50 (n= 43,q= 89) | 269 | sublime a parser.rs lat i= 43.9 +/- 5.4, a= 48, d= 52 (n= 32,q= 97) | 197 | lat i= 47.8 +/- 8.4, a= 49, d= 49 (n= 29,q= 97) | 197_ | lat i= 46.1 +/- 6.8, a= 47, d= 49 (n= 42,q= 97) | 196_ | lat i= 63.3 +/- 9.3, a= 63, d= 62 (n= 68,q=118) | _963__ | vscode aa parser.rs lat i= 63.6 +/- 7.6, a= 64, d= 65 (n= 71,q=139) | _49__ _ | lat i= 62.3 +/- 6.3, a= 61, d= 59 (n= 52,q=132) | _791 | lat i= 62.0 +/- 5.8, a= 61, d= 60 (n= 40,q=111) | _49_ | lat i= 61.9 +/- 9.7, a= 62, d= 61 (n= 35,q=111) | 981_ | lat i= 53.1 +/- 7.7, a= 51, d= 49 (n= 54,q=116) | _79__ | vscode a parser.rs lat i= 52.2 +/- 6.3, a= 52, d= 51 (n= 41,q=133) | 692 | lat i= 53.2 +/- 7.8, a= 52, d= 52 (n= 57,q=134) | 591_ | lat i= 52.1 +/- 7.1, a= 52, d= 52 (n= 55,q=134) | 591_ |
Я сделал множество измерений в разное время и с незначительными вариациями, чтобы подтвердить эффект. Как видите, между измерениями в одном и том же сценарии есть некоторые различия, но гораздо больше вариаций между простым вводом 'а' и добавлением 'а' после существующего 'а'. Посмотрите на столбец a=
, который включает в себя результаты и вставки, и удаления, поэтому там наименьший шум. Sublime быстрее вводит второй символ в последовательности 'aa', чем первый, а VSCode — наоборот.
В обоих редакторах 'aa' заставляет появиться и исчезнуть всплывающее окно автозаполнения для выбора варианта между двумя списками и буквами 'a'. Могу предположить, что Sublime медленнее в случае 'а', потому что открытие и закрытие всплывающего окна автозаполнения занимает определённое время, но у меня нет чёткой версии, почему VSCode медленнее в случае 'aa' как при вставке, так и при удалении.
Рассинхрон с обновлением экрана
Потом я заметил, что значения стандартных отклонений какие-то подозрительно низкие. Иногда стандартное отклонение получалось 1 мс, хотя по логике оно должно превышать 4,6 мс из-за интервалов обновления экрана 16 мс.
Я посмотрел на свой код и понял, что измерения случайно синхронизируются с обновлениями экрана. После каждого измерения прошивка ждала ровно 300 мс, прежде чем снова набрать 'a' или удалить символ и произвести следующее измерение. Это значит, что входные данные всегда отправлялись примерно через 300 мс после обновления экрана, довольно стабильно попадая в один и тот же интервал обновлений. Я исправил эту проблему, добавив между измерениями случайную задержку 50 мс, такой своеобразный джиттеринг.
В основном, эта ошибка вызывала слишком низкие отклонения, но могла привести и к неправильным средним значениям, если приложение пропустит дедлайн для отрисовки, когда входное событие приходит в конце фрейма. Я обнаружил это во время тестирования для этой статьи и уже не мог повторять все предыдущие тесты, поэтому кое-где можете заметить некоторые низкие значения отклонений. Но я перепроверил средние значения по важным тестам, таким как Sublime и VSCode.
Я протестировал время отклика нескольких текстовых редакторов на одном и том же обычном текстовом файле. Но как я заметил выше, тесты сделаны до реализации рассинхрона с обновлением экрана через джиттеринг. Повторные тесты проведены только для Sublime и VSCode.
lat i= 32.5 +/- 4.0, a= 34, d= 35 (n= 38,q= 78) | 9_ | sublime text lat i= 33.4 +/- 1.4, a= 33, d= 33 (n= 68,q= 23) | _9 | textedit lat i= 47.6 +/- 7.0, a= 47, d= 47 (n= 71,q=130) | 219 | vscode lat i= 34.2 +/- 3.5, a= 34, d= 33 (n= 57,q= 37) | 9 _ | chrome html input lat i= 33.2 +/- 1.1, a= 33, d= 33 (n= 55,q= 30) | 9 | stock mac emacs lat i= 45.6 +/- 7.0, a= 43, d= 41 (n= 35,q= 56) | 992_ | atom lat i= 35.0 +/- 4.7, a= 35, d= 35 (n= 66,q= 11) | 9__ | xi
Учитывая отсутствие джиттеринга, по этим результатам я бы сказал, что все редакторы, кроме VSCode и Atom, работают одинаково отлично. И даже у этих двух при обычном наборе текста меньший штраф на время отклика, чем обычно у монитора или клавиатуры.
Я также измерил задержку в разных терминалах. Похоже, что у терминала Apple по умолчанию и у kitty оптимальное время отклика, в то время как iTerm2 и Alacritty выглядят чуть хуже.
lat i= 53.1 +/- 6.6, a= 54, d= 55 (n= 53,q= 59) | 291 _ | iterm2 gpu render lat i= 50.5 +/- 2.5, a= 50, d= 50 (n= 56,q= 59) | 19_ | iterm2 no gpu lat i= 35.8 +/- 7.0, a= 34, d= 33 (n= 73,q= 48) | 9___ | apple terminal lat i= 35.1 +/- 2.5, a= 34, d= 32 (n= 35,q= 52) | 9_ | apple terminal vim lat i= 50.4 +/- 3.9, a= 50, d= 49 (n= 60,q=269) | _59 | alacritty lat i= 36.1 +/- 5.6, a= 35, d= 34 (n= 78,q=199) | 9__ | kitty
Вот список деталей, которые я использовал:
- $12: Teensy LC или любой другой микроконтроллер Teensy 3+. Можете взять Arduino, но USB-библиотека Teensy опрашивает плату на частоте 1000 Гц (задержка 1 мс), в то время как большинство USB-устройств по умолчанию работают на частоте 125 Гц (дополнительные 8 мс случайной задержки в ваших измерениях). Впрочем, можете использовать любой микроконтроллер на 1000 Гц. Если не хотите припаивать пины, купите плату с уже припаянными, такой Teensy 3 стоит подороже.
- $12: модуль оптического датчика (Amazon предлагает пакет из десяти штук, я использовал только один). Можете изготовить собственную схему, но эти модули экономят кучу времени и легко подключаются к плате.
- $13: штатив для удерживания оптического датчика в стабильном положении возле экрана.
- Какая-нибудь кнопка/переключатель для запуска тестирования
- Провода для подключения оптического датчика, Teensy и кнопки
- Изолента для изготовления чёрного экрана, который ограничивает поле зрения датчика
- Кабель USB micro-B для подключения Teensy к компьютеру
Устройство можно собрать разными способами. Суть в том, что нужно просто каким-то образом подключить три провода (3V, заземление, аналоговый выход) от модуля оптического датчика к соответствующим пинам Teensy (3V, заземление и любой аналоговый пин). Самый простой способ сделать это, который даже не требует пайки, если вы купили Teensy с предварительно припаянными пинами — подключить три провода с джамперами мама-мама. Затем просто нужен какой-то переключатель, чтобы запустить тест. Тут один пин на Teensy подключается к земле, а другой к пину цифрового ввода-вывода. Если вы действительно ленивы, достаточно прикосновения двух проводов!
Чтобы убедиться, что модуль оптического датчика видит только ограниченную область экрана, я завернул его в маленький цилиндр из изоленты — и аккуратно отрезал конец ножницами. Получилось маленькое круглое окошко, которое можно прижать к экрану штативом, чтобы свести к минимуму внешние помехи и получить самый чистый сигнал.
Я уже раньше делал корпус для педали с Teensy LC и небольшой платой внутри, и у него сбоку был дополнительный разъём TRRS, который я специально поставил, ожидая в будущем подобного проекта, где он понадобится. Поэтому оставалось припаять оптический датчик к TRRS-кабелю. Тогда для управления тестированием можно использовать одну из моих педалей!
С пайкой мне повезло, потому что для одного проекта я как раз купил магнитные ручки, который можно использовать и в процессе пайки. К сожалению, выяснилось, что на самом деле у меня не так много больших кусков металла, на которые они могут прикрепиться, поэтому в конечном итоге для пайки я взял чугунную сковороду, а для операций на столе — свой вольфрамовый кубик (оказалось, что он слегка магнитится).
Предлагаю вам проявить фантазию и сделать что-нибудь более интересное, чем просто болтающиеся соединительные провода. Для своей педали я купил в местном магазине электроники пластиковую коробку, просверлил несколько отверстий по бокам, вставил аудиоразъёмы и поставил небольшую макетную плату, чтобы перенастраивать подключения. На Amazon куча педалей для татуировочных машин и электрических пианино с телефонными разъёмами ¼», можете выбрать из них. Вот мои любимые по тишине и тактильным ощущениям. Но есть более дешёвые варианты, которые могут быть ненадёжными, трудно нажиматься или неприятно шуметь.
Я бы не рекомендовал использовать для сенсорного модуля гнездо TRRS по моему примеру. Хотя оно хорошее и компактное и для него много доступных кабелей, но позже оказалось, что из-за него слишком часто закорачиваются разные соединения при подключении и отключении питания. Я попытался свести это к минимуму, разведя питание и заземление по противоположным концам платы, но вам советую взять кабель какого-нибудь более лучшего типа, может, телефонный.
Для определения начала и конца вывода символа на экран у меня не самая изысканная прошивка, но я потратил некоторое время на настройку, чтобы она хорошо работала, и добавил различные функции, поэтому рекомендую начать с неё. Установите Teensyduino, затем можете загрузить мой скетч Arduino, который работает для педали —, но вы можете закомментировать ненужные фрагменты и настроить его на использование правильных контактов. Затем всё просто — долгое нажатие на кнопку запускает измерения, а короткое нажатие выводит результаты!