Skip to content

fix(client): scroll ticks wait for the app's answer, and a slow app gets at most 12 a go - #52

Merged
jesse23 merged 1 commit into
mainfrom
fix/tick-backpressure
Oct 16, 2026
Merged

jesse23 merged 1 commit into
mainfrom
fix/tick-backpressure

Conversation

@jesse23

@jesse23 jesse23 commented Oct 16, 2026

Copy link
Copy Markdown
Owner

Description

Stacked on #51 (merge that first).

Scrolling Claude Code's fullscreen mode inside herdr lagged and then jumped a long way; Claude Code on its own, pi, and pi inside herdr were smooth, and 1.15 lagged the same way. Traced in the browser and on the server: webtty was idle and flow control never blocked; herdr redraws its whole screen (~35 KB, one synchronized update) for every scroll tick, starting 30–45 ms later, and with ticks paced about one a frame (ADR 045) each was redrawn separately, so a slow app fell behind and caught up in a jump.

Ticks wait for the app's answer. TickPacer sends no more ticks until the app has answered the last ones with output that leaves no synchronized update open (or 100 ms pass); the ticks due meanwhile go together. Apps that answer within a frame (pi, vim, Claude Code alone) get their ticks as before.

A slow app gets at most 12 ticks a go. While the app's answers average over 40 ms, a go sends at most 12 ticks and drops the rest past one more go. A real TrackPoint capture (hard pushes of 300–1,150 px every 15–60 ms) moved Claude Code inside herdr 24 lines an update and up to 190, unevenly; capped, 12 lines every update. Fast apps are never capped (identical replay).

Measured on a real herdr session at its size, a fast scroll: tick to output 31–42 ms at the 90th percentile and 45–48 ms worst, against 50–125 / 67–212 ms with paced ticks and 48–79 / 49–116 ms for 1.15. Scroll benchmark against the build before: unchanged for pi, vim, nvim, yazi, less, the shell, the stream test and pi inside herdr; Claude Code inside herdr 28.5 → 35.9 paints/s with a wheel and 375 → 83 ms p90 gap with a TrackPoint; Claude Code alone with a wheel paints less often for the same distance (46 → 36/s). Details in ADR 069.

Tests: ticks due while waiting go together, a slow app is capped and the excess dropped, a fast app is not, an unanswered app gets ticks after 100 ms, the end of a synchronized update is the answer (each fails without its rule). Unit tests (3091), lint, coverage targets and the architecture check pass.

ADRs

Base automatically changed from fix/paint-cost to main October 16, 2026 11:03
…ets at most 12 a go

Ticks sent to an app go out once its output has answered the last ones (leaving no
synchronized update open), the ticks due meanwhile together, so an app that redraws
slowly (Claude Code fullscreen inside herdr) no longer falls behind and jumps. While
the app's answers average over 40 ms, a go sends at most 12 ticks and drops the rest
past one more go, so a fast TrackPoint push moves evenly. ADR 069.
@jesse23
jesse23 force-pushed the fix/tick-backpressure branch from 0349557 to 3d389c1 Compare October 16, 2026 11:03
@jesse23
jesse23 merged commit bd35120 into main Oct 16, 2026
3 checks passed
@jesse23
jesse23 deleted the fix/tick-backpressure branch October 16, 2026 11:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant