perf(web): adapt dashboard refresh hydration #49

Merged
vince merged 1 commit from codex/dashboard-adaptive-hydration into main 2026-06-20 14:52:01 +02:00
3 changed files with 502 additions and 16 deletions
Showing only changes of commit a071df3194 - Show all commits

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]

View file

@ -1,11 +1,17 @@
import { describe, expect, test } from "vitest";
import {
attachDashboardRefreshLifecycle,
collectVisibleDashboardModelIds,
createDashboardRefreshDelay,
createDashboardTileSnapshotStore,
createDashboardTileBackoff,
createDashboardRequestAborter,
runDashboardHydrationQueue,
runViewportAwareDashboardHydrationQueue,
shouldPauseDashboardRefresh,
splitDashboardHydrationItemsByVisibility,
waitForDashboardHydrationIdle,
type DashboardIntersectionEntry,
} from "./dashboard-refresh";
describe("dashboard refresh lifecycle", () => {
@ -113,6 +119,134 @@ describe("dashboard refresh lifecycle", () => {
expect(maxActive).toBe(2);
});
test("hydrates visible items before deferred items and waits for idle", async () => {
const calls: string[] = [];
const idleReleases: Array<() => void> = [];
const hydration = runViewportAwareDashboardHydrationQueue({
collectVisibleModelIds: async () => new Set(["b"]),
concurrency: 1,
getModelId: (item) => item,
hydrate: async (item) => {
calls.push(item);
},
items: ["a", "b", "c"],
signal: new AbortController().signal,
waitForIdle: async () => {
calls.push("idle");
await new Promise<void>((resolve) => idleReleases.push(resolve));
},
});
await waitFor(() => calls.includes("idle"));
expect(calls).toEqual(["b", "idle"]);
idleReleases.shift()?.();
await hydration;
expect(calls).toEqual(["b", "idle", "a", "c"]);
});
test("splits visible hydration items while preserving document order", () => {
expect(splitDashboardHydrationItemsByVisibility({
getModelId: (item) => item.id,
items: [{ id: "status" }, { id: "telemetry" }, { id: "service" }],
visibleModelIds: new Set(["service", "status"]),
})).toEqual({
visible: [{ id: "status" }, { id: "service" }],
deferred: [{ id: "telemetry" }],
});
});
test("collects visible data-model-id elements with IntersectionObserver", async () => {
const elements = [
modelElement("status"),
modelElement("telemetry"),
modelElement("unrelated"),
];
let callback:
| ((entries: DashboardIntersectionEntry[]) => void)
| undefined;
let finishObservation: (() => void) | undefined;
let disconnected = false;
const observed: string[] = [];
const visible = collectVisibleDashboardModelIds({
createObserver: (observerCallback) => {
callback = observerCallback;
return {
disconnect() {
disconnected = true;
},
observe(element) {
observed.push(element.getAttribute("data-model-id") || "");
},
};
},
documentTarget: {
querySelectorAll: () => elements,
},
modelIds: ["status", "telemetry"],
setTimeout: (handler) => {
finishObservation = handler;
return 1 as unknown as ReturnType<typeof setTimeout>;
},
clearTimeout: () => undefined,
signal: new AbortController().signal,
});
callback?.([
{
isIntersecting: false,
intersectionRatio: 0,
target: elements[0],
},
{
isIntersecting: true,
target: elements[1],
},
]);
finishObservation?.();
expect(await visible).toEqual(new Set(["telemetry"]));
expect(observed).toEqual(["status", "telemetry"]);
expect(disconnected).toBe(true);
});
test("waits for requestIdleCallback when available", async () => {
let idleCallback: (() => void) | undefined;
let cancelledIdle: number | undefined;
const wait = waitForDashboardHydrationIdle({
cancelIdleCallback: (handle) => {
cancelledIdle = handle;
},
requestIdleCallback: (callback) => {
idleCallback = callback;
return 7;
},
signal: new AbortController().signal,
});
expect(cancelledIdle).toBeUndefined();
idleCallback?.();
await wait;
expect(cancelledIdle).toBe(7);
});
test("jitters refresh delays and slows repeated failures", () => {
const delay = createDashboardRefreshDelay({
jitterRatio: 0.1,
random: () => 1,
});
expect(delay.nextDelayMs(1_000)).toBe(1_100);
delay.recordFailure();
expect(delay.nextDelayMs(1_000)).toBe(2_200);
delay.recordFailure();
expect(delay.nextDelayMs(1_000)).toBe(4_400);
delay.recordSuccess();
expect(delay.nextDelayMs(1_000)).toBe(1_100);
});
test("backs off failed tile keys and resets after success", () => {
const backoff = createDashboardTileBackoff();
@ -299,3 +433,11 @@ function createMemoryStorage(): Storage {
},
};
}
function modelElement(modelId: string) {
return {
getAttribute(name: string) {
return name === "data-model-id" ? modelId : null;
},
};
}

View file

@ -151,6 +151,263 @@ export async function runDashboardHydrationQueue<TItem>(
);
}
export interface DashboardViewportHydrationQueueOptions<TItem> {
collectVisibleModelIds: () => Promise<ReadonlySet<string>>;
concurrency: number;
getModelId: (item: TItem) => string;
hydrate: (item: TItem) => Promise<void> | void;
items: TItem[];
signal: AbortSignal;
waitForIdle: () => Promise<void>;
}
export async function runViewportAwareDashboardHydrationQueue<TItem>(
options: DashboardViewportHydrationQueueOptions<TItem>,
): Promise<void> {
const visibleModelIds = await options.collectVisibleModelIds();
if (options.signal.aborted) return;
const { visible, deferred } = splitDashboardHydrationItemsByVisibility({
getModelId: options.getModelId,
items: options.items,
visibleModelIds,
});
await runDashboardHydrationQueue({
concurrency: options.concurrency,
hydrate: options.hydrate,
items: visible,
signal: options.signal,
});
if (!deferred.length || options.signal.aborted) return;
await options.waitForIdle();
if (options.signal.aborted) return;
await runDashboardHydrationQueue({
concurrency: options.concurrency,
hydrate: options.hydrate,
items: deferred,
signal: options.signal,
});
}
export function splitDashboardHydrationItemsByVisibility<TItem>(options: {
getModelId: (item: TItem) => string;
items: TItem[];
visibleModelIds: ReadonlySet<string>;
}): {
deferred: TItem[];
visible: TItem[];
} {
const visible: TItem[] = [];
const deferred: TItem[] = [];
for (const item of options.items) {
if (options.visibleModelIds.has(options.getModelId(item))) {
visible.push(item);
} else {
deferred.push(item);
}
}
return { visible, deferred };
}
export interface DashboardModelElement {
getAttribute(name: string): string | null;
}
export interface DashboardViewportElementSource {
querySelectorAll(selector: string): ArrayLike<DashboardModelElement>;
}
export interface DashboardIntersectionEntry {
intersectionRatio?: number;
isIntersecting: boolean;
target: DashboardModelElement;
}
export interface DashboardIntersectionObserver {
disconnect(): void;
observe(element: DashboardModelElement): void;
}
export type DashboardIntersectionObserverFactory = (
callback: (entries: DashboardIntersectionEntry[]) => void,
) => DashboardIntersectionObserver;
type DashboardTimerHandle = ReturnType<typeof setTimeout>;
export interface DashboardVisibleModelIdCollectorOptions {
clearTimeout?: (handle: DashboardTimerHandle) => void;
createObserver?: DashboardIntersectionObserverFactory;
documentTarget: DashboardViewportElementSource;
modelIds: Iterable<string>;
setTimeout?: (callback: () => void, timeoutMs: number) => DashboardTimerHandle;
signal: AbortSignal;
timeoutMs?: number;
}
export async function collectVisibleDashboardModelIds(
options: DashboardVisibleModelIdCollectorOptions,
): Promise<Set<string>> {
const targetIds = new Set(options.modelIds);
if (!targetIds.size || options.signal.aborted) return new Set();
const elements = Array.from(
options.documentTarget.querySelectorAll("[data-model-id]"),
).filter((element) => {
const modelId = element.getAttribute("data-model-id");
return modelId ? targetIds.has(modelId) : false;
});
if (!elements.length) return new Set();
if (!options.createObserver) return targetIds;
const createObserver = options.createObserver;
const setTimer =
options.setTimeout ||
((callback: () => void, timeoutMs: number) =>
globalThis.setTimeout(callback, timeoutMs));
const clearTimer =
options.clearTimeout ||
((handle: DashboardTimerHandle) => globalThis.clearTimeout(handle));
const timeoutMs = options.timeoutMs ?? 80;
return new Promise((resolve) => {
const visibleModelIds = new Set<string>();
let settled = false;
let timeoutHandle: DashboardTimerHandle | undefined;
let observer: DashboardIntersectionObserver | undefined;
function cleanup() {
if (timeoutHandle !== undefined) {
clearTimer(timeoutHandle);
}
observer?.disconnect();
options.signal.removeEventListener("abort", finish);
}
function finish() {
if (settled) return;
settled = true;
cleanup();
resolve(visibleModelIds);
}
observer = createObserver((entries) => {
for (const entry of entries) {
const modelId = entry.target.getAttribute("data-model-id");
if (
modelId &&
targetIds.has(modelId) &&
(entry.isIntersecting || (entry.intersectionRatio ?? 0) > 0)
) {
visibleModelIds.add(modelId);
}
}
});
for (const element of elements) {
observer.observe(element);
}
options.signal.addEventListener("abort", finish, { once: true });
timeoutHandle = setTimer(finish, timeoutMs);
});
}
export interface DashboardHydrationIdleOptions {
cancelIdleCallback?: (handle: number) => void;
clearTimeout?: (handle: DashboardTimerHandle) => void;
requestIdleCallback?: (
callback: () => void,
options?: { timeout?: number },
) => number;
setTimeout?: (callback: () => void, timeoutMs: number) => DashboardTimerHandle;
signal: AbortSignal;
timeoutMs?: number;
}
export async function waitForDashboardHydrationIdle(
options: DashboardHydrationIdleOptions,
): Promise<void> {
if (options.signal.aborted) return;
const setTimer =
options.setTimeout ||
((callback: () => void, timeoutMs: number) =>
globalThis.setTimeout(callback, timeoutMs));
const clearTimer =
options.clearTimeout ||
((handle: DashboardTimerHandle) => globalThis.clearTimeout(handle));
await new Promise<void>((resolve) => {
let settled = false;
let idleHandle: number | undefined;
let timeoutHandle: DashboardTimerHandle | undefined;
function cleanup() {
if (idleHandle !== undefined) {
options.cancelIdleCallback?.(idleHandle);
}
if (timeoutHandle !== undefined) {
clearTimer(timeoutHandle);
}
options.signal.removeEventListener("abort", finish);
}
function finish() {
if (settled) return;
settled = true;
cleanup();
resolve();
}
options.signal.addEventListener("abort", finish, { once: true });
if (options.requestIdleCallback) {
idleHandle = options.requestIdleCallback(finish, {
timeout: options.timeoutMs ?? 1_000,
});
} else {
timeoutHandle = setTimer(finish, 0);
}
});
}
export interface DashboardRefreshDelayOptions {
failureMultiplierLimit?: number;
jitterRatio?: number;
random?: () => number;
}
export function createDashboardRefreshDelay(
options: DashboardRefreshDelayOptions = {},
) {
const random = options.random || Math.random;
const jitterRatio = options.jitterRatio ?? 0.1;
const failureMultiplierLimit = options.failureMultiplierLimit ?? 8;
let consecutiveFailures = 0;
return {
nextDelayMs(baseDelayMs: number): number {
const failureMultiplier = consecutiveFailures
? Math.min(2 ** consecutiveFailures, failureMultiplierLimit)
: 1;
const jitterFactor = 1 + ((random() * 2) - 1) * jitterRatio;
return Math.max(0, Math.round(baseDelayMs * failureMultiplier * jitterFactor));
},
recordFailure(): void {
consecutiveFailures += 1;
},
recordSuccess(): void {
consecutiveFailures = 0;
},
};
}
const dashboardTileBackoffDelaysMs = [15_000, 30_000, 60_000, 120_000];
export function createDashboardTileBackoff() {