Repository navigation
perf(painter): resend only the menu rows that changed - #1296
Open
bhouse-nexthop wants to merge 1 commit into
Open
bhouse-nexthop wants to merge 1 commit into
bhouse-nexthop wants to merge 1 commit into
Conversation
Every repaint erased from the prompt down and printed the whole menu again, even when the menu had not changed. Over a slow link that is the dominant cost: a 64-entry completion grid is about 1.2 KB, so on a 9600 baud serial console each arrow key under an open menu took over a second to draw. The painter now remembers the menu rows it drew. While the menu opens on the same row of an unscrolled screen of the same size, a repaint erases only the rows above the menu and redraws only the menu rows that differ. Moving the selection with the arrow keys, or with Tab when partial completions are off, redraws the one or two rows whose highlight moved, and a repaint whose menu is unchanged sends only the prompt and buffer. The menu is drawn whole when any of that fails, and when most rows changed, since each changed row pays for its own cursor move and erase. Each remembered row carries the style in effect where it starts, since a description menu opens a color on one row and resets it rows later; a redrawn row starts from that style and ends reset. Every path that writes past the painter, or moves the anchor, forgets the remembered rows: external messages, resizes, a stale anchor, clears and the exit path. A row wider than the screen wraps and moves the rows below it, and a bare line feed (a list menu's multi-line description) starts the next row mid-screen, so such a menu is always drawn whole, as is one the text after the cursor runs into. A menu taller than the screen and a dumb terminal are always drawn whole. Typing under a menu fed by a host completer, and Tab with partial completions on, still redraw it whole: the engine marks the anchor stale whenever such a completer runs, and a stale anchor drops the remembered rows. Signed-off-by: Brad House <bhouse@nexthop.ai>
bhouse-nexthop
force-pushed
the
painter-keep-unchanged-menu-rows
branch
2 times, most recently
from
November 12, 2026 05:27
3c07781 to
d9c1848
Compare
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
Every repaint erased from the prompt down and printed the whole completion menu again, even when nothing in it had changed. On a 9600 baud serial console that is the dominant cost: a 64-entry
ColumnarMenuis about 3.6 KB with colors, so each arrow key under an open menu takes several seconds to draw.The painter now remembers the menu rows it drew. While the menu opens on the same row of an unscrolled screen of the same size, a repaint erases only the rows above the menu and resends only the menu rows that differ. Each remembered row carries the SGR state in effect where it starts, so a style that spans rows (a
DescriptionMenudescription) comes out the same on a redrawn row.The menu is drawn whole whenever row-for-row reuse can't be proven to match a full paint:
ListMenumulti-line description)TERM=dumbAnything that writes past the painter forgets the remembered rows: external messages, resizes, clears,
$EDITOR, and the exit path.There is no public API change.
Before / After
Bytes sent per event: a 64-entry
ColumnarMenu, 80x24,DefaultCompleter, partial completions off, measured through the engine.Typing still redraws the menu whole. The engine marks the anchor stale whenever a host completer runs (#1130), and a stale anchor drops the remembered rows. #1297 lets an in-process completer opt out of that. Tab with partial completions on goes through the same path.
Additional notes
The tests compare screens rendered by a vt100 emulator, cell by cell with attributes and cursor, against a full paint of the same frame. They cover ANSI on and off; right and multi-line prompts; status lines; menus that grow, shrink, wrap or end on the last row; wide characters; tabs; bare line feeds; SGR spanning rows, including resets inside parameter lists and zero-valued color arguments; and the
DescriptionMenuandListMenupaths through the engine.Two engine tests (
columnar_menu_below_the_status_line_ignores_the_hidden_hint,status_line_stays_above_a_columnar_menu_taller_than_the_screen) fail onmainwhen the checkout path is long, becauseDefaultPromptprints the cwd and wraps. They pass from a short path, with or without this change.