Optimize screenshot updates using pyte dirty tracking

- get_screen_state() now returns has_changes flag indicating if screen changed
- pyte's dirty set tracks which rows have been modified since last read
- Screenshot handler returns cached SVG immediately when no changes detected
- Removed _screenshot_last_rendered_activity tracking (replaced by dirty flag)
- Added test for dirty flag behavior

Bump version to 0.1.16
This commit is contained in:
GitHub Copilot
2026-01-24 11:27:33 +00:00
parent 8ae3f77f23
commit ff8f5efabd
5 changed files with 54 additions and 34 deletions
+8 -21
View File
@@ -211,7 +211,6 @@ class LocalServer:
self._screenshot_cache_etag: dict[str, str] = {}
self._screenshot_locks: dict[str, asyncio.Lock] = {}
self._route_last_activity: dict[str, float] = {}
self._screenshot_last_rendered_activity: dict[str, float] = {}
@property
def app_count(self) -> int:
@@ -516,30 +515,21 @@ class LocalServer:
if session_process is None or not hasattr(session_process, "get_screen_state"):
raise web.HTTPNotFound(text="Session not found")
# If nothing has changed since the last render, serve cached screenshot without
# touching the session replay buffer.
last_activity = self._route_last_activity.get(route_key, 0.0)
last_rendered_activity = self._screenshot_last_rendered_activity.get(route_key, -1.0)
if last_activity <= last_rendered_activity:
# Get the actual screen state from the terminal session's pyte screen
# This includes has_changes flag from pyte's dirty tracking
screen_width, screen_height, screen_buffer, has_changes = await session_process.get_screen_state() # type: ignore[union-attr]
# If screen hasn't changed, serve cached screenshot immediately
cached = self._screenshot_cache.get(route_key)
if cached is not None and not has_changes:
cached_response = self._get_cached_screenshot_response(request, route_key)
if cached_response is not None:
return cached_response
# Get the actual screen state from the terminal session's pyte screen
# This uses the correct dimensions that match what the terminal is rendering
screen_width, screen_height, screen_buffer = await session_process.get_screen_state() # type: ignore[union-attr]
now = asyncio.get_event_loop().time()
ttl = self._get_screenshot_cache_ttl(route_key, now)
cached = self._screenshot_cache.get(route_key)
# If we have a cached screenshot and the session is idle, keep serving it until
# new activity occurs (no periodic re-render).
if cached is not None and self._route_last_activity.get(route_key, 0.0) == 0.0:
cached_response = self._get_cached_screenshot_response(request, route_key)
if cached_response is not None:
return cached_response
# Also check time-based cache for recently rendered screenshots
if cached is not None and (now - cached[0]) < ttl:
cached_response = self._get_cached_screenshot_response(request, route_key)
if cached_response is not None:
@@ -620,9 +610,6 @@ class LocalServer:
etag = hashlib.sha1(svg.encode("utf-8"), usedforsecurity=False).hexdigest()
self._screenshot_cache[route_key] = (asyncio.get_event_loop().time(), svg)
self._screenshot_cache_etag[route_key] = etag
self._screenshot_last_rendered_activity[route_key] = self._route_last_activity.get(
route_key, 0.0
)
headers = {"Cache-Control": "no-cache", "ETag": etag}
return web.Response(text=svg, content_type="image/svg+xml", headers=headers)