diff --git a/server/src/wled_controller/core/processing/wled_target_processor.py b/server/src/wled_controller/core/processing/wled_target_processor.py index 5bc124c..e401c87 100644 --- a/server/src/wled_controller/core/processing/wled_target_processor.py +++ b/server/src/wled_controller/core/processing/wled_target_processor.py @@ -427,6 +427,8 @@ class WledTargetProcessor(TargetProcessor): f"(display={self._resolved_display_index}, fps={self._target_fps})" ) + next_frame_time = time.perf_counter() + try: with high_resolution_timer(): while self._is_running: @@ -516,11 +518,12 @@ class WledTargetProcessor(TargetProcessor): 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 prev_frame_time_stamp = now - fps_samples.append(1.0 / interval if interval > 0 else 0) - self._metrics.fps_actual = sum(fps_samples) / len(fps_samples) + if self._metrics.frames_processed > 1: + fps_samples.append(1.0 / interval if interval > 0 else 0) + self._metrics.fps_actual = sum(fps_samples) / len(fps_samples) processing_time = now - loop_start self._metrics.fps_potential = 1.0 / processing_time if processing_time > 0 else 0 @@ -534,19 +537,24 @@ class WledTargetProcessor(TargetProcessor): self._metrics.last_error = str(e) logger.error(f"Processing error for target {self._target_id}: {e}", exc_info=True) - # Throttle to target FPS - elapsed = now - loop_start - remaining = frame_time - elapsed - if remaining > 0: + # Drift-compensating throttle: sleep until the absolute + # next_frame_time so overshoots in one frame are recovered + # in the next, keeping average FPS on target. + next_frame_time += frame_time + sleep_time = next_frame_time - time.perf_counter() + if sleep_time > 0: t_sleep_start = time.perf_counter() - await asyncio.sleep(remaining) + await asyncio.sleep(sleep_time) t_sleep_end = time.perf_counter() actual_sleep = (t_sleep_end - t_sleep_start) * 1000 - requested_sleep = remaining * 1000 + requested_sleep = sleep_time * 1000 jitter = actual_sleep - requested_sleep _diag_sleep_jitters.append((requested_sleep, actual_sleep)) if jitter > 10.0: # >10ms overshoot _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 iter_end = time.perf_counter()