[Перевод] Измеряем задержку от клавиатуры до фотона с помощью оптического датчика

Для измерения времени отклика или задержки (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 пропорционально представляют собой, насколько заполнен этот бакет. Символ _ означает бакет с по крайней мере одним образцом, но его недостаточно до заполнения одной девятой бакета, так что это хвост задержек с малым количеством случайных образцов. Вот как это выглядит (на фотографии мониторы в портретном режиме, но все тесты делались в ландшафтном):

4151f5016c68014682c8e09eac08b240.jpg

Я также сделал так, что при повторном нажатии кнопки выводятся все результаты измерений, например, [35, 35, 33, 44], так что можно сгенерировать график:

3267b97ef54e907c5d656dd0b7ae75d2.png


Начну с моего любимого набора результатов:

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 мс.

d3250002bac009103ed40a5c47cb56bc.png

Для исправления я добавил измерение силы сигнала от пика к пику после полного перехода для проверки, что получаю адекватное разрешение измерений для своего порога в пять шагов к началу перехода. Это цифра, которую вы видите после 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-кабелю. Тогда для управления тестированием можно использовать одну из моих педалей!

С пайкой мне повезло, потому что для одного проекта я как раз купил магнитные ручки, который можно использовать и в процессе пайки. К сожалению, выяснилось, что на самом деле у меня не так много больших кусков металла, на которые они могут прикрепиться, поэтому в конечном итоге для пайки я взял чугунную сковороду, а для операций на столе — свой вольфрамовый кубик (оказалось, что он слегка магнитится).

ccfae58364a964a71628451dcb1e9feb.jpg

Предлагаю вам проявить фантазию и сделать что-нибудь более интересное, чем просто болтающиеся соединительные провода. Для своей педали я купил в местном магазине электроники пластиковую коробку, просверлил несколько отверстий по бокам, вставил аудиоразъёмы и поставил небольшую макетную плату, чтобы перенастраивать подключения. На Amazon куча педалей для татуировочных машин и электрических пианино с телефонными разъёмами ¼», можете выбрать из них. Вот мои любимые по тишине и тактильным ощущениям. Но есть более дешёвые варианты, которые могут быть ненадёжными, трудно нажиматься или неприятно шуметь.

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

3d5fcecfdc63a4901b487091142a09c1.jpg


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

© Habrahabr.ru