From 806d16d0dfd01973e0fb1fbdfb8a9491675ae295 Mon Sep 17 00:00:00 2001 From: aliou Date: Sat, 27 Jun 2026 22:30:52 +0200 Subject: [PATCH] docs: add keyboard-latency-tracer and document the modtap latency fix --- AGENTS.md | 23 ++ README.md | 7 +- .../README.md | 95 ++++++ docs/README.md | 1 + tools/keyboard-latency-tracer/.gitignore | 1 + tools/keyboard-latency-tracer/Makefile | 10 + tools/keyboard-latency-tracer/README.md | 74 +++++ tools/keyboard-latency-tracer/tracer.c | 295 ++++++++++++++++++ 8 files changed, 505 insertions(+), 1 deletion(-) create mode 100644 docs/2026-06-19-keyboard-latency-investigation/README.md create mode 100644 tools/keyboard-latency-tracer/.gitignore create mode 100644 tools/keyboard-latency-tracer/Makefile create mode 100644 tools/keyboard-latency-tracer/README.md create mode 100644 tools/keyboard-latency-tracer/tracer.c diff --git a/AGENTS.md b/AGENTS.md index 59e7dee..390e117 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -148,6 +148,29 @@ The geometry and labels are hand-maintained in `src/keyboards.ts`; update that file whenever a keymap changes. See `tools/keyboard-tester/AGENTS.md` for the sub-project conventions. +### Latency tracer (`tools/keyboard-latency-tracer/`) + +Tiny macOS CGEventTap used to measure the gap between a mod-tap Control +modifier going down and the next key going down. It was built to confirm the +Mirage's `LCTL_T(KC_ESC)` felt sluggish on `Ctrl + anything` and to verify +that switching to eager per-row debounce fixed it. It only observes events; +it does not inject or swallow them. + +Build requires Xcode command line tools: + +``` +cd tools/keyboard-latency-tracer +make +./tracer +``` + +The first run will request **Input Monitoring** permission for the terminal +app in System Settings. Stop the tracer with a bare `q`, 10 seconds of idle, +or `pkill tracer`. + +See `docs/2026-06-19-keyboard-latency-investigation/README.md` for the +measurement findings and the debounce fix. + ## Committing code - `git status` first; stage only files relevant to the change. Avoid diff --git a/README.md b/README.md index f9305cb..ff5d54e 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,8 @@ patches/ -- 0001: read-only tolerance. 0002: edthu wireless (NEO65). vendor/ -- edthu-wireless source (regen base) + stock qwertykeys binaries. scripts/ -- regen-wireless-patch.sh, ble-scan.swift/.py. justfile -- `just mirage`, `just neo`, `just bakeneko`, `just all`, ... -tools/keyboard-tester/ -- local switch-tester web app for rebuilding hotswap boards. +tools/keyboard-tester/ -- local switch-tester web app for rebuilding hotswap boards. +tools/keyboard-latency-tracer/ -- macOS CGEventTap to measure QMK modtap latency. ``` ## First run @@ -185,6 +186,10 @@ detected within `DEBOUNCE` ms. Measured on the Mirage: 7.4ms -> ~0.04ms. Combined with `HOLD_ON_OTHER_KEY_PRESS`, rolled Ctrl+key combos send `` instead of ``. +The latency was measured with `tools/keyboard-latency-tracer/`, a small macOS +CGEventTap that prints every OS-level key event with high-resolution +timestamps. See that directory's README and `docs/2026-06-19-keyboard-latency-investigation/README.md`. + ### macOS Fn / Globe double-tap (`CTL_DBL_FN`) Every active keymap (Mirage, NEO65, DB60) maps the bottom-left physical key diff --git a/docs/2026-06-19-keyboard-latency-investigation/README.md b/docs/2026-06-19-keyboard-latency-investigation/README.md new file mode 100644 index 0000000..234efb0 --- /dev/null +++ b/docs/2026-06-19-keyboard-latency-investigation/README.md @@ -0,0 +1,95 @@ +# Keyboard modtap latency investigation + +Dated: 2026-06-19 + +## Background + +With the Mirage keymap, the Caps position is a mod-tap (`LCTL_T(KC_ESC)`): +tap for Esc, hold for left Control. When rolling a `Ctrl + key` combo, the +interaction felt sluggish compared to a regular board. + +## Goals + +- Measure exactly how much latency QMK was adding between the physical press + and the OS receiving the Control modifier. +- Find a QMK configuration that reduces the latency to effectively zero. + +## Investigation + +We built `tools/keyboard-latency-tracer/`, a small macOS CGEventTap that logs +every OS-level key event with high-resolution timestamps. Running the same +sequence on the Mirage and on a non-modtap board confirmed that QMK's default +debounce strategy was holding the Control event. + +The relevant sequence is: + +1. Press and hold the mod-tap `LCTL_T(KC_ESC)` key. +2. Press a second key, e.g. `c`. + +With the default QMK debounce (`sym_defer_g`, 5ms), the press is not reported +until the debounce window has passed with no further bounce. This meant the +Control modifier arrived about 7.4ms after the physical key action. More +importantly, because the modifier and the rolled character are resolved +through the mod-tap machinery, the effective gap felt even larger in practice. + +## Decision + +Switch to eager per-row debounce (`sym_eager_pr`) with a 5ms window: + +```c +#define DEBOUNCE_TYPE sym_eager_pr +#define DEBOUNCE 5 +``` + +With eager debounce, the key-down is reported immediately. If noise is +detected inside the 5ms window, the report is reverted; otherwise it sticks. +This removes the deferral delay entirely. + +We also enabled `HOLD_ON_OTHER_KEY_PRESS` so that, when the mod-tap key is +held and a second key is pressed, QMK resolves the mod-tap as a held modifier +rather than a tap. This stops rolled combos from producing the tap output +(`Esc + key`) and instead emits `Ctrl + key`. + +## Results + +Measured latency on the Mirage: + +- Before (`sym_defer_g`): ~7.4ms between Control-going-down and the next key. +- After (`sym_eager_pr`): ~0.04ms. + +The interaction now feels identical to a board with dedicated modifiers. + +## Implementation + +Eager debounce and the hold-on-other-key-press flag are set in each active +keymap's `config.h`: + +- `keyboards/mode/m256wh/keymaps/mirage/config.h` +- `keyboards/neo/neo65_trimode/keymaps/aliou/config.h` + +(`sym_eager_pr` was present from the initial Mirage keymap commit. The NEO65 +keymap received the same treatment when it was brought up to match the Mirage +layout.) + +## Risks + +- Eager debounce can in theory report a noisy/bouncing switch press that a + deferred debounce would suppress. With `DEBOUNCE 5` ms this has not been a + problem in practice. +- `HOLD_ON_OTHER_KEY_PRESS` makes intentional tap+quick-roll sequences behave + like modifier holds. For this layout that is the desired behavior, but it + may not suit every keymap. + +## Status + +Solved. The tracer is kept in the repo as a reusable diagnostic tool under +`tools/keyboard-latency-tracer/`. + +## Affected files and commands + +- `tools/keyboard-latency-tracer/Makefile` +- `tools/keyboard-latency-tracer/tracer.c` +- `tools/keyboard-latency-tracer/README.md` +- `keyboards/mode/m256wh/keymaps/mirage/config.h` +- `keyboards/neo/neo65_trimode/keymaps/aliou/config.h` +- Build: `make` inside `tools/keyboard-latency-tracer/`, then `./tracer` diff --git a/docs/README.md b/docs/README.md index 16496cf..54c9dcd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,6 +4,7 @@ Dated decision/operation records and architecture notes for the keebs repo. | Date | Topic | File | |---|---|---| +| 2026-06-19 | Keyboard modtap latency investigation | `2026-06-19-keyboard-latency-investigation/README.md` | | 2026-06-20 | Archiving retired keymaps | `2026-06-20-archive-keyboards/README.md` | | 2026-06-20 | edthu wireless vendoring + NEO65 tri-mode support | `2026-06-20-edthu-wireless-neo65/README.md` | | 2026-06-20 | Auto-flash Nushell scripts + DFU polling | `2026-06-20-auto-flash-scripts/README.md` | diff --git a/tools/keyboard-latency-tracer/.gitignore b/tools/keyboard-latency-tracer/.gitignore new file mode 100644 index 0000000..282dd6b --- /dev/null +++ b/tools/keyboard-latency-tracer/.gitignore @@ -0,0 +1 @@ +tracer diff --git a/tools/keyboard-latency-tracer/Makefile b/tools/keyboard-latency-tracer/Makefile new file mode 100644 index 0000000..54f1031 --- /dev/null +++ b/tools/keyboard-latency-tracer/Makefile @@ -0,0 +1,10 @@ +CFLAGS = -O2 -Wall -Wextra +LDFLAGS = -framework ApplicationServices -framework CoreFoundation + +tracer: tracer.c + $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) + +clean: + rm -f tracer + +.PHONY: clean diff --git a/tools/keyboard-latency-tracer/README.md b/tools/keyboard-latency-tracer/README.md new file mode 100644 index 0000000..a3acffd --- /dev/null +++ b/tools/keyboard-latency-tracer/README.md @@ -0,0 +1,74 @@ +# keyboard-latency-tracer + +A tiny macOS host-side tool to debug QMK modtap latency (e.g. `LCTL_T(KC_ESC)` +on a Mirage build feeling sluggish when doing `Ctrl + anything`). + +It does NOT modify the keyboard. It only records every key event the OS sees, +with high-resolution timestamps, so you can measure the gap between the control +modifier going down and the next typed character going down. With a non-modded +keyboard (e.g. qk65) that gap is basically the time you take to roll your +fingers. With a modtap, if QMK is holding the modifier event until a second key +or `TAPPING_TERM` triggers it, you'll see it. + +## What it measures + +For every event it prints one line: + +``` + keycode= name=