From 01a88f7b19dc302b121e2c9427737ccc7ca08955 Mon Sep 17 00:00:00 2001 From: Vaclav Tvrdik Date: Thu, 10 Sep 2026 13:03:15 +0200 Subject: [PATCH] halve the parse cost of coloured output and keep drawing during bursts --- vterm/emulator.py | 30 +++++++++++++++++++++--------- vterm/terminal.py | 4 ++-- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/vterm/emulator.py b/vterm/emulator.py index c21669a..f1cac45 100644 --- a/vterm/emulator.py +++ b/vterm/emulator.py @@ -147,7 +147,9 @@ class VtScreen(pyte.Screen): self._default_char = Char(data=" ", fg="default", bg="default", reverse=False) # SGR transitions repeat endlessly in TUI output; memoized per (attrs, params) self._sgr_cache = {} - # chars are identical while cursor.attrs is unchanged (draw rebuilds per run) + # one char -> Char cache per attribute set; attrs recur constantly in + # coloured output, so the caches are kept rather than rebuilt on a change + self._char_caches = {} self._char_cache = {} self._cache_attrs = None self.history = deque(maxlen=history) @@ -164,6 +166,8 @@ class VtScreen(pyte.Screen): """RIS: reset the screen along with the cursor, scrollback and caches""" self._default_char = Char(data=" ", fg="default", bg="default", reverse=False) self._sgr_cache.clear() + self._char_caches.clear() + self._cache_attrs = None super().reset() self.cursor = Cursor(0, 0) self.cursor_style = 0 @@ -331,10 +335,17 @@ class VtScreen(pyte.Screen): cursor = self.cursor buffer = self.buffer columns = self.columns - # per-run cache: cells differ only by their data while attrs are unchanged - if cursor.attrs is not self._cache_attrs: - self._cache_attrs = cursor.attrs - self._char_cache = {} + attrs = cursor.attrs + # cells differ only by their data within one attribute set; the cache is + # picked per set, not discarded, or colour changes would thrash it + if attrs is not self._cache_attrs: + self._cache_attrs = attrs + cache = self._char_caches.get(attrs) + if cache is None: + if len(self._char_caches) > 256: + self._char_caches.clear() + cache = self._char_caches[attrs] = {} + self._char_cache = cache cache = self._char_cache line = buffer[cursor.y] @@ -356,12 +367,12 @@ class VtScreen(pyte.Screen): self.insert_characters(char_width) cell = cache.get(char) if cell is None: - cell = cache[char] = cursor.attrs._replace(data=char) + cell = cache[char] = attrs._replace(data=char) line[cursor.x] = cell if char_width == 2 and cursor.x + 1 < columns: filler = cache.get("") if filler is None: - filler = cache[""] = cursor.attrs._replace(data="") + filler = cache[""] = attrs._replace(data="") line[cursor.x + 1] = filler # a combining mark belongs to the cell before it, which may be the # last column of the previous row @@ -391,9 +402,10 @@ class VtScreen(pyte.Screen): break if char_width > 0: - self.cursor.x = min(self.cursor.x + char_width, self.columns) + x = cursor.x + char_width + cursor.x = x if x < columns else columns - self.dirty.add(self.cursor.y) + self.dirty.add(cursor.y) def index(self): """Line feed; at the bottom of the primary screen the top row goes to scrollback.""" diff --git a/vterm/terminal.py b/vterm/terminal.py index 76b6c4a..cb92688 100644 --- a/vterm/terminal.py +++ b/vterm/terminal.py @@ -24,8 +24,8 @@ RENDER_PERIOD = 0.03 # draw rate for a visible terminal the user is not typing in, ~15 fps UNFOCUSED_RENDER_PERIOD = 0.075 -# max chars parsed per tick, so a giant burst still renders intermediate frames -FEED_LIMIT = 256 * 1024 +# max chars parsed per tick, so a giant burst still renders intermediate frames. +FEED_LIMIT = 48 * 1024 # SUPPRESS_HISTORY_AFTER_CLEAR in emulator.py must outlast this REPAINT_NUDGE_DELAY = 0.25