perf(web): adapt dashboard refresh hydration

This commit is contained in:
vince 2026-06-20 14:47:53 +02:00
parent 30bff9da37
commit a071df3194
3 changed files with 502 additions and 16 deletions

View file

@ -9,11 +9,16 @@ import type {
import type { DashboardRuntimeState } from "$lib/server/dashboard";
import {
attachDashboardRefreshLifecycle,
collectVisibleDashboardModelIds,
createDashboardRefreshDelay,
createDashboardTileSnapshotStore,
createDashboardTileBackoff,
createDashboardRequestAborter,
runDashboardHydrationQueue,
runViewportAwareDashboardHydrationQueue,
shouldPauseDashboardRefresh,
waitForDashboardHydrationIdle,
type DashboardIntersectionObserverFactory,
type DashboardTileReference,
type DashboardTileSnapshotStoreContext,
type RestoredDashboardTileSnapshot,
} from "$lib/client/dashboard-refresh";
@ -29,12 +34,6 @@ import {
type UiDashboardPreview,
} from "@dimensionlab/ui";
type DashboardTileReference =
| { kind: "telemetry"; id: string }
| { kind: "service"; groupId: string; id: string }
| { kind: "module"; id: string }
| { kind: "status"; stripId: string; id: string };
type DashboardTileResponse =
| {
state: "ready";
@ -55,6 +54,8 @@ type DashboardTileResponse =
type DashboardTileHydrationResult = "aborted" | "failed" | "ready";
const dashboardTileHydrationConcurrency = 6;
const dashboardFallbackRefreshIntervalMs = 30_000;
const dashboardViewportObservationTimeoutMs = 80;
type DashboardTileSnapshotStore = ReturnType<typeof createDashboardTileSnapshotStore>;
export function AppStateView({
@ -170,8 +171,10 @@ export default function App() {
useEffect(() => {
let cancelled = false;
let refreshTimer: number | undefined;
let lastRefreshIntervalMs = dashboardFallbackRefreshIntervalMs;
let hydrationRun = 0;
const requestAborter = createDashboardRequestAborter();
const refreshDelay = createDashboardRefreshDelay();
const tileBackoff = createDashboardTileBackoff();
const tileSnapshotStore = createDashboardTileSnapshotStore(
getTileSnapshotStorage(),
@ -185,12 +188,22 @@ export default function App() {
}
function clearRefreshTimer() {
if (refreshTimer) {
window.clearInterval(refreshTimer);
if (refreshTimer !== undefined) {
window.clearTimeout(refreshTimer);
refreshTimer = undefined;
}
}
function scheduleNextRefresh(baseDelayMs: number) {
clearRefreshTimer();
if (cancelled || refreshPaused()) return;
refreshTimer = window.setTimeout(() => {
refreshTimer = undefined;
void loadDashboard();
}, refreshDelay.nextDelayMs(baseDelayMs));
}
function pauseRefreshes() {
clearRefreshTimer();
requestAborter.abortActiveRequests();
@ -200,6 +213,7 @@ export default function App() {
async function loadDashboard() {
if (cancelled || refreshPaused()) return;
clearRefreshTimer();
const shellSignal = requestAborter.beginShellRun();
try {
@ -207,6 +221,7 @@ export default function App() {
const nextDashboard = (await response.json()) as DashboardRuntimeState;
if (cancelled || shellSignal.aborted || refreshPaused()) return;
refreshDelay.recordSuccess();
const restored = restoreDashboardTileSnapshots(
nextDashboard,
nextDashboard.state === "ready"
@ -247,22 +262,20 @@ export default function App() {
setHydratingItemIds(new Set());
}
clearRefreshTimer();
const refreshIntervalSeconds =
nextDashboard.state === "ready"
? nextDashboard.document.metadata.refreshIntervalSeconds
: undefined;
if (refreshIntervalSeconds && !refreshPaused()) {
refreshTimer = window.setInterval(
() => void loadDashboard(),
refreshIntervalSeconds * 1000,
);
lastRefreshIntervalMs = refreshIntervalSeconds * 1000;
scheduleNextRefresh(lastRefreshIntervalMs);
}
} catch (error) {
if (!isAbortError(error) && !cancelled) {
refreshDelay.recordFailure();
console.error("Dashboard refresh failed", error);
scheduleNextRefresh(lastRefreshIntervalMs);
}
}
}
@ -273,8 +286,23 @@ export default function App() {
signal: AbortSignal,
snapshotContext: DashboardTileSnapshotStoreContext,
) {
void runDashboardHydrationQueue({
const modelIds = tiles.map(dashboardTileModelId);
void runViewportAwareDashboardHydrationQueue({
collectVisibleModelIds: async () => {
await waitForDashboardRenderFrame(signal);
return collectVisibleDashboardModelIds({
clearTimeout: window.clearTimeout.bind(window),
createObserver: getDashboardIntersectionObserverFactory(),
documentTarget: document,
modelIds,
setTimeout: window.setTimeout.bind(window),
signal,
timeoutMs: dashboardViewportObservationTimeoutMs,
});
},
concurrency: dashboardTileHydrationConcurrency,
getModelId: dashboardTileModelId,
hydrate: async (tile) => {
const key = dashboardTileKey(tile);
const result = await hydrateDashboardTile(
@ -292,6 +320,13 @@ export default function App() {
},
items: tiles,
signal,
waitForIdle: () =>
waitForDashboardHydrationIdle({
...getDashboardIdleCallbacks(),
clearTimeout: window.clearTimeout.bind(window),
setTimeout: window.setTimeout.bind(window),
signal,
}),
});
}
@ -406,6 +441,53 @@ function getTileSnapshotStorage(): Storage | undefined {
}
}
type DashboardIdleWindow = Window & {
cancelIdleCallback?: (handle: number) => void;
requestIdleCallback?: (
callback: () => void,
options?: { timeout?: number },
) => number;
};
function getDashboardIdleCallbacks() {
const idleWindow = window as DashboardIdleWindow;
return {
cancelIdleCallback: idleWindow.cancelIdleCallback?.bind(idleWindow),
requestIdleCallback: idleWindow.requestIdleCallback?.bind(idleWindow),
};
}
function getDashboardIntersectionObserverFactory():
| DashboardIntersectionObserverFactory
| undefined {
if (typeof window.IntersectionObserver === "undefined") return undefined;
return (callback) =>
new window.IntersectionObserver((entries) => {
callback(entries);
});
}
async function waitForDashboardRenderFrame(signal: AbortSignal): Promise<void> {
if (signal.aborted) return;
await new Promise<void>((resolve) => {
let settled = false;
let frame: number | undefined;
function finish() {
if (settled) return;
settled = true;
if (frame !== undefined) window.cancelAnimationFrame(frame);
signal.removeEventListener("abort", finish);
resolve();
}
signal.addEventListener("abort", finish, { once: true });
frame = window.requestAnimationFrame(finish);
});
}
function isAbortError(error: unknown): boolean {
return (
typeof error === "object" &&
@ -539,6 +621,11 @@ function dashboardTileKey(tile: DashboardTileReference): string {
return `${tile.kind}:${tile.id}`;
}
function dashboardTileModelId(tile: DashboardTileReference): string {
if (tile.kind === "status") return `${tile.stripId}:${tile.id}`;
return tile.id;
}
function dashboardTileUrl(tile: DashboardTileReference): string {
const parts = tile.kind === "status"
? ["api", "dashboard", "tile", tile.kind, tile.stripId, tile.id]