Repository navigation
native: terminal output rides the busy ring's frame; the ring at 24fps - #1011
Merged
Merged
Conversation
A busy ring asked for 30 frames a second and every chunk of the session's output asked for one of its own, so a streaming agent under a spinning ring drew 41 frames a second in a real window, each costing the same fixed GPU time whatever it showed. Theme.Loop now records the frame it asked for in a LoopBeat the window shares with its terminals, and output that lands while one is due waits for it; output within 200ms of a write to the PTY, a keystroke's echo, still draws at once. The ring runs at 24fps, film's rate, which on a 22px ring reads the same as 30.
2 tasks done
omartelo
added a commit
that referenced
this pull request
Nov 1, 2026
Measured after #1011, a busy session with its agent streaming still drew 37 frames a second, not 24: the terminal's answers to the program's queries (cursor position, device attributes, which Claude Code asks for as it draws) went through send and counted as someone typing, so the output that followed always drew at once instead of riding the ring's frame. Replies now go through their own write and leave the echo window closed.
4 tasks done
omartelo
added a commit
that referenced
this pull request
Nov 2, 2026
… beat, a cursor row left alone (#1013) * native: fewer idle frames: a slow ring without focus, a rarer service beat, a cursor row left alone Every frame costs the window the same whatever it shows, about 0.3% of a laptop GPU, so what is left after #1011 is the frames nobody asked for: - Without focus the ring stopped dead; now it turns at 8fps, so a session is still seen working from another monitor, at a fifth of the frames. - A running service's dot beat every 2.4s, a burst of frames each time: 8 frames a second all day for a 14dp dot. It beats every 6s. - The terminal recomposed and re-uploaded the cursor's row on every frame while the cursor stayed put. The row is recomposed when the cursor moves or the row's own text changes. * native: the surplus-frame test judges its own log line TestBrowserFrameSurplusDecodesOnlyTheNewest captured the process log and wanted it empty, but a layout save of an earlier test, failing after that test's backend closed, logs there too: on the macOS runner it did. The test now looks for the decode's own line, logged under the event's name.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Measured on a real window (1704x1386, one busy session with its agent streaming): 41 frames a second, 13.6% of the GPU. The busy ring asks for 30 frames a second, and every chunk of the session's output asks for a frame of its own; the two never line up, so they add. Each frame costs the same fixed GPU time whatever it shows, so on this stack the window's cost is simply frames × that.
Same rig, same 12fps TUI spinner, same GPU: Ghostty 0.8–1.4%, Kitty 2.6–7.4%, lich with the session idle (only the TUI asking) 4–12% at 12fps, lich with the session busy 7–11% at 36fps.
What
Theme.Looprecords the frame it asked for in aui.LoopBeatthe window shares with its terminals. Output that lands while a looping frame is due waits for it instead of asking for its own: a busy ring and a streaming agent cost one frame rate. Output within 200ms of a write to the PTY is a keystroke's echo and draws at once, as before. With nothing looping, output draws at once, as before.ui.LoopFramegoes from 33ms to 42ms: 24fps, film's rate. On a 22px ring the arc's tip moves 2.6px a frame instead of 2.1, and reads the same.Together, a busy session with a streaming agent draws 24 frames a second instead of 41.
Test plan
TestLoopingAnimationsRedrawAt24fps,TestLoopRecordsTheFrameItAskedFor,TestStillThemeHoldsTheFrameTestOutputRidesTheLoopingFrame: output asks for its own frame with nothing looping, rides a frame that is due, asks again once it is pastTestAKeystrokesEchoNeverWaitsnative/: gofmt,go vet ./...,go test ./...