1
0
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:
2026-04-11 06:20:29 +06:00
parent dbad418093
commit 21b82c2283
4 changed files with 77 additions and 10 deletions

View File

@ -21,5 +21,14 @@ export async function fetchSandboxMetrics(id: string, range: MetricRange): Promi
export const METRIC_RANGES: MetricRange[] = ['5m', '10m', '1h', '6h', '24h']; 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; export const METRIC_POLL_INTERVAL = 10_000;

View File

@ -26,6 +26,8 @@
let chartRam: any = null; let chartRam: any = null;
let pollInterval: ReturnType<typeof setInterval> | null = 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() { async function load() {
const result = await fetchStats(range); const result = await fetchStats(range);
@ -43,6 +45,10 @@
function updateCharts() { function updateCharts() {
if (!stats) return; 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 // 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. // wraps arrays in reactive proxies which Chart.js can't iterate reliably.
const labels = formatLabels(Array.from(stats.series.labels), range); const labels = formatLabels(Array.from(stats.series.labels), range);
@ -77,14 +83,19 @@
}); });
} }
function stopPolling() {
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
}
function restartPolling() { function restartPolling() {
if (pollInterval) clearInterval(pollInterval); stopPolling();
load(); load();
pollInterval = setInterval(load, POLL_INTERVALS[range]); pollInterval = setInterval(load, POLL_INTERVALS[range]);
} }
function setRange(r: TimeRange) { function setRange(r: TimeRange) {
range = r; range = r;
lastDataKey = ''; // force chart redraw on range switch
goto(`?range=${r}`, { replaceState: true, noScroll: true, keepFocus: true }); goto(`?range=${r}`, { replaceState: true, noScroll: true, keepFocus: true });
restartPolling(); restartPolling();
} }
@ -237,10 +248,21 @@
updateCharts(); updateCharts();
restartPolling(); 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(() => { onDestroy(() => {
if (pollInterval) clearInterval(pollInterval); stopPolling();
if (visibilityHandler) document.removeEventListener('visibilitychange', visibilityHandler);
chartRunning?.destroy(); chartRunning?.destroy();
chartCpu?.destroy(); chartCpu?.destroy();
chartRam?.destroy(); chartRam?.destroy();

View File

@ -36,6 +36,7 @@
// Polling // Polling
let pollInterval: ReturnType<typeof setInterval> | null = null; let pollInterval: ReturnType<typeof setInterval> | null = null;
let hasActiveBuilds = $derived(builds.some((b) => b.status === 'pending' || b.status === 'running')); let hasActiveBuilds = $derived(builds.some((b) => b.status === 'pending' || b.status === 'running'));
let visibilityHandler: (() => void) | null = null;
// Build log expansion // Build log expansion
let expandedBuildId = $state<string | null>(null); let expandedBuildId = $state<string | null>(null);
@ -97,7 +98,7 @@
function startPolling() { function startPolling() {
stopPolling(); stopPolling();
pollInterval = setInterval(() => { pollInterval = setInterval(() => {
if (hasActiveBuilds) fetchBuilds(); if (hasActiveBuilds && activeTab === 'builds') fetchBuilds();
}, 3000); }, 3000);
} }
@ -242,9 +243,22 @@
onMount(() => { onMount(() => {
fetchTemplates(); fetchTemplates();
fetchBuilds().then(startPolling); 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> </script>
<div class="flex h-screen overflow-hidden bg-[var(--color-bg-0)]"> <div class="flex h-screen overflow-hidden bg-[var(--color-bg-0)]">

View File

@ -11,7 +11,7 @@
import { import {
fetchSandboxMetrics, fetchSandboxMetrics,
METRIC_RANGES, METRIC_RANGES,
METRIC_POLL_INTERVAL, METRIC_POLL_INTERVALS,
type MetricRange, type MetricRange,
type MetricPoint type MetricPoint
} from '$lib/api/metrics'; } from '$lib/api/metrics';
@ -80,6 +80,8 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
let ChartJS = $state<any>(null); let ChartJS = $state<any>(null);
let pollInterval: ReturnType<typeof setInterval> | null = null; let pollInterval: ReturnType<typeof setInterval> | null = null;
let lastDataKey = ''; // fingerprint to skip redundant chart redraws
let visibilityHandler: (() => void) | null = null;
const metricsAvailable = $derived( const metricsAvailable = $derived(
capsule?.status === 'running' || capsule?.status === 'paused' capsule?.status === 'running' || capsule?.status === 'paused'
@ -141,6 +143,10 @@
function updateCharts() { function updateCharts() {
if (!points.length) return; 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 labels = formatLabels(Array.from(points), range);
const w = smoothWindow(points.length); const w = smoothWindow(points.length);
if (chartCpu) { if (chartCpu) {
@ -171,15 +177,20 @@
function setRange(r: MetricRange) { function setRange(r: MetricRange) {
range = r; range = r;
lastDataKey = ''; // force chart redraw on range switch
goto(`?range=${r}`, { replaceState: true, noScroll: true, keepFocus: true }); goto(`?range=${r}`, { replaceState: true, noScroll: true, keepFocus: true });
metricsLoading = true; metricsLoading = true;
restartPolling(); restartPolling();
} }
function stopPolling() {
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
}
function restartPolling() { function restartPolling() {
if (pollInterval) clearInterval(pollInterval); stopPolling();
loadMetrics(); loadMetrics();
pollInterval = setInterval(loadMetrics, METRIC_POLL_INTERVAL); pollInterval = setInterval(loadMetrics, METRIC_POLL_INTERVALS[range]);
} }
// Chart design tokens (match StatsPanel.svelte) // Chart design tokens (match StatsPanel.svelte)
@ -342,7 +353,7 @@
}); });
return () => { return () => {
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } stopPolling();
chartCpu?.destroy(); chartCpu = null; chartCpu?.destroy(); chartCpu = null;
chartRam?.destroy(); chartRam = null; chartRam?.destroy(); chartRam = null;
}; };
@ -367,10 +378,21 @@
const mod = await import('chart.js/auto'); const mod = await import('chart.js/auto');
ChartJS = mod.Chart; 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(() => { onDestroy(() => {
if (pollInterval) clearInterval(pollInterval); stopPolling();
if (visibilityHandler) document.removeEventListener('visibilitychange', visibilityHandler);
chartCpu?.destroy(); chartCpu?.destroy();
chartRam?.destroy(); chartRam?.destroy();
}); });