Repository navigation
perf(terminal): replay in line with output, cheaper font steps and output path - #925
Merged
Merged
Conversation
…ith its output terminal.Replay queues the session's tail on /ws as a replay frame, behind the output already emitted and ahead of the rest, and drops what the coalescer held, which the tail already carries. The window registers the terminal before asking, takes no output until the replay frame lands, and sends none of the terminal's replies to queries inside the tail. Before, the window wrote the tail returned by the RPC into a terminal wired to the PTY: the answers to queries in it (nvim's device attributes, cursor reports) were typed into the session, and output emitted between the snapshot and the terminal's registration was lost. The RPC now returns whether a replay frame is coming. Tests that read the tail through Replay read the buffer directly: the contract changed.
Every fc-match is a subprocess, and a terminal asked for its family's four styles each time its renderer was built: a card's first show and every font size step, inside the frame. Locate keeps each answer by the font asked for. A family installed after it was asked for keeps its substitute until lich restarts, written down in docs/ceilings.md.
A size step built a new renderer for every open terminal, loading the family's faces again inside the frame. Renderer gains SetSize, which the next Measure takes in the same way it takes a new display scale; a new family still builds a renderer.
A hidden session flooding at ~147 MB/s made the backend allocate about 7.7x its output: the coalescer dropped its batch buffer after every flush and grew a new one, the replay tail allocated a chunk per PTY read, and the outbox copied each batch only for the transport to copy it again into the /ws frame. - The coalescer keeps its batch buffer across flushes; emit already may not retain it. - The replay tail is one buffer grown up to its 2 MB cap and then overwritten from its oldest byte. - The outbox copies a batch straight into its /ws frame behind the session's header, and the transport sends that frame as is. Start refuses a session id no frame header can carry (over 255 bytes) before its PTY spawns, where its output used to fall back to /events, which the window does not read. BenchmarkHiddenFlood (new): 232 KB allocated per 32 KB read, 2 allocs, down to 34 KB and 0 allocs. TestReplayBufferKeepsOneChunkLargerThanCap is replaced by TestReplayBufferKeepsTheEndOfAWriteLargerThanCap: the contract changed, a write larger than the cap now keeps its last capBytes (a PTY read never is one). The transport tests build frames with frameFor, since send takes a frame.
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.
Summary
Four fixes from a stress run of the native terminal (floods, nvim, Claude Code and Codex sessions, splits, font and layout changes), one commit each.
fix(terminal)). The window wrote the replayed tail into a terminal already wired to the PTY, so the terminal answered the queries in it again (nvim's device attributes, cursor reports) and the answers landed on the session's input:command not foundin a shell, stray text in an agent's prompt. Output emitted between the replay snapshot and the terminal's registration was dropped.terminal.Replaynow queues the tail on/wsas a replay frame ([0][id length][id][tail]), in line with the output frames and with what the coalescer held folded into it; the window registers the terminal first, takes no output until the replay frame and sends no replies while it writes it. The RPC returns whether a replay frame is coming.perf(fonts)). Every terminal renderer built ran fourfc-matchsubprocesses inside the frame.fonts.Locatekeeps each answer for the run; the trap this sets (a family installed while lich runs keeps its substitute) is indocs/ceilings.md.perf(native)). It built a new renderer per open terminal.RenderergainsSetSize, taken in by the nextMeasurethe way a new display scale is.perf(terminal)). A hidden flood allocated about 7.7x its output in the backend. The coalescer keeps its batch buffer, the replay tail is one buffer grown up to its 2 MB cap and then overwritten, and the outbox copies a batch straight into its/wsframe.Startrefuses a session id over 255 bytes, which no frame header can carry, before its PTY spawns.Measured
A/B against
564f74c89on a rig (own backend, virtual display, interleaved runs):seq 1 2000000, card opened mid-floodVisible flood throughput and fps, hidden flood backend CPU and idle CPU stayed within noise.
BenchmarkHiddenFlood(new) for the last commit: 232 KB allocated per 32 KB read, 2 allocs, down to 34 KB and 0 allocs.Tests changed
respawn_testandsandbox_spawn_linux_testhelpers read the tail from the replay buffer:Replayno longer returns it (contract changed).TestReplayBufferKeepsOneChunkLargerThanCapis replaced byTestReplayBufferKeepsTheEndOfAWriteLargerThanCap: a write larger than the cap now keeps its last 2 MB (contract changed; a PTY read is 32 KB).frameFor, sincesendtakes a frame.Test plan
gofmt,go vet ./...,go test ./...at the root and innative/;internal/terminalwith-raceTestServiceReplayQueuesTheTailBetweenOutputFrames(fails with the coalescer's batch kept on a cut),TestAttachedTerminalStartsFromTheReplay(fails both with output taken before the replay and with replies sent during it)TestLocateAsksFontconfigOncePerFont(fails without the cache),TestFontSizeStepResizesTheRenderersItHas(fails on the rebuild path)TestReplayBufferKeepsTheLastCapBytesAcrossReads,TestStartRefusesAnIDNoFrameCanCarrygo test ./api -update:terminal.Replaynow returnsbool