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'];
|
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;
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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)]">
|
||||||
|
|||||||
@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user