perf(web): restore last known dashboard tiles

This commit is contained in:
vince 2026-06-20 14:25:43 +02:00
parent 243d71686d
commit 4ce42fb52b
4 changed files with 412 additions and 8 deletions

View file

@ -2,7 +2,11 @@ import { renderToString } from "react-dom/server";
import { describe, expect, test } from "vitest";
import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
import { AppStateView, dashboardHydrationTiles } from "./App";
import {
AppStateView,
dashboardHydrationTiles,
restoreDashboardTileSnapshots,
} from "./App";
describe("React app dashboard state view", () => {
test("renders loading dashboard state", () => {
@ -120,4 +124,39 @@ describe("React app dashboard state view", () => {
),
);
});
test("applies restored tile snapshots without marking them as loading", () => {
const restored = restoreDashboardTileSnapshots(
{
state: "ready",
document: dimensionLabDashboardFixture,
schemaVersion: "dashboard.v1",
currentRevisionId: "revision-a",
},
[
{
ageMs: 60_000,
response: {
state: "ready",
tile: { kind: "telemetry", id: "infra-ram" },
item: {
...dimensionLabDashboardFixture.telemetry[0],
detail: "cached - stale 60s",
severity: "stale",
},
},
},
],
);
expect(restored.restoredItemIds).toEqual(new Set(["telemetry:infra-ram"]));
if (restored.dashboard.state !== "ready") {
throw new Error("Expected dashboard to be ready");
}
expect(restored.dashboard.document.telemetry[0]).toMatchObject({
id: "infra-ram",
detail: "cached - stale 60s",
severity: "stale",
});
});
});

View file

@ -9,10 +9,13 @@ import type {
import type { DashboardRuntimeState } from "$lib/server/dashboard";
import {
attachDashboardRefreshLifecycle,
createDashboardTileSnapshotStore,
createDashboardTileBackoff,
createDashboardRequestAborter,
runDashboardHydrationQueue,
shouldPauseDashboardRefresh,
type DashboardTileSnapshotStoreContext,
type RestoredDashboardTileSnapshot,
} from "$lib/client/dashboard-refresh";
import { dashboardDocumentToUiDashboard } from "$lib/ui-adapter/model-renderer";
import {
@ -47,6 +50,7 @@ type DashboardTileResponse =
type DashboardTileHydrationResult = "aborted" | "failed" | "ready";
const dashboardTileHydrationConcurrency = 6;
type DashboardTileSnapshotStore = ReturnType<typeof createDashboardTileSnapshotStore>;
export function AppStateView({
dashboard,
@ -164,6 +168,9 @@ export default function App() {
let hydrationRun = 0;
const requestAborter = createDashboardRequestAborter();
const tileBackoff = createDashboardTileBackoff();
const tileSnapshotStore = createDashboardTileSnapshotStore(
getTileSnapshotStorage(),
);
function refreshPaused() {
return shouldPauseDashboardRefresh({
@ -195,19 +202,42 @@ export default function App() {
const nextDashboard = (await response.json()) as DashboardRuntimeState;
if (cancelled || shellSignal.aborted || refreshPaused()) return;
setDashboard(nextDashboard);
const restored = restoreDashboardTileSnapshots(
nextDashboard,
nextDashboard.state === "ready"
? tileSnapshotStore.restore({
currentRevisionId: nextDashboard.currentRevisionId,
schemaVersion: nextDashboard.schemaVersion,
})
: [],
);
setDashboard(restored.dashboard);
const currentRun = ++hydrationRun;
if (
nextDashboard.state === "ready" &&
nextDashboard.liveDatasourceHydration?.enabled !== false
restored.dashboard.state === "ready" &&
restored.dashboard.liveDatasourceHydration?.enabled !== false
) {
const tiles = dashboardHydrationTiles(nextDashboard.document).filter(
const tiles = dashboardHydrationTiles(restored.dashboard.document).filter(
(tile) => tileBackoff.canAttempt(dashboardTileKey(tile)),
);
const tileSignal = requestAborter.beginTileRun();
setHydratingItemIds(new Set(tiles.map(dashboardTileKey)));
hydrateDashboardTiles(tiles, currentRun, tileSignal);
setHydratingItemIds(
new Set(
tiles
.map(dashboardTileKey)
.filter((key) => !restored.restoredItemIds.has(key)),
),
);
hydrateDashboardTiles(
tiles,
currentRun,
tileSignal,
{
currentRevisionId: restored.dashboard.currentRevisionId,
schemaVersion: restored.dashboard.schemaVersion,
},
);
} else {
setHydratingItemIds(new Set());
}
@ -236,12 +266,18 @@ export default function App() {
tiles: DashboardTileReference[],
run: number,
signal: AbortSignal,
snapshotContext: DashboardTileSnapshotStoreContext,
) {
void runDashboardHydrationQueue({
concurrency: dashboardTileHydrationConcurrency,
hydrate: async (tile) => {
const key = dashboardTileKey(tile);
const result = await hydrateDashboardTile(tile, run, signal);
const result = await hydrateDashboardTile(
tile,
run,
signal,
snapshotContext,
);
if (result === "ready") {
tileBackoff.recordSuccess(key);
@ -258,6 +294,7 @@ export default function App() {
tile: DashboardTileReference,
run: number,
signal: AbortSignal,
snapshotContext: DashboardTileSnapshotStoreContext,
): Promise<DashboardTileHydrationResult> {
const key = dashboardTileKey(tile);
@ -286,6 +323,10 @@ export default function App() {
}
: current,
);
tileSnapshotStore.saveReadyTile({
...snapshotContext,
response: readyTileResponse,
});
return "ready";
} catch (error) {
if (!isAbortError(error) && !cancelled) {
@ -352,6 +393,14 @@ function getThemeStorage(): Storage | undefined {
}
}
function getTileSnapshotStorage(): Storage | undefined {
try {
return window.localStorage;
} catch {
return undefined;
}
}
function isAbortError(error: unknown): boolean {
return (
typeof error === "object" &&
@ -361,6 +410,41 @@ function isAbortError(error: unknown): boolean {
);
}
export function restoreDashboardTileSnapshots(
dashboard: DashboardRuntimeState,
snapshots: RestoredDashboardTileSnapshot[],
): {
dashboard: DashboardRuntimeState;
restoredItemIds: Set<string>;
} {
if (dashboard.state !== "ready" || snapshots.length === 0) {
return {
dashboard,
restoredItemIds: new Set(),
};
}
return snapshots.reduce(
(current, snapshot) => ({
dashboard: {
...current.dashboard,
document: applyDashboardTile(
current.dashboard.document,
snapshot.response as Extract<DashboardTileResponse, { state: "ready" }>,
),
},
restoredItemIds: new Set([
...current.restoredItemIds,
dashboardTileKey(snapshot.response.tile),
]),
}),
{
dashboard,
restoredItemIds: new Set<string>(),
},
);
}
function markHydratingItems(
dashboard: UiDashboardPreview,
hydratingItemIds?: ReadonlySet<string>,

View file

@ -1,6 +1,7 @@
import { describe, expect, test } from "vitest";
import {
attachDashboardRefreshLifecycle,
createDashboardTileSnapshotStore,
createDashboardTileBackoff,
createDashboardRequestAborter,
runDashboardHydrationQueue,
@ -131,6 +132,67 @@ describe("dashboard refresh lifecycle", () => {
backoff.recordSuccess("telemetry:infra-ram");
expect(backoff.canAttempt("telemetry:infra-ram", 107_000)).toBe(true);
});
test("stores only ready tile snapshots and restores them for matching revisions", () => {
const storage = createMemoryStorage();
let now = 1_000;
const store = createDashboardTileSnapshotStore(storage, {
now: () => now,
});
store.saveReadyTile({
currentRevisionId: "revision-a",
response: {
state: "ready",
tile: { kind: "telemetry", id: "infra-ram" },
item: {
id: "infra-ram",
label: "Infra RAM",
value: { kind: "percent", value: 42 },
detail: "live",
severity: "ok",
},
},
schemaVersion: "dashboard.v1",
});
store.saveTile({
currentRevisionId: "revision-a",
response: {
state: "not_found",
tile: { kind: "telemetry", id: "missing" },
message: "Missing",
},
schemaVersion: "dashboard.v1",
});
now = 61_000;
const restored = store.restore({
currentRevisionId: "revision-a",
schemaVersion: "dashboard.v1",
});
expect(restored).toEqual([
{
ageMs: 60_000,
response: {
state: "ready",
tile: { kind: "telemetry", id: "infra-ram" },
item: {
id: "infra-ram",
label: "Infra RAM",
value: { kind: "percent", value: 42 },
detail: "live - stale 60s",
severity: "stale",
},
},
},
]);
expect(store.restore({
currentRevisionId: "revision-b",
schemaVersion: "dashboard.v1",
})).toEqual([]);
});
});
async function waitFor(predicate: () => boolean) {
@ -141,3 +203,27 @@ async function waitFor(predicate: () => boolean) {
throw new Error("condition was not met");
}
function createMemoryStorage(): Storage {
const values = new Map<string, string>();
return {
get length() {
return values.size;
},
clear() {
values.clear();
},
getItem(key) {
return values.get(key) ?? null;
},
key(index) {
return [...values.keys()][index] ?? null;
},
removeItem(key) {
values.delete(key);
},
setItem(key, value) {
values.set(key, value);
},
};
}

View file

@ -3,6 +3,39 @@ export interface DashboardRefreshPauseState {
online: boolean;
}
export type DashboardTileReference =
| { kind: "telemetry"; id: string }
| { kind: "service"; groupId: string; id: string }
| { kind: "module"; id: string }
| { kind: "status"; stripId: string; id: string };
type SnapshotMetricValue = Record<string, unknown>;
export type DashboardTileSnapshotItem = {
detail?: string;
id: string;
label?: string;
severity?: string;
value?: SnapshotMetricValue | string;
} & Record<string, unknown>;
export type DashboardTileSnapshotResponse =
| {
state: "ready";
tile: DashboardTileReference;
item: DashboardTileSnapshotItem;
}
| {
state: "not_found";
tile: DashboardTileReference;
message: string;
}
| {
state: "disabled";
tile: DashboardTileReference;
message: string;
};
export function shouldPauseDashboardRefresh(
state: DashboardRefreshPauseState,
): boolean {
@ -145,3 +178,165 @@ export function createDashboardTileBackoff() {
},
};
}
interface DashboardTileSnapshotRecord {
item: DashboardTileSnapshotItem;
savedAt: number;
tile: DashboardTileReference;
}
interface DashboardTileSnapshotPayload {
currentRevisionId: string;
schemaVersion: string;
tiles: DashboardTileSnapshotRecord[];
version: 1;
}
export interface DashboardTileSnapshotStoreContext {
currentRevisionId: string;
schemaVersion: string;
}
export interface DashboardTileSnapshotStoreOptions {
now?: () => number;
}
export interface RestoredDashboardTileSnapshot {
ageMs: number;
response: Extract<DashboardTileSnapshotResponse, { state: "ready" }>;
}
const dashboardTileSnapshotStorageKey = "dimensionlab.dashboard.tiles.v1";
export function createDashboardTileSnapshotStore(
storage: Storage | undefined,
options: DashboardTileSnapshotStoreOptions = {},
) {
const now = options.now || Date.now;
function read(): DashboardTileSnapshotPayload | null {
if (!storage) return null;
try {
const serialized = storage.getItem(dashboardTileSnapshotStorageKey);
if (!serialized) return null;
const payload = JSON.parse(serialized) as Partial<DashboardTileSnapshotPayload>;
if (
payload.version !== 1 ||
typeof payload.currentRevisionId !== "string" ||
typeof payload.schemaVersion !== "string" ||
!Array.isArray(payload.tiles)
) {
return null;
}
return payload as DashboardTileSnapshotPayload;
} catch {
return null;
}
}
function write(payload: DashboardTileSnapshotPayload): void {
if (!storage) return;
try {
storage.setItem(dashboardTileSnapshotStorageKey, JSON.stringify(payload));
} catch {
// Best-effort warm-start cache; quota and privacy failures are non-fatal.
}
}
function matchingPayload(
context: DashboardTileSnapshotStoreContext,
): DashboardTileSnapshotPayload {
const payload = read();
if (
payload &&
payload.currentRevisionId === context.currentRevisionId &&
payload.schemaVersion === context.schemaVersion
) {
return payload;
}
return {
currentRevisionId: context.currentRevisionId,
schemaVersion: context.schemaVersion,
tiles: [],
version: 1,
};
}
function saveReadyTile(
input: DashboardTileSnapshotStoreContext & {
response: Extract<DashboardTileSnapshotResponse, { state: "ready" }>;
},
): void {
const payload = matchingPayload(input);
const key = dashboardTileSnapshotKey(input.response.tile);
const nextRecord: DashboardTileSnapshotRecord = {
item: input.response.item,
savedAt: now(),
tile: input.response.tile,
};
payload.tiles = [
nextRecord,
...payload.tiles.filter((record) =>
dashboardTileSnapshotKey(record.tile) !== key
),
];
write(payload);
}
return {
restore(context: DashboardTileSnapshotStoreContext): RestoredDashboardTileSnapshot[] {
const payload = read();
if (
!payload ||
payload.currentRevisionId !== context.currentRevisionId ||
payload.schemaVersion !== context.schemaVersion
) {
return [];
}
const restoredAt = now();
return payload.tiles.map((record) => ({
ageMs: Math.max(0, restoredAt - record.savedAt),
response: {
state: "ready",
tile: record.tile,
item: {
...record.item,
detail: staleDashboardTileDetail(record.item.detail, restoredAt - record.savedAt),
severity: "stale",
},
},
}));
},
saveReadyTile,
saveTile(input: DashboardTileSnapshotStoreContext & {
response: DashboardTileSnapshotResponse;
}): void {
if (input.response.state === "ready") {
saveReadyTile({
currentRevisionId: input.currentRevisionId,
response: input.response,
schemaVersion: input.schemaVersion,
});
}
},
};
}
function dashboardTileSnapshotKey(tile: DashboardTileReference): string {
return JSON.stringify(tile);
}
function staleDashboardTileDetail(
detail: string | undefined,
ageMs: number,
): string | undefined {
if (!detail) return detail;
const ageSeconds = Math.max(0, Math.floor(ageMs / 1_000));
return ageSeconds > 0 ? `${detail} - stale ${ageSeconds}s` : detail;
}