diff --git a/src/shared/components/ActiveRequestsPanel.tsx b/src/shared/components/ActiveRequestsPanel.tsx index 2466f26ac3..9e84069710 100644 --- a/src/shared/components/ActiveRequestsPanel.tsx +++ b/src/shared/components/ActiveRequestsPanel.tsx @@ -62,6 +62,8 @@ export default function ActiveRequestsPanel() { let cancelled = false; const load = async () => { + // Skip polling when tab is not visible to save resources + if (document.visibilityState !== "visible") return; try { const res = await fetch("/api/logs/active", { cache: "no-store" }); if (!res.ok) return; @@ -81,7 +83,7 @@ export default function ActiveRequestsPanel() { }; load(); - const interval = setInterval(load, 3000); + const interval = setInterval(load, 15_000); return () => { cancelled = true; clearInterval(interval); diff --git a/src/shared/components/RequestLoggerV2.tsx b/src/shared/components/RequestLoggerV2.tsx index f2cc14a1b4..1ca9d039e9 100644 --- a/src/shared/components/RequestLoggerV2.tsx +++ b/src/shared/components/RequestLoggerV2.tsx @@ -20,11 +20,20 @@ import { } from "@/shared/utils/formatting"; import { getProviderDisplayLabel } from "@/shared/utils/providerDisplayLabel"; import useEmailPrivacyStore from "@/store/emailPrivacyStore"; +import { + computeLogsSignature, + resolveInitialVisibility, + shouldAutoRefresh, +} from "./requestLoggerSignature"; // Number of call-log rows fetched per page. The viewer grows its window by this // amount on "Load more" / infinite scroll so users can browse past the first // page (previously hardcoded to a single 300-row window). See #2565. -const PAGE_SIZE = 300; +// Reduced from 300 → 50 to avoid browser freeze and network saturation. +const PAGE_SIZE = 50; + +// Polling interval in ms. 15s balances freshness vs resource usage. +const POLL_INTERVAL_MS = 15_000; function getLogTotalTokens(log) { return (log?.tokens?.in || 0) + (log?.tokens?.out || 0); @@ -116,6 +125,7 @@ export default function RequestLoggerV2() { const scrollContainerRef = useRef(null); const loadMoreSentinelRef = useRef(null); const [providerNodes, setProviderNodes] = useState([]); + const visibleRef = useRef(resolveInitialVisibility()); // Column visibility with localStorage persistence const [visibleColumns, setVisibleColumns] = useState(() => { @@ -159,8 +169,10 @@ export default function RequestLoggerV2() { const data = await res.json(); // If the server returned a full window, more rows may exist beyond it. setHasMore(Array.isArray(data) && data.length >= limit); - // Skip re-render if data hasn't changed (#1369 GPU perf) - const sig = JSON.stringify(data.map?.((l: any) => l.id) ?? []); + // Skip re-render if data hasn't changed (#1369 GPU perf). The signature + // captures id + status + duration + tokens_out so in-progress updates + // still re-render while identical snapshots are skipped. + const sig = computeLogsSignature(data); if (sig !== logsSignatureRef.current) { logsSignatureRef.current = sig; setLogs(data); @@ -202,16 +214,31 @@ export default function RequestLoggerV2() { .catch(() => {}); }, []); - // Auto-refresh + // Visibility-aware auto-refresh: pause polling when tab is hidden or user has + // scrolled past the first page to avoid escalating payload sizes. + useEffect(() => { + const onVisibility = () => { + const isVisible = document.visibilityState === "visible"; + visibleRef.current = isVisible; + if (isVisible && shouldAutoRefresh(recording, limit, PAGE_SIZE)) { + fetchLogs(false); + } + }; + document.addEventListener("visibilitychange", onVisibility); + return () => document.removeEventListener("visibilitychange", onVisibility); + }, [recording, limit, fetchLogs]); + useEffect(() => { if (intervalRef.current) clearInterval(intervalRef.current); - if (recording) { - intervalRef.current = setInterval(() => fetchLogs(false), 3000); + if (shouldAutoRefresh(recording, limit, PAGE_SIZE)) { + intervalRef.current = setInterval(() => { + if (visibleRef.current) fetchLogs(false); + }, POLL_INTERVAL_MS); } return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; - }, [recording, fetchLogs]); + }, [recording, fetchLogs, limit]); // Reset the window back to the first page whenever the active filters change, // so switching filters doesn't keep fetching a large expanded window. @@ -323,23 +350,37 @@ export default function RequestLoggerV2() { // Unique accounts and providers for dropdowns - const uniqueAccounts = [...new Set(logs.map((l) => l.account).filter((a) => a && a !== "-"))]; - const uniqueModels = [ - ...new Set(logs.flatMap((l) => [l.model, l.requestedModel]).filter((value) => Boolean(value))), - ].sort(); - const uniqueProviders = [ - ...new Set(logs.map((l) => l.provider).filter((p) => p && p !== "-")), - ].sort(); - const uniqueApiKeys = [ - ...new Set(logs.map((l) => l.apiKeyId || l.apiKeyName).filter(Boolean)), - ].sort(); + const uniqueAccounts = useMemo( + () => [...new Set(logs.map((l) => l.account).filter((a) => a && a !== "-"))], + [logs] + ); + const uniqueModels = useMemo( + () => [ + ...new Set(logs.flatMap((l) => [l.model, l.requestedModel]).filter((value) => Boolean(value))), + ].sort(), + [logs] + ); + const uniqueProviders = useMemo( + () => [ + ...new Set(logs.map((l) => l.provider).filter((p) => p && p !== "-")), + ].sort(), + [logs] + ); + const uniqueApiKeys = useMemo( + () => [ + ...new Set(logs.map((l) => l.apiKeyId || l.apiKeyName).filter(Boolean)), + ].sort(), + [logs] + ); - // Stats - const totalCount = filteredLogs.length; - const okCount = filteredLogs.filter((l) => l.status >= 200 && l.status < 300).length; - const errorCount = filteredLogs.filter((l) => l.status >= 400).length; - const comboCount = logs.filter((l) => l.comboName).length; - const apiKeyCount = uniqueApiKeys.length; + // Stats (memoized to avoid re-computation on every render) + const { totalCount, okCount, errorCount, comboCount, apiKeyCount } = useMemo(() => ({ + totalCount: filteredLogs.length, + okCount: filteredLogs.filter((l) => l.status >= 200 && l.status < 300).length, + errorCount: filteredLogs.filter((l) => l.status >= 400).length, + comboCount: logs.filter((l) => l.comboName).length, + apiKeyCount: uniqueApiKeys.length, + }), [filteredLogs, logs, uniqueApiKeys]); return (