From c9657b1d46746b43310c3045a06f58a773d73099 Mon Sep 17 00:00:00 2001 From: Anso Date: Fri, 24 Apr 2026 23:36:45 -0400 Subject: [PATCH] fix(frontend): stream log lines on next paint and show ms-precision timestamps (#764) Two compounding issues made logs feel laggy and timestamps look duplicated: 1. State updates were batched in a 250ms setInterval, so a burst of lines all rendered together every quarter second. Replace with a requestAnimationFrame scheduler: lines flush on the next paint (~16ms at 60Hz) while still collapsing a single burst into one React commit. Cleanup uses cancelAnimationFrame. 2. The timestamp formatter rendered HH:mm:ss only, dropping the sub-second precision that docker logs -t already emits. Two lines logged within the same second appeared identically. Render HH:mm:ss.SSS so successive lines remain visually distinct. --- .../src/components/StructuredLogViewer.tsx | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/StructuredLogViewer.tsx b/frontend/src/components/StructuredLogViewer.tsx index 1dcc6f62..0e24bdfa 100644 --- a/frontend/src/components/StructuredLogViewer.tsx +++ b/frontend/src/components/StructuredLogViewer.tsx @@ -43,7 +43,8 @@ function formatTs(iso: string | null): string { const hh = String(d.getHours()).padStart(2, '0'); const mm = String(d.getMinutes()).padStart(2, '0'); const ss = String(d.getSeconds()).padStart(2, '0'); - return `${hh}:${mm}:${ss}`; + const ms = String(d.getMilliseconds()).padStart(3, '0'); + return `${hh}:${mm}:${ss}.${ms}`; } export default function StructuredLogViewer({ stackName }: StructuredLogViewerProps) { @@ -68,6 +69,22 @@ export default function StructuredLogViewer({ stackName }: StructuredLogViewerPr const ws = new WebSocket(wsUrl); wsRef.current = ws; + let rafId = 0; + const flushPending = () => { + rafId = 0; + if (pendingRef.current.length === 0) return; + const incoming = pendingRef.current; + pendingRef.current = []; + setRows((prev) => { + const merged = prev.concat(incoming); + return merged.length > BUFFER_CAP ? merged.slice(merged.length - BUFFER_CAP) : merged; + }); + }; + const scheduleFlush = () => { + if (rafId !== 0) return; + rafId = requestAnimationFrame(flushPending); + }; + ws.onmessage = (event) => { if (closed) return; const text = typeof event.data === 'string' ? event.data : ''; @@ -79,24 +96,14 @@ export default function StructuredLogViewer({ stackName }: StructuredLogViewerPr rowIdRef.current += 1; pendingRef.current.push({ id: rowIdRef.current, ...parsed }); } + scheduleFlush(); }; ws.onerror = () => { /* surface nothing; reconnection is backend's job */ }; - // Batch incoming lines into state every 250 ms to avoid thrashing React. - const flushInterval = window.setInterval(() => { - if (pendingRef.current.length === 0) return; - const incoming = pendingRef.current; - pendingRef.current = []; - setRows((prev) => { - const merged = prev.concat(incoming); - return merged.length > BUFFER_CAP ? merged.slice(merged.length - BUFFER_CAP) : merged; - }); - }, 250); - return () => { closed = true; - window.clearInterval(flushInterval); + if (rafId !== 0) cancelAnimationFrame(rafId); try { ws.close(); } catch { /* ignore */ } wsRef.current = null; pendingRef.current = [];