diff --git a/interface/src/app/status/SystemLog.tsx b/interface/src/app/status/SystemLog.tsx
index fae5da6d5..43c6580e2 100644
--- a/interface/src/app/status/SystemLog.tsx
+++ b/interface/src/app/status/SystemLog.tsx
@@ -1,4 +1,11 @@
-import { memo, useCallback, useEffect, useRef, useState } from 'react';
+import {
+ memo,
+ useCallback,
+ useEffect,
+ useLayoutEffect,
+ useRef,
+ useState
+} from 'react';
import { toast } from 'react-toastify';
import DownloadIcon from '@mui/icons-material/GetApp';
@@ -68,34 +75,37 @@ const levelLabel = (level: LogLevel) => {
}
};
+const paddedLevelLabel = (level: LogLevel, compact: boolean) => {
+ const label = levelLabel(level);
+ return compact ? ' ' + label[0] : label.padStart(8, '\xa0');
+};
+
+const paddedNameLabel = (name: string, compact: boolean) => {
+ const label = '[' + name + ']';
+ return compact ? label : label.padEnd(12, '\xa0');
+};
+
+const paddedIDLabel = (id: number, compact: boolean) => {
+ const label = id + ':';
+ return compact ? label : label.padEnd(7, '\xa0');
+};
+
// Memoized log entry component to prevent unnecessary re-renders
const LogEntryItem = memo(
({ entry, compact }: { entry: LogEntry; compact: boolean }) => {
- const paddedLevelLabel = (level: LogLevel) => {
- const label = levelLabel(level);
- return compact ? ' ' + label[0] : label.padStart(8, '\xa0');
- };
-
- const paddedNameLabel = (name: string) => {
- const label = '[' + name + ']';
- return compact ? label : label.padEnd(12, '\xa0');
- };
-
- const paddedIDLabel = (id: number) => {
- const label = id + ':';
- return compact ? label : label.padEnd(7, '\xa0');
- };
-
return (
{entry.t}
- {paddedLevelLabel(entry.l)}
- {paddedIDLabel(entry.i)}
- {paddedNameLabel(entry.n)}
+ {paddedLevelLabel(entry.l, compact)}
+ {paddedIDLabel(entry.i, compact)}
+ {paddedNameLabel(entry.n, compact)}
{entry.m}
);
- }
+ },
+ (prevProps, nextProps) =>
+ prevProps.entry.i === nextProps.entry.i &&
+ prevProps.compact === nextProps.compact
);
const SystemLog = () => {
@@ -129,7 +139,7 @@ const SystemLog = () => {
const [readOpen, setReadOpen] = useState(false);
const [logEntries, setLogEntries] = useState([]);
const [autoscroll, setAutoscroll] = useState(true);
- const [lastId, setLastId] = useState(-1);
+ const [boxPosition, setBoxPosition] = useState({ top: 0, left: 0 });
const ALPHA_NUMERIC_DASH_REGEX = /^[a-fA-F0-9 ]+$/;
@@ -140,24 +150,69 @@ const SystemLog = () => {
updateDataValue as (value: unknown) => void
);
+ // Calculate box position after layout
+ useLayoutEffect(() => {
+ const logWindow = document.getElementById('log-window');
+ if (!logWindow) {
+ return;
+ }
+
+ const updatePosition = () => {
+ const windowElement = document.getElementById('log-window');
+ if (!windowElement) {
+ return;
+ }
+ const rect = windowElement.getBoundingClientRect();
+ setBoxPosition({ top: rect.bottom, left: rect.left });
+ };
+
+ updatePosition();
+
+ // Debounce resize events with requestAnimationFrame
+ let rafId: number;
+ const handleResize = () => {
+ cancelAnimationFrame(rafId);
+ rafId = requestAnimationFrame(updatePosition);
+ };
+
+ // Update position on window resize
+ window.addEventListener('resize', handleResize);
+ const resizeObserver = new ResizeObserver(handleResize);
+ resizeObserver.observe(logWindow);
+
+ return () => {
+ window.removeEventListener('resize', handleResize);
+ resizeObserver.disconnect();
+ cancelAnimationFrame(rafId);
+ };
+ }, [data]); // Recalculate when data changes (in case layout shifts)
+
+ // Memoize message handler to avoid recreating on every render
+ const handleLogMessage = useCallback((message: { data: string }) => {
+ const rawData = message.data;
+ const logentry = JSON.parse(rawData) as LogEntry;
+ setLogEntries((log) => {
+ // Skip if this is a duplicate entry (check last entry id)
+ if (log.length > 0) {
+ const lastEntry = log[log.length - 1];
+ if (lastEntry && logentry.i <= lastEntry.i) {
+ return log;
+ }
+ }
+ const newLog = [...log, logentry];
+ // Limit log entries to prevent memory issues - only slice when necessary
+ if (newLog.length > MAX_LOG_ENTRIES) {
+ return newLog.slice(-MAX_LOG_ENTRIES);
+ }
+ return newLog;
+ });
+ }, []);
+
useSSE(fetchLogES, {
immediate: true,
interceptByGlobalResponded: false
})
- .onMessage((message: { data: string }) => {
- const rawData = message.data;
- const logentry = JSON.parse(rawData) as LogEntry;
- if (lastId < logentry.i) {
- setLogEntries((log) => {
- const newLog = [...log, logentry];
- // Limit log entries to prevent memory issues
- return newLog.length > MAX_LOG_ENTRIES
- ? newLog.slice(-MAX_LOG_ENTRIES)
- : newLog;
- });
- setLastId(logentry.i);
- }
- })
+ .onMessage(handleLogMessage)
.onError(() => {
toast.error('No connection to Log service');
});
@@ -182,14 +237,18 @@ const SystemLog = () => {
await saveData();
}, [saveData]);
- // handle scrolling
+ // handle scrolling - optimized to only scroll when needed
const ref = useRef(null);
+ const logWindowRef = useRef(null);
+
useEffect(() => {
if (logEntries.length && autoscroll) {
- ref.current?.scrollIntoView({
- behavior: 'smooth',
- block: 'end'
- });
+ const container = logWindowRef.current;
+ if (container) {
+ requestAnimationFrame(() => {
+ container.scrollTop = container.scrollHeight;
+ });
+ }
}
}, [logEntries.length, autoscroll]);
@@ -206,15 +265,6 @@ const SystemLog = () => {
}
}, [readValue, readOpen, send]);
- const boxPosition = () => {
- const logWindow = document.getElementById('log-window');
- if (!logWindow) {
- return { top: 0, left: 0 };
- }
- const rect = logWindow.getBoundingClientRect();
- return { top: rect.bottom, left: rect.left };
- };
-
const content = () => {
if (!data) {
return ;
@@ -352,14 +402,15 @@ const SystemLog = () => {