client: Linux keystroke latency trails alacritty and foot #403
Labels
No labels
priority/P0
priority/P1
priority/P2
release/v0.1.0
status/blocked
status/planned
type/bug
type/design
type/test-gap
type/tracker
No milestone
No project
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
natsukium/felis#403
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Keystroke-to-pixel latency on Linux with
wl-latency(docs/reference/benchmarks.md, felis9d1d842f, ms):felis's key goes client → daemon → PTY → echo → daemon → client → present, two more hops than a single-process terminal.
Candidates:
crates/felis-render-wgpu/src/lib.rsconfiguresdesired_maximum_frame_latency: 2withPresentMode::AutoVsync; measure 1.Done when each hop is timed and the ones that wait are removed or shown to be needed.
Linux measurement (kilimanjaro, niri 60 Hz, NVIDIA through the pinned ICD described in #398). felis
mainef96bb46release build. Load average (1 min) is given with each figure.Each hop, timed with
strace -f -ttton both processesBoth processes ran under
strace -f -ttt -e trace=read,write,recvmsg,sendmsg,recvfrom,sendto. The session was a wrapper that sets a bar cursor and thenexec cat > /dev/null, as the latency suite does, so the echo comes from the line discipline.wtype atyped 10 keys 300 ms apart into the focused window (188x93, load about 4). Medians over the 10 keys, with strace's per-syscall overhead included, so absolute values are high by a few tens of µs per hop:recvmsg(wl)→ tokiosendto(sock, 20 B)recvfrom(20 B)write(master, 1 B)read(master) = 1sendto(sock, 247 B)recvfrom(247 B)sendmsg(wl)(attach/commit)Answers to the candidates:
ioctl/poll/futexshows, for one key: 0.57 ms of main-thread CPU with no syscalls, then the GPU submitioctl, a 0.12 mspollon the render fence, then the commit.What the 0.57 ms is: the whole grid is rebuilt and re-uploaded per keystroke
The client under
perf record -e cycles:u --call-graph lbrwhile typing 200 keys 50 ms apart (188x93 grid), main thread, inclusive:Renderer::render69%, of which:Renderer::build_cell_instances(crates/felis-render-wgpu/src/lib.rs:1091) 27%:CellPainter::extend_instances(crates/felis-render-wgpu/src/instances.rs:403) 14.8% self,resolve_cell_colors(instances.rs:480) 7.4%,ScreenBuffer::cell6.6%.Renderer::upload_frame_buffers(lib.rs:1339) →Queue::write_buffer19.7%, almost all of itmemmove(17%): the full instance buffer is copied every frame.populate_glyphs6%,queue.submit6%,CommandEncoder::finish5%.One changed cell costs a full-grid instance rebuild plus a full-buffer upload, so the cost scales with window area, not with the change.
desired_maximum_frame_latency: 1(local patch, not committed)wl-latencythrough the harness (crossterm.py run latency --terminals foot, 200 samples, 150 ms delay), alternating builds:No gain, and the commit is already issued right after the render fence, so a second frame in flight is not what the key waits on. Keep 2.
Where the rest of the gap is
felis's own path, key → commit, is about 1.9 ms even with strace overhead included. The instrument reads 5.2-5.5 ms, so about 3.4 ms falls after the commit: the compositor latching and compositing felis's GPU buffer, plus screencopy. foot's whole path is 2.3 ms with an shm buffer. The published alacritty figure (4.6 ms), also a GPU buffer, suggests most of that post-commit time is shared by GPU-buffer clients on this compositor and driver, not specific to felis. I did not instrument niri, so this attribution is by elimination.
Proposed fix, with expected gain
queue.write_bufferat an offset), instead ofbuild_cell_instancesplus a fullupload_frame_buffersper frame. Expected: most of the 0.5-0.6 ms frame build per keystroke at 188x93, less at the harness's 106x35, and less CPU per frame under floods (#398/#397).desired_maximum_frame_latencystays at 2 (measured, no gain from 1).