Pace the Vegas frame loop adaptively: 31.5 -> 78.7 fps

The loop slept a fixed frame_interval on top of however long the frame took, so
at a measured 31.6ms per frame a flat 8ms of that was pure idle — a quarter of
the budget spent not rendering. It now sleeps only the remainder of the budget.

Measured on hardware: 31.5 fps to 78.7 fps sustained, with CPU going *down* from
150% to 127%. Scroll speed is unchanged at 49.9px/s against a configured 50,
because motion is derived from elapsed time rather than frame count — this buys
smoothness, not speed.

Worth recording what the bottleneck was not: the per-frame render path measures
0.34ms in total (0.18ms for the numpy slice, 0.17ms for the dirty-tracking
digest), which is a theoretical 2900 fps. Optimising any of that would have been
wasted effort. The frame was idle, not busy.

Also nices the prefetch thread. Its work is PIL and numpy that releases the GIL,
so the scheduler can act on the priority, and without it the prefetch competes
for the same cores as the render loop.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
This commit is contained in:
ChuckBuilds
2026-07-29 14:39:16 -04:00
co-authored by Claude
parent 94032e2101
commit 932e89ccb8
2 changed files with 20 additions and 2 deletions
+10 -2
View File
@@ -376,6 +376,8 @@ class VegasModeCoordinator:
logger.info("Starting Vegas iteration for %.1fs", duration) logger.info("Starting Vegas iteration for %.1fs", duration)
while True: while True:
frame_started = time.time()
# Check for STATIC mode plugin that should pause scroll # Check for STATIC mode plugin that should pause scroll
static_plugin = self._check_static_plugin_trigger() static_plugin = self._check_static_plugin_trigger()
if static_plugin: if static_plugin:
@@ -396,8 +398,14 @@ class VegasModeCoordinator:
# Paused for live priority - let caller handle # Paused for live priority - let caller handle
return False return False
# Sleep for frame interval # Sleep only the remainder of the frame budget. This used to sleep
time.sleep(frame_interval) # the whole interval on top of however long the frame took, so at a
# measured 31.6ms per frame a fixed 8ms of that was pure idle — a
# quarter of the budget spent not rendering. Subtracting the work
# already done keeps the pacing target while reclaiming that time,
# and yields the GIL either way so other threads still run.
frame_elapsed = time.time() - frame_started
time.sleep(max(0.0, frame_interval - frame_elapsed))
# Increment frame count and check for interrupt periodically # Increment frame count and check for interrupt periodically
frame_count += 1 frame_count += 1
+10
View File
@@ -6,6 +6,7 @@ Uses the existing ScrollHelper for numpy-optimized scroll operations.
""" """
import logging import logging
import os
import time import time
import threading import threading
from collections import deque from collections import deque
@@ -256,6 +257,15 @@ class RenderPipeline:
return # already have one waiting return # already have one waiting
def _work(): def _work():
# Deprioritise against the render loop. Linux applies nice
# per-thread, and the heavy lifting here is PIL and numpy work
# that releases the GIL, so the scheduler can actually act on
# it — without this the prefetch competes for the same cores and
# costs frames.
try:
os.nice(10)
except (OSError, AttributeError):
pass
try: try:
group = self.stream_manager.take_next_group(offscreen_only=True) group = self.stream_manager.take_next_group(offscreen_only=True)
except Exception: except Exception: