Add FPS throttling to capture, processing, and send loops

All three frame pipeline loops were running unthrottled, consuming
excessive CPU. Now each sleeps for the remaining frame budget after
completing work.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 01:39:18 +03:00
parent cc91ccd75a
commit c4955bcb34
2 changed files with 23 additions and 15 deletions

View File

@@ -141,8 +141,11 @@ class ScreenCaptureLiveStream(LiveStream):
except Exception as e:
logger.error(f"Capture error (display={self._capture_stream.display_index}): {e}")
# No FPS throttling - capture as fast as frames are available
# But sleep briefly during idle periods to avoid burning CPU
# Throttle to target FPS
elapsed = time.time() - loop_start
remaining = frame_time - elapsed
if remaining > 0:
time.sleep(remaining)
class ProcessedLiveStream(LiveStream):
@@ -209,18 +212,17 @@ class ProcessedLiveStream(LiveStream):
# processed by a consumer), so the 3rd slot is always safe to reuse.
_ring: List[Optional[np.ndarray]] = [None, None, None]
_ring_idx = 0
frame_time = 1.0 / self._source.target_fps if self._source.target_fps > 0 else 1.0
while self._running:
source_frame = self._source.get_latest_frame()
if source_frame is None:
# Small sleep when waiting for frames to avoid CPU spinning
time.sleep(0.001)
continue
loop_start = time.time()
# Identity cache: Skip processing duplicate frames to save CPU
# (Compare object identity to detect when capture engine returns same frame)
if source_frame is cached_source_frame:
time.sleep(0.001)
source_frame = self._source.get_latest_frame()
if source_frame is None or source_frame is cached_source_frame:
# Sleep until next frame is expected
elapsed = time.time() - loop_start
remaining = frame_time - elapsed
time.sleep(max(remaining, 0.001))
continue
cached_source_frame = source_frame

View File

@@ -859,8 +859,11 @@ class ProcessorManager:
state.metrics.last_error = str(e)
logger.error(f"Processing error for target {target_id}: {e}", exc_info=True)
# No FPS control - process frames as fast as they arrive (match test behavior)
pass
# Throttle to target FPS
elapsed = time.time() - loop_start
remaining = frame_time - elapsed
if remaining > 0:
await asyncio.sleep(remaining)
except asyncio.CancelledError:
logger.info(f"Processing loop cancelled for target {target_id}")
@@ -1503,8 +1506,11 @@ class ProcessorManager:
state.metrics.last_error = str(e)
logger.error(f"KC processing error for {target_id}: {e}", exc_info=True)
# No FPS control - process frames as fast as they arrive (match test behavior)
pass
# Throttle to target FPS
elapsed = time.time() - loop_start
remaining = frame_time - elapsed
if remaining > 0:
await asyncio.sleep(remaining)
except asyncio.CancelledError:
logger.info(f"KC processing loop cancelled for target {target_id}")