forked from wrenn/wrenn
Optimize frontend polling: visibility API, range-based intervals, skip redundant redraws
Adds Page Visibility API to StatsPanel, templates, and capsule detail pages so polling pauses when the browser tab is hidden. Capsule metrics now use range-appropriate poll intervals (10s for 5m/10m, up to 120s for 24h) instead of a flat 10s. Chart updates are skipped when the data fingerprint hasn't changed, avoiding unnecessary Canvas redraws.
This commit is contained in:
@ -21,5 +21,14 @@ export async function fetchSandboxMetrics(id: string, range: MetricRange): Promi
|
||||
|
||||
export const METRIC_RANGES: MetricRange[] = ['5m', '10m', '1h', '6h', '24h'];
|
||||
|
||||
// All ranges poll every 10 seconds.
|
||||
// Poll interval varies by range — shorter ranges need fresher data.
|
||||
export const METRIC_POLL_INTERVALS: Record<MetricRange, number> = {
|
||||
'5m': 10_000,
|
||||
'10m': 10_000,
|
||||
'1h': 30_000,
|
||||
'6h': 60_000,
|
||||
'24h': 120_000,
|
||||
};
|
||||
|
||||
/** @deprecated Use METRIC_POLL_INTERVALS instead */
|
||||
export const METRIC_POLL_INTERVAL = 10_000;
|
||||
|
||||
@ -26,6 +26,8 @@
|
||||
let chartRam: any = null;
|
||||
|
||||
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let lastDataKey = ''; // cheap fingerprint to skip redundant chart redraws
|
||||
let visibilityHandler: (() => void) | null = null;
|
||||
|
||||
async function load() {
|
||||
const result = await fetchStats(range);
|
||||
@ -43,6 +45,10 @@
|
||||
|
||||
function updateCharts() {
|
||||
if (!stats) return;
|
||||
// Skip redraw if data hasn't changed (same length + same last label).
|
||||
const key = `${stats.series.labels.length}:${stats.series.labels.at(-1) ?? ''}`;
|
||||
if (key === lastDataKey) return;
|
||||
lastDataKey = key;
|
||||
// Use Array.from to pass plain JS arrays to Chart.js — Svelte 5 $state
|
||||
// wraps arrays in reactive proxies which Chart.js can't iterate reliably.
|
||||
const labels = formatLabels(Array.from(stats.series.labels), range);
|
||||
@ -77,14 +83,19 @@
|
||||
});
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
|
||||
}
|
||||
|
||||
function restartPolling() {
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
stopPolling();
|
||||
load();
|
||||
pollInterval = setInterval(load, POLL_INTERVALS[range]);
|
||||
}
|
||||
|
||||
function setRange(r: TimeRange) {
|
||||
range = r;
|
||||
lastDataKey = ''; // force chart redraw on range switch
|
||||
goto(`?range=${r}`, { replaceState: true, noScroll: true, keepFocus: true });
|
||||
restartPolling();
|
||||
}
|
||||
@ -237,10 +248,21 @@
|
||||
updateCharts();
|
||||
|
||||
restartPolling();
|
||||
|
||||
// Pause polling when the browser tab is hidden to save bandwidth/CPU.
|
||||
visibilityHandler = () => {
|
||||
if (document.hidden) {
|
||||
stopPolling();
|
||||
} else {
|
||||
restartPolling();
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', visibilityHandler);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
stopPolling();
|
||||
if (visibilityHandler) document.removeEventListener('visibilitychange', visibilityHandler);
|
||||
chartRunning?.destroy();
|
||||
chartCpu?.destroy();
|
||||
chartRam?.destroy();
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
// Polling
|
||||
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let hasActiveBuilds = $derived(builds.some((b) => b.status === 'pending' || b.status === 'running'));
|
||||
let visibilityHandler: (() => void) | null = null;
|
||||
|
||||
// Build log expansion
|
||||
let expandedBuildId = $state<string | null>(null);
|
||||
@ -97,7 +98,7 @@
|
||||
function startPolling() {
|
||||
stopPolling();
|
||||
pollInterval = setInterval(() => {
|
||||
if (hasActiveBuilds) fetchBuilds();
|
||||
if (hasActiveBuilds && activeTab === 'builds') fetchBuilds();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
@ -242,9 +243,22 @@
|
||||
onMount(() => {
|
||||
fetchTemplates();
|
||||
fetchBuilds().then(startPolling);
|
||||
|
||||
// Pause polling when the browser tab is hidden.
|
||||
visibilityHandler = () => {
|
||||
if (document.hidden) {
|
||||
stopPolling();
|
||||
} else {
|
||||
startPolling();
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', visibilityHandler);
|
||||
});
|
||||
|
||||
onDestroy(stopPolling);
|
||||
onDestroy(() => {
|
||||
stopPolling();
|
||||
if (visibilityHandler) document.removeEventListener('visibilitychange', visibilityHandler);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex h-screen overflow-hidden bg-[var(--color-bg-0)]">
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
import {
|
||||
fetchSandboxMetrics,
|
||||
METRIC_RANGES,
|
||||
METRIC_POLL_INTERVAL,
|
||||
METRIC_POLL_INTERVALS,
|
||||
type MetricRange,
|
||||
type MetricPoint
|
||||
} from '$lib/api/metrics';
|
||||
@ -80,6 +80,8 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let ChartJS = $state<any>(null);
|
||||
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let lastDataKey = ''; // fingerprint to skip redundant chart redraws
|
||||
let visibilityHandler: (() => void) | null = null;
|
||||
|
||||
const metricsAvailable = $derived(
|
||||
capsule?.status === 'running' || capsule?.status === 'paused'
|
||||
@ -141,6 +143,10 @@
|
||||
|
||||
function updateCharts() {
|
||||
if (!points.length) return;
|
||||
// Skip redraw if data hasn't changed.
|
||||
const key = `${points.length}:${points.at(-1)?.timestamp_unix ?? ''}`;
|
||||
if (key === lastDataKey) return;
|
||||
lastDataKey = key;
|
||||
const labels = formatLabels(Array.from(points), range);
|
||||
const w = smoothWindow(points.length);
|
||||
if (chartCpu) {
|
||||
@ -171,15 +177,20 @@
|
||||
|
||||
function setRange(r: MetricRange) {
|
||||
range = r;
|
||||
lastDataKey = ''; // force chart redraw on range switch
|
||||
goto(`?range=${r}`, { replaceState: true, noScroll: true, keepFocus: true });
|
||||
metricsLoading = true;
|
||||
restartPolling();
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
|
||||
}
|
||||
|
||||
function restartPolling() {
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
stopPolling();
|
||||
loadMetrics();
|
||||
pollInterval = setInterval(loadMetrics, METRIC_POLL_INTERVAL);
|
||||
pollInterval = setInterval(loadMetrics, METRIC_POLL_INTERVALS[range]);
|
||||
}
|
||||
|
||||
// Chart design tokens (match StatsPanel.svelte)
|
||||
@ -342,7 +353,7 @@
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
|
||||
stopPolling();
|
||||
chartCpu?.destroy(); chartCpu = null;
|
||||
chartRam?.destroy(); chartRam = null;
|
||||
};
|
||||
@ -367,10 +378,21 @@
|
||||
|
||||
const mod = await import('chart.js/auto');
|
||||
ChartJS = mod.Chart;
|
||||
|
||||
// Pause polling when the browser tab is hidden.
|
||||
visibilityHandler = () => {
|
||||
if (document.hidden) {
|
||||
stopPolling();
|
||||
} else if (activeTab === 'metrics' && metricsAvailable) {
|
||||
restartPolling();
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', visibilityHandler);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
stopPolling();
|
||||
if (visibilityHandler) document.removeEventListener('visibilitychange', visibilityHandler);
|
||||
chartCpu?.destroy();
|
||||
chartRam?.destroy();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user