diff --git a/backend/monitoring/telemetry.py b/backend/monitoring/telemetry.py index e9680d2..a476f99 100644 --- a/backend/monitoring/telemetry.py +++ b/backend/monitoring/telemetry.py @@ -677,9 +677,9 @@ class NetworkTelemetryService: d_to_b_video = _coerce_float(d_to_b_video_raw) if d_to_b_video_raw is not None else None ack_estimate = self._control_ack_tracker.get_latest_estimate() capture_to_send_raw = video_receiver_status.get("latest_capture_to_send_ms") - a_recv_to_paint_raw = display_probe_status.get("a_recv_to_paint_ms") + request_to_paint_raw = display_probe_status.get("request_to_paint_ms") capture_to_send_ms = _coerce_float(capture_to_send_raw) if capture_to_send_raw is not None else None - a_recv_to_paint_ms = _coerce_float(a_recv_to_paint_raw) if a_recv_to_paint_raw is not None else None + request_to_paint_ms = _coerce_float(request_to_paint_raw) if request_to_paint_raw is not None else None video_network_oneway_est_ms = ( round((a_to_d_video + d_to_b_video) / 2.0, 3) if a_to_d_video is not None and d_to_b_video is not None @@ -689,8 +689,8 @@ class NetworkTelemetryService: if capture_to_send_ms is not None and video_network_oneway_est_ms is not None: video_partial_est_ms = round(capture_to_send_ms + video_network_oneway_est_ms, 3) video_e2e_est_ms = None - if video_partial_est_ms is not None and a_recv_to_paint_ms is not None: - video_e2e_est_ms = round(video_partial_est_ms + a_recv_to_paint_ms, 3) + if video_partial_est_ms is not None and request_to_paint_ms is not None: + video_e2e_est_ms = round(video_partial_est_ms + request_to_paint_ms, 3) return { "control_loop_rtt_ms": ack_estimate.get("control_loop_rtt_ms"), @@ -710,7 +710,7 @@ class NetworkTelemetryService: "video_e2e_est_ms": video_e2e_est_ms, "estimate_method": { "control": "ack_loop" if ack_estimate.get("ack_available") else "srtt_fallback", - "video": "capture_to_send+srtt/2+recv_to_paint" if video_e2e_est_ms is not None else "capture_to_send+srtt/2", + "video": "capture_to_send+srtt/2+request_to_paint" if video_e2e_est_ms is not None else "capture_to_send+srtt/2", }, "clock_sync_required": False, "assumptions": [ diff --git a/backend/monitoring/video.py b/backend/monitoring/video.py index 7b137bd..edf8071 100644 --- a/backend/monitoring/video.py +++ b/backend/monitoring/video.py @@ -48,18 +48,27 @@ class VideoDisplayProbeStore: input_to_next_fresh_frame_ms=None, input_to_next_changed_frame_ms=None, input_to_next_paint_ms=None, - a_recv_to_paint_ms=None, + request_to_paint_ms=None, + response_to_paint_ms=None, + backend_to_request_ms=None, + backend_to_paint_ms_raw=None, ) def record_event(self, payload: dict[str, Any]) -> None: backend_received_unix_ns = payload.get("backend_received_unix_ns") + request_started_unix_ms = payload.get("request_started_unix_ms") + response_received_unix_ms = payload.get("response_received_unix_ms") paint_unix_ms = payload.get("paint_unix_ms") - a_recv_to_paint_ms = None + request_to_paint_ms = self._duration_ms(paint_unix_ms, request_started_unix_ms, clamp_floor_zero=True) + response_to_paint_ms = self._duration_ms(paint_unix_ms, response_received_unix_ms, clamp_floor_zero=True) + backend_received_unix_ms = None try: - if backend_received_unix_ns is not None and paint_unix_ms is not None: - a_recv_to_paint_ms = round(float(paint_unix_ms) - (int(backend_received_unix_ns) / 1_000_000.0), 3) + if backend_received_unix_ns is not None: + backend_received_unix_ms = int(backend_received_unix_ns) / 1_000_000.0 except (TypeError, ValueError): - a_recv_to_paint_ms = None + backend_received_unix_ms = None + backend_to_request_ms = self._duration_ms(request_started_unix_ms, backend_received_unix_ms, clamp_floor_zero=False) + backend_to_paint_ms_raw = self._duration_ms(paint_unix_ms, backend_received_unix_ms, clamp_floor_zero=False) status = VideoDisplayProbeStatus( updated_at=str(payload.get("updated_at") or ""), @@ -68,7 +77,10 @@ class VideoDisplayProbeStore: input_to_next_fresh_frame_ms=self._coerce_float(payload.get("input_to_next_fresh_frame_ms")), input_to_next_changed_frame_ms=self._coerce_float(payload.get("input_to_next_changed_frame_ms")), input_to_next_paint_ms=self._coerce_float(payload.get("input_to_next_paint_ms")), - a_recv_to_paint_ms=a_recv_to_paint_ms, + request_to_paint_ms=request_to_paint_ms, + response_to_paint_ms=response_to_paint_ms, + backend_to_request_ms=backend_to_request_ms, + backend_to_paint_ms_raw=backend_to_paint_ms_raw, ) with self._lock: self._latest = status @@ -84,7 +96,10 @@ class VideoDisplayProbeStore: "input_to_next_fresh_frame_ms": latest.input_to_next_fresh_frame_ms, "input_to_next_changed_frame_ms": latest.input_to_next_changed_frame_ms, "input_to_next_paint_ms": latest.input_to_next_paint_ms, - "a_recv_to_paint_ms": latest.a_recv_to_paint_ms, + "request_to_paint_ms": latest.request_to_paint_ms, + "response_to_paint_ms": latest.response_to_paint_ms, + "backend_to_request_ms": latest.backend_to_request_ms, + "backend_to_paint_ms_raw": latest.backend_to_paint_ms_raw, } def close(self) -> None: @@ -99,6 +114,17 @@ class VideoDisplayProbeStore: except (TypeError, ValueError): return None + @classmethod + def _duration_ms(cls, end_ms: Any, start_ms: Any, *, clamp_floor_zero: bool) -> float | None: + end_value = cls._coerce_float(end_ms) + start_value = cls._coerce_float(start_ms) + if end_value is None or start_value is None: + return None + delta = round(end_value - start_value, 3) + if clamp_floor_zero: + delta = max(0.0, delta) + return delta + @dataclass(frozen=True) class FrameTrailerMetadata: @@ -123,7 +149,10 @@ class VideoDisplayProbeStatus: input_to_next_fresh_frame_ms: float | None input_to_next_changed_frame_ms: float | None input_to_next_paint_ms: float | None - a_recv_to_paint_ms: float | None + request_to_paint_ms: float | None + response_to_paint_ms: float | None + backend_to_request_ms: float | None + backend_to_paint_ms_raw: float | None class OmniSocketVideoReceiver: diff --git a/frontend/src/components/VideoPanel.vue b/frontend/src/components/VideoPanel.vue index 8851d78..8016a29 100644 --- a/frontend/src/components/VideoPanel.vue +++ b/frontend/src/components/VideoPanel.vue @@ -76,6 +76,10 @@ function formatNumber(value: number | null | undefined, suffix = '') { return `${value.toFixed(1)}${suffix}` } +function wallClockNowMs() { + return Date.now() +} + let frameTimer: number | null = null let statusTimer: number | null = null let probeTimer: number | null = null @@ -151,7 +155,7 @@ async function runDisplayProbe() { } probeRequestPending = true - const requestStartedUnixMs = performance.timeOrigin + performance.now() + const requestStartedUnixMs = wallClockNowMs() try { probeKey += 1 @@ -167,7 +171,7 @@ async function runDisplayProbe() { const frameHashHeader = response.headers.get('X-Blitz-Frame-Hash') ?? '' const frameSeq = frameSeqHeader ? Number(frameSeqHeader) : null const backendReceivedUnixNs = backendReceivedHeader ? Number(backendReceivedHeader) : null - const responseReceivedUnixMs = performance.timeOrigin + performance.now() + const responseReceivedUnixMs = wallClockNowMs() const blob = await response.blob() const objectUrl = URL.createObjectURL(blob) @@ -175,11 +179,11 @@ async function runDisplayProbe() { const probeImage = new Image() probeImage.src = objectUrl await probeImage.decode() - const decodedUnixMs = performance.timeOrigin + performance.now() + const decodedUnixMs = wallClockNowMs() await new Promise((resolve) => { requestAnimationFrame(() => resolve()) }) - const paintUnixMs = performance.timeOrigin + performance.now() + const paintUnixMs = wallClockNowMs() let inputToNextFreshFrameMs: number | null = null let inputToNextChangedFrameMs: number | null = null @@ -339,7 +343,7 @@ watch(
Paint Delay - {{ formatNumber(displayVideo?.display_probe?.a_recv_to_paint_ms, ' ms') }} + {{ formatNumber(displayVideo?.display_probe?.request_to_paint_ms, ' ms') }}
@@ -367,7 +371,7 @@ watch(

Input to next seq: {{ formatNumber(operatorMetrics.input_to_next_fresh_frame_ms, ' ms') }}

Input to changed frame: {{ formatNumber(operatorMetrics.input_to_next_changed_frame_ms, ' ms') }}

Input to paint: {{ formatNumber(operatorMetrics.input_to_next_paint_ms, ' ms') }}

-

Display probe recv-to-paint: {{ formatNumber(displayVideo?.display_probe?.a_recv_to_paint_ms, ' ms') }}

+

Display probe request-to-paint: {{ formatNumber(displayVideo?.display_probe?.request_to_paint_ms, ' ms') }}

diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 35758f8..48ceb80 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -271,7 +271,10 @@ export interface VideoStatus { input_to_next_fresh_frame_ms: number | null input_to_next_changed_frame_ms: number | null input_to_next_paint_ms: number | null - a_recv_to_paint_ms: number | null + request_to_paint_ms: number | null + response_to_paint_ms: number | null + backend_to_request_ms: number | null + backend_to_paint_ms_raw: number | null } receiver?: { backend_ready: boolean