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.
This commit is contained in:
Anso
2026-04-24 23:36:45 -04:00
committed by GitHub
parent 57461043b0
commit c9657b1d46
+20 -13
View File
@@ -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 = [];