Drift-compensating frame throttle, fix FPS startup spike

Replace per-frame sleep(remaining) with absolute next_frame_time
tracking so asyncio.sleep() overshoots are recovered in subsequent
frames, keeping average FPS on target. Skip first FPS sample to
avoid ~2000+ spike from near-zero init interval.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 01:09:43 +03:00
parent 2a01c2947a
commit 27575930b8

View File

@@ -427,6 +427,8 @@ class WledTargetProcessor(TargetProcessor):
f"(display={self._resolved_display_index}, fps={self._target_fps})" f"(display={self._resolved_display_index}, fps={self._target_fps})"
) )
next_frame_time = time.perf_counter()
try: try:
with high_resolution_timer(): with high_resolution_timer():
while self._is_running: while self._is_running:
@@ -516,9 +518,10 @@ class WledTargetProcessor(TargetProcessor):
f"({len(send_colors)} LEDs) — send={send_ms:.1f}ms" f"({len(send_colors)} LEDs) — send={send_ms:.1f}ms"
) )
# FPS tracking # FPS tracking (skip first sample — interval from loop init is near-zero)
interval = now - prev_frame_time_stamp interval = now - prev_frame_time_stamp
prev_frame_time_stamp = now prev_frame_time_stamp = now
if self._metrics.frames_processed > 1:
fps_samples.append(1.0 / interval if interval > 0 else 0) fps_samples.append(1.0 / interval if interval > 0 else 0)
self._metrics.fps_actual = sum(fps_samples) / len(fps_samples) self._metrics.fps_actual = sum(fps_samples) / len(fps_samples)
@@ -534,19 +537,24 @@ class WledTargetProcessor(TargetProcessor):
self._metrics.last_error = str(e) self._metrics.last_error = str(e)
logger.error(f"Processing error for target {self._target_id}: {e}", exc_info=True) logger.error(f"Processing error for target {self._target_id}: {e}", exc_info=True)
# Throttle to target FPS # Drift-compensating throttle: sleep until the absolute
elapsed = now - loop_start # next_frame_time so overshoots in one frame are recovered
remaining = frame_time - elapsed # in the next, keeping average FPS on target.
if remaining > 0: next_frame_time += frame_time
sleep_time = next_frame_time - time.perf_counter()
if sleep_time > 0:
t_sleep_start = time.perf_counter() t_sleep_start = time.perf_counter()
await asyncio.sleep(remaining) await asyncio.sleep(sleep_time)
t_sleep_end = time.perf_counter() t_sleep_end = time.perf_counter()
actual_sleep = (t_sleep_end - t_sleep_start) * 1000 actual_sleep = (t_sleep_end - t_sleep_start) * 1000
requested_sleep = remaining * 1000 requested_sleep = sleep_time * 1000
jitter = actual_sleep - requested_sleep jitter = actual_sleep - requested_sleep
_diag_sleep_jitters.append((requested_sleep, actual_sleep)) _diag_sleep_jitters.append((requested_sleep, actual_sleep))
if jitter > 10.0: # >10ms overshoot if jitter > 10.0: # >10ms overshoot
_diag_slow_iters.append(((t_sleep_end - loop_start) * 1000, "sleep_jitter")) _diag_slow_iters.append(((t_sleep_end - loop_start) * 1000, "sleep_jitter"))
elif sleep_time < -frame_time:
# Too far behind — reset to avoid burst catch-up
next_frame_time = time.perf_counter()
# Track total iteration time # Track total iteration time
iter_end = time.perf_counter() iter_end = time.perf_counter()