wake the renderer on pty output instead of polling at 33 fps

This commit is contained in:
Vaclav Tvrdik
2026-09-10 11:21:55 +02:00
parent 2a8fffb9c6
commit 28c9c398a1
3 changed files with 27 additions and 2 deletions

View File

@@ -264,6 +264,7 @@ class VtermClearScreenCommand(sublime_plugin.TextCommand):
return return
# the renderer thread owns the screen; it applies this on its next tick # the renderer thread owns the screen; it applies this on its next tick
terminal.pending_clear_screen = True terminal.pending_clear_screen = True
terminal.wake()
self.view.run_command("vterm_clear_undo_stack") self.view.run_command("vterm_clear_undo_stack")
def is_enabled(self): def is_enabled(self):

View File

@@ -27,6 +27,7 @@ class VtermCoreEventListener(sublime_plugin.EventListener):
# background — via the renderer tick: a render here would race the # background — via the renderer tick: a render here would race the
# renderer thread, which owns the emulator # renderer thread, which owns the emulator
terminal.pending_render = True terminal.pending_render = True
terminal.wake()
view.run_command("vterm_show_cursor", {"scroll": False}) view.run_command("vterm_show_cursor", {"scroll": False})
elif view.settings().get("vterm_view") and not view.settings().get("vterm_view.finished"): elif view.settings().get("vterm_view") and not view.settings().get("vterm_view.finished"):
# the plugin host restarted; this tab's terminal is unreachable # the plugin host restarted; this tab's terminal is unreachable

View File

@@ -30,6 +30,9 @@ FEED_LIMIT = 256 * 1024
# SUPPRESS_HISTORY_AFTER_CLEAR in emulator.py must outlast this # SUPPRESS_HISTORY_AFTER_CLEAR in emulator.py must outlast this
REPAINT_NUDGE_DELAY = 0.25 REPAINT_NUDGE_DELAY = 0.25
# extra wait once a tick finds nothing pending, on top of RENDER_PERIOD.
IDLE_TICK = 0.2
def view_size(view, default=None): def view_size(view, default=None):
"""Terminal (rows, columns) that fit the view's viewport.""" """Terminal (rows, columns) that fit the view's viewport."""
@@ -116,6 +119,12 @@ class Terminal:
self.pending_render = False self.pending_render = False
# a UTF-8 sequence split across read chunks must not be dropped # a UTF-8 sequence split across read chunks must not be dropped
self._decoder = codecs.getincrementaldecoder("utf-8")("replace") self._decoder = codecs.getincrementaldecoder("utf-8")("replace")
# set by the reader (and the UI thread) to end the renderer's idle park
self._wake = threading.Event()
def wake(self):
"""Cut short the renderer's idle wait, for a flag set from another thread"""
self._wake.set()
@classmethod @classmethod
def from_id(cls, vid): def from_id(cls, vid):
@@ -203,6 +212,7 @@ class Terminal:
"""Terminate the child and drop this terminal from the registry.""" """Terminate the child and drop this terminal from the registry."""
LOG.debug("[kill] terminating process") LOG.debug("[kill] terminating process")
self.alive = False self.alive = False
self._wake.set()
scheme_manager.remove_listener(self.apply_scheme) scheme_manager.remove_listener(self.apply_scheme)
try: try:
self.process.terminate() self.process.terminate()
@@ -254,11 +264,13 @@ class Terminal:
except EOFError: except EOFError:
break break
chunks.append(self._decoder.decode(raw)) chunks.append(self._decoder.decode(raw))
self._wake.set()
if done[0] or not self.is_hosted(): if done[0] or not self.is_hosted():
LOG.debug("[reader] stops") LOG.debug("[reader] stops")
break break
self.alive = False self.alive = False
done[0] = True done[0] = True
self._wake.set()
def feed_data(): def feed_data():
"""Parse up to FEED_LIMIT of pending output into the screen.""" """Parse up to FEED_LIMIT of pending output into the screen."""
@@ -280,9 +292,10 @@ class Terminal:
"""Own the emulator: parse, then ask sublime to redraw, once per tick.""" """Own the emulator: parse, then ask sublime to redraw, once per tick."""
while True: while True:
# a hidden tab is not drawn at all: the screen keeps the state # a hidden tab is not drawn at all: the screen keeps the state
# and one render on unhide catches the view up. The tick stays at # and one render on unhide catches the view up.
# full rate either way, or the parser would fall behind the pty
visible = self.is_visible() visible = self.is_visible()
self._wake.clear()
with intermission(period=RENDER_PERIOD): with intermission(period=RENDER_PERIOD):
feed_data() feed_data()
if self.screen.in_sync_frame(): if self.screen.in_sync_frame():
@@ -300,6 +313,16 @@ class Terminal:
LOG.debug("[renderer] stops") LOG.debug("[renderer] stops")
break break
# nothing left to do: park until woken, instead of spinning at
# RENDER_PERIOD. A sync frame or a teardown skipped this by
# leaving the block above, and keeps its own cadence.
# dirty rows only count while visible: nothing clears them for a
# hidden tab, and on_activated wakes us with pending_render
if not (
chunks or self.pending_render or self.pending_clear or self.pending_clear_screen or (visible and self.screen.dirty)
):
self._wake.wait(IDLE_TICK)
while chunks: while chunks:
feed_data() feed_data()
done[0] = True done[0] = True