perf(web): add dashboard hydration observability

This commit is contained in:
vince 2026-06-20 14:58:25 +02:00
parent b6de67f081
commit 328f6e00bf
5 changed files with 339 additions and 22 deletions

View file

@ -2,7 +2,9 @@ import { describe, expect, test } from "vitest";
import {
attachDashboardRefreshLifecycle,
collectVisibleDashboardModelIds,
createDashboardPerformanceMarks,
createDashboardRefreshDelay,
dashboardPerformanceMarks,
createDashboardTileSnapshotStore,
createDashboardTileBackoff,
createDashboardRequestAborter,
@ -131,6 +133,8 @@ describe("dashboard refresh lifecycle", () => {
calls.push(item);
},
items: ["a", "b", "c"],
onAllItemsSettled: () => calls.push("all-settled"),
onVisibleItemsSettled: () => calls.push("visible-settled"),
signal: new AbortController().signal,
waitForIdle: async () => {
calls.push("idle");
@ -139,11 +143,18 @@ describe("dashboard refresh lifecycle", () => {
});
await waitFor(() => calls.includes("idle"));
expect(calls).toEqual(["b", "idle"]);
expect(calls).toEqual(["b", "visible-settled", "idle"]);
idleReleases.shift()?.();
await hydration;
expect(calls).toEqual(["b", "idle", "a", "c"]);
expect(calls).toEqual([
"b",
"visible-settled",
"idle",
"a",
"c",
"all-settled",
]);
});
test("splits visible hydration items while preserving document order", () => {
@ -247,6 +258,30 @@ describe("dashboard refresh lifecycle", () => {
expect(delay.nextDelayMs(1_000)).toBe(1_100);
});
test("marks dashboard performance milestones once per shell run", () => {
const marks: string[] = [];
const performanceMarks = createDashboardPerformanceMarks({
mark: (name) => marks.push(name),
});
performanceMarks.markShellLoad();
performanceMarks.markFirstTileReady();
performanceMarks.markFirstTileReady();
performanceMarks.markVisibleTilesReady();
performanceMarks.markAllTilesSettled();
performanceMarks.markShellLoad();
performanceMarks.markFirstTileReady();
expect(marks).toEqual([
dashboardPerformanceMarks.shellLoad,
dashboardPerformanceMarks.firstTileReady,
dashboardPerformanceMarks.visibleTilesReady,
dashboardPerformanceMarks.allTilesSettled,
dashboardPerformanceMarks.shellLoad,
dashboardPerformanceMarks.firstTileReady,
]);
});
test("backs off failed tile keys and resets after success", () => {
const backoff = createDashboardTileBackoff();

View file

@ -157,6 +157,8 @@ export interface DashboardViewportHydrationQueueOptions<TItem> {
getModelId: (item: TItem) => string;
hydrate: (item: TItem) => Promise<void> | void;
items: TItem[];
onAllItemsSettled?: () => void;
onVisibleItemsSettled?: () => void;
signal: AbortSignal;
waitForIdle: () => Promise<void>;
}
@ -179,8 +181,13 @@ export async function runViewportAwareDashboardHydrationQueue<TItem>(
items: visible,
signal: options.signal,
});
if (options.signal.aborted) return;
options.onVisibleItemsSettled?.();
if (!deferred.length || options.signal.aborted) return;
if (!deferred.length) {
options.onAllItemsSettled?.();
return;
}
await options.waitForIdle();
if (options.signal.aborted) return;
@ -191,6 +198,7 @@ export async function runViewportAwareDashboardHydrationQueue<TItem>(
items: deferred,
signal: options.signal,
});
options.onAllItemsSettled?.();
}
export function splitDashboardHydrationItemsByVisibility<TItem>(options: {
@ -408,6 +416,51 @@ export function createDashboardRefreshDelay(
};
}
export interface DashboardPerformanceMarkOptions {
mark?: (name: string) => void;
}
export const dashboardPerformanceMarks = {
allTilesSettled: "dashboard:all-tiles-settled",
firstTileReady: "dashboard:first-tile-ready",
shellLoad: "dashboard:shell-load",
visibleTilesReady: "dashboard:visible-tiles-ready",
} as const;
export function createDashboardPerformanceMarks(
options: DashboardPerformanceMarkOptions = {},
) {
const mark = options.mark ||
globalThis.performance?.mark?.bind(globalThis.performance);
let firstTileReadyMarked = false;
function safeMark(name: string) {
try {
mark?.(name);
} catch {
// Performance marks are diagnostics only.
}
}
return {
markAllTilesSettled(): void {
safeMark(dashboardPerformanceMarks.allTilesSettled);
},
markFirstTileReady(): void {
if (firstTileReadyMarked) return;
firstTileReadyMarked = true;
safeMark(dashboardPerformanceMarks.firstTileReady);
},
markShellLoad(): void {
firstTileReadyMarked = false;
safeMark(dashboardPerformanceMarks.shellLoad);
},
markVisibleTilesReady(): void {
safeMark(dashboardPerformanceMarks.visibleTilesReady);
},
};
}
const dashboardTileBackoffDelaysMs = [15_000, 30_000, 60_000, 120_000];
export function createDashboardTileBackoff() {