perf(web): batch dashboard tile hydration #51

Merged
vince merged 2 commits from codex/dashboard-batch-tiles into main 2026-06-20 15:18:26 +02:00
7 changed files with 518 additions and 40 deletions
Showing only changes of commit de87464ef1 - Show all commits

View file

@ -52,6 +52,11 @@ type DashboardTileResponse =
message: string;
};
type DashboardTilesBatchResponse = {
state: "ready";
tiles: DashboardTileResponse[];
};
type DashboardTileHydrationResult = "aborted" | "failed" | "ready";
const dashboardTileHydrationConcurrency = 6;
@ -292,6 +297,7 @@ export default function App() {
const modelIds = tiles.map(dashboardTileModelId);
void runViewportAwareDashboardHydrationQueue({
batchSize: dashboardTileHydrationConcurrency,
collectVisibleModelIds: async () => {
await waitForDashboardRenderFrame(signal);
return collectVisibleDashboardModelIds({
@ -321,6 +327,23 @@ export default function App() {
tileBackoff.recordFailure(key);
}
},
hydrateBatch: async (batch) => {
const results = await hydrateDashboardTileBatch(
batch,
run,
signal,
snapshotContext,
);
for (const { result, tile } of results) {
const key = dashboardTileKey(tile);
if (result === "ready") {
tileBackoff.recordSuccess(key);
} else if (result === "failed") {
tileBackoff.recordFailure(key);
}
}
},
items: tiles,
onAllItemsSettled: () => performanceMarks.markAllTilesSettled(),
onVisibleItemsSettled: () => performanceMarks.markVisibleTilesReady(),
@ -335,6 +358,69 @@ export default function App() {
});
}
async function hydrateDashboardTileBatch(
tiles: DashboardTileReference[],
run: number,
signal: AbortSignal,
snapshotContext: DashboardTileSnapshotStoreContext,
): Promise<Array<{ result: DashboardTileHydrationResult; tile: DashboardTileReference }>> {
try {
const response = await fetch("/api/dashboard/tiles", {
body: JSON.stringify({ tiles }),
headers: { "Content-Type": "application/json" },
method: "POST",
signal,
});
const batchResponse = (await response.json()) as DashboardTilesBatchResponse;
if (
cancelled ||
signal.aborted ||
run !== hydrationRun ||
!response.ok ||
batchResponse.state !== "ready"
) {
throw new Error("Dashboard tile batch hydration failed");
}
const responsesByKey = new Map(
batchResponse.tiles.map((tileResponse) => [
dashboardTileKey(tileResponse.tile),
tileResponse,
]),
);
return tiles.map((tile) => {
const key = dashboardTileKey(tile);
try {
return {
tile,
result: applyDashboardTileHydrationResponse(
tile,
responsesByKey.get(key),
run,
signal,
snapshotContext,
),
};
} finally {
finishDashboardTileHydration(key, run, signal);
}
});
} catch (error) {
if (signal.aborted || cancelled || run !== hydrationRun) {
return tiles.map((tile) => ({ result: "aborted", tile }));
}
return Promise.all(
tiles.map(async (tile) => ({
tile,
result: await hydrateDashboardTile(tile, run, signal, snapshotContext),
})),
);
}
}
async function hydrateDashboardTile(
tile: DashboardTileReference,
run: number,
@ -347,32 +433,23 @@ export default function App() {
const response = await fetch(dashboardTileUrl(tile), { signal });
const tileResponse = (await response.json()) as DashboardTileResponse;
if (
cancelled ||
signal.aborted ||
run !== hydrationRun ||
!response.ok ||
tileResponse.state !== "ready"
) {
if (!response.ok) {
return "failed";
}
const result = applyDashboardTileHydrationResponse(
tile,
tileResponse,
run,
signal,
snapshotContext,
);
if (result !== "ready") {
return signal.aborted || cancelled || run !== hydrationRun
? "aborted"
: "failed";
}
const readyTileResponse = tileResponse;
setDashboard((current) =>
current?.state === "ready"
? {
...current,
document: applyDashboardTile(current.document, readyTileResponse),
}
: current,
);
tileSnapshotStore.saveReadyTile({
...snapshotContext,
response: readyTileResponse,
});
performanceMarks.markFirstTileReady();
return "ready";
} catch (error) {
if (!isAbortError(error) && !cancelled) {
@ -381,13 +458,58 @@ export default function App() {
}
return "aborted";
} finally {
if (!cancelled && !signal.aborted && run === hydrationRun) {
setHydratingItemIds((current) => {
const next = new Set(current);
next.delete(key);
return next;
});
}
finishDashboardTileHydration(key, run, signal);
}
}
function applyDashboardTileHydrationResponse(
tile: DashboardTileReference,
tileResponse: DashboardTileResponse | undefined,
run: number,
signal: AbortSignal,
snapshotContext: DashboardTileSnapshotStoreContext,
): DashboardTileHydrationResult {
if (
cancelled ||
signal.aborted ||
run !== hydrationRun ||
!tileResponse ||
dashboardTileKey(tileResponse.tile) !== dashboardTileKey(tile) ||
tileResponse.state !== "ready"
) {
return signal.aborted || cancelled || run !== hydrationRun
? "aborted"
: "failed";
}
const readyTileResponse = tileResponse;
setDashboard((current) =>
current?.state === "ready"
? {
...current,
document: applyDashboardTile(current.document, readyTileResponse),
}
: current,
);
tileSnapshotStore.saveReadyTile({
...snapshotContext,
response: readyTileResponse,
});
performanceMarks.markFirstTileReady();
return "ready";
}
function finishDashboardTileHydration(
key: string,
run: number,
signal: AbortSignal,
) {
if (!cancelled && !signal.aborted && run === hydrationRun) {
setHydratingItemIds((current) => {
const next = new Set(current);
next.delete(key);
return next;
});
}
}

View file

@ -9,6 +9,7 @@ import {
createDashboardTileBackoff,
createDashboardRequestAborter,
runDashboardHydrationQueue,
runDashboardHydrationBatchQueue,
runViewportAwareDashboardHydrationQueue,
shouldPauseDashboardRefresh,
splitDashboardHydrationItemsByVisibility,
@ -121,6 +122,25 @@ describe("dashboard refresh lifecycle", () => {
expect(maxActive).toBe(2);
});
test("hydrates queued work in fixed-size batches", async () => {
const batches: number[][] = [];
await runDashboardHydrationBatchQueue({
batchSize: 3,
hydrateBatch: (items) => {
batches.push(items);
},
items: [1, 2, 3, 4, 5, 6, 7],
signal: new AbortController().signal,
});
expect(batches).toEqual([
[1, 2, 3],
[4, 5, 6],
[7],
]);
});
test("hydrates visible items before deferred items and waits for idle", async () => {
const calls: string[] = [];
const idleReleases: Array<() => void> = [];

View file

@ -152,10 +152,12 @@ export async function runDashboardHydrationQueue<TItem>(
}
export interface DashboardViewportHydrationQueueOptions<TItem> {
batchSize?: number;
collectVisibleModelIds: () => Promise<ReadonlySet<string>>;
concurrency: number;
getModelId: (item: TItem) => string;
hydrate: (item: TItem) => Promise<void> | void;
hydrateBatch?: (items: TItem[]) => Promise<void> | void;
items: TItem[];
onAllItemsSettled?: () => void;
onVisibleItemsSettled?: () => void;
@ -175,12 +177,7 @@ export async function runViewportAwareDashboardHydrationQueue<TItem>(
visibleModelIds,
});
await runDashboardHydrationQueue({
concurrency: options.concurrency,
hydrate: options.hydrate,
items: visible,
signal: options.signal,
});
await runDashboardHydrationItems(options, visible);
if (options.signal.aborted) return;
options.onVisibleItemsSettled?.();
@ -192,13 +189,44 @@ export async function runViewportAwareDashboardHydrationQueue<TItem>(
await options.waitForIdle();
if (options.signal.aborted) return;
await runDashboardHydrationQueue({
concurrency: options.concurrency,
hydrate: options.hydrate,
items: deferred,
await runDashboardHydrationItems(options, deferred);
options.onAllItemsSettled?.();
}
async function runDashboardHydrationItems<TItem>(
options: DashboardViewportHydrationQueueOptions<TItem>,
items: TItem[],
): Promise<void> {
if (!options.hydrateBatch) {
await runDashboardHydrationQueue({
concurrency: options.concurrency,
hydrate: options.hydrate,
items,
signal: options.signal,
});
return;
}
await runDashboardHydrationBatchQueue({
batchSize: options.batchSize ?? options.concurrency,
hydrateBatch: options.hydrateBatch,
items,
signal: options.signal,
});
options.onAllItemsSettled?.();
}
export async function runDashboardHydrationBatchQueue<TItem>(options: {
batchSize: number;
hydrateBatch: (items: TItem[]) => Promise<void> | void;
items: TItem[];
signal: AbortSignal;
}): Promise<void> {
const batchSize = Math.max(1, Math.floor(options.batchSize));
for (let index = 0; index < options.items.length; index += batchSize) {
if (options.signal.aborted) return;
await options.hydrateBatch(options.items.slice(index, index + batchSize));
}
}
export function splitDashboardHydrationItemsByVisibility<TItem>(options: {

View file

@ -0,0 +1,53 @@
import { afterAll, describe, expect, test, vi } from "vitest";
import { handleRequest } from "./index";
describe("server request routing", () => {
const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => undefined);
afterAll(() => {
consoleInfo.mockRestore();
});
test("routes dashboard tile batch requests", async () => {
const response = await handleRequest(
new Request("https://example.test/api/dashboard/tiles", {
method: "POST",
body: JSON.stringify({
tiles: [
{
kind: "status",
stripId: "footer-status",
id: "auto-refresh",
},
],
}),
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
state: "ready",
tiles: [
{
state: "ready",
tile: {
kind: "status",
stripId: "footer-status",
id: "auto-refresh",
},
},
],
});
});
test("rejects non-post dashboard tile batch requests", async () => {
const response = await handleRequest(
new Request("https://example.test/api/dashboard/tiles", {
method: "GET",
}),
);
expect(response.status).toBe(405);
expect(response.headers.get("allow")).toBe("POST");
});
});

View file

@ -1,6 +1,10 @@
import { extname, normalize } from "node:path";
import { handleAgentDashboardRoute } from "./routes/agent-dashboard";
import { handleDashboardRoute, handleDashboardTileRoute } from "./routes/dashboard";
import {
handleDashboardRoute,
handleDashboardTileRoute,
handleDashboardTilesRoute,
} from "./routes/dashboard";
const host = process.env.HOST || "0.0.0.0";
const port = Number(process.env.PORT || 3000);
@ -23,6 +27,11 @@ export async function handleRequest(request: Request): Promise<Response> {
return handleDashboardRoute();
}
if (url.pathname === "/api/dashboard/tiles") {
if (request.method !== "POST") return methodNotAllowed(["POST"]);
return handleDashboardTilesRoute(request);
}
if (url.pathname.startsWith("/api/dashboard/tile/")) {
if (request.method !== "GET") return methodNotAllowed(["GET"]);
return handleDashboardTileRoute(url.pathname);

View file

@ -3,8 +3,10 @@ import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
import {
createDashboardTileCache,
dashboardTileCacheKey,
handleDashboardTilesRoute,
handleDashboardTileRoute,
loadDashboardResponse,
loadDashboardTilesResponse,
loadDashboardTileResponse,
type DashboardTileResolutionLogEvent,
} from "./dashboard";
@ -563,6 +565,115 @@ describe("dashboard API route", () => {
);
});
test("serves batch tile route responses", async () => {
const response = await handleDashboardTilesRoute(
new Request("https://example.test/api/dashboard/tiles", {
method: "POST",
body: JSON.stringify({
tiles: [
{
kind: "status",
stripId: "footer-status",
id: "auto-refresh",
},
],
}),
}),
{
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache: createDashboardTileCache(),
},
);
expect(response.status).toBe(200);
expect(response.headers.get("cache-control")).toBe(
"private, max-age=5, stale-while-revalidate=30",
);
await expect(response.json()).resolves.toMatchObject({
state: "ready",
tiles: [
{
state: "ready",
tile: {
kind: "status",
stripId: "footer-status",
id: "auto-refresh",
},
},
],
});
});
test("rejects invalid batch tile requests", async () => {
const response = await handleDashboardTilesRoute(
new Request("https://example.test/api/dashboard/tiles", {
method: "POST",
body: JSON.stringify({
tiles: [{ kind: "service", id: "missing-group" }],
}),
}),
);
expect(response.status).toBe(400);
});
test("resolves batch tile responses with server concurrency capped at six", async () => {
let active = 0;
let maxActive = 0;
const started: string[] = [];
const releases = new Map<string, () => void>();
const service = dimensionLabDashboardFixture.serviceGroups
.flatMap((group) => group.services)[0];
if (!service) throw new Error("missing service fixture");
const tiles = Array.from({ length: 7 }, (_, index) => ({
kind: "service" as const,
groupId: "essentials",
id: `service-${index}`,
}));
const batch = loadDashboardTilesResponse(tiles, {
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache: {
async resolve(key) {
active += 1;
maxActive = Math.max(maxActive, active);
started.push(key);
await new Promise<void>((resolve) => releases.set(key, resolve));
active -= 1;
return {
cache: "miss",
coalesced: false,
response: {
state: "ready",
tile: JSON.parse(key),
item: service,
},
};
},
},
});
await waitFor(() => started.length === 6);
expect(maxActive).toBe(6);
releases.get(started[0])?.();
await waitFor(() => started.length === 7);
for (const release of releases.values()) release();
await expect(batch).resolves.toMatchObject({
state: "ready",
tiles: expect.arrayContaining([
expect.objectContaining({
state: "ready",
tile: tiles[0],
}),
]),
});
expect(maxActive).toBe(6);
});
test("does not hydrate tile routes when live datasources are disabled", async () => {
const previous = process.env.DISABLE_LIVE_DATASOURCES;
process.env.DISABLE_LIVE_DATASOURCES = "1";
@ -598,6 +709,15 @@ function jsonResponse(payload: unknown): Response {
});
}
async function waitFor(predicate: () => boolean) {
for (let attempt = 0; attempt < 20; attempt += 1) {
if (predicate()) return;
await Promise.resolve();
}
throw new Error("condition was not met");
}
function telemetryFetch() {
return vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);

View file

@ -43,6 +43,8 @@ const dashboardTileResponseHeaders = {
"Cache-Control": "private, max-age=5, stale-while-revalidate=30",
};
const dashboardBatchTileConcurrency = 6;
const defaultDashboardTileCache = createDashboardTileCache();
interface DashboardTileCacheResult {
@ -60,6 +62,11 @@ export interface DashboardTileResolutionLogEvent {
tileKey: string;
}
export interface DashboardTilesBatchResponse {
state: "ready";
tiles: DashboardTileResolution[];
}
export function createDashboardTileCache(): DashboardTileCache {
const entries = new Map<string, DashboardTileCacheEntry>();
const inFlight = new Map<string, Promise<DashboardTileResolution>>();
@ -236,6 +243,16 @@ export async function loadDashboardTileResponse(
}
}
export async function loadDashboardTilesResponse(
tiles: DashboardTileReference[],
options: LoadDashboardResponseOptions = {},
): Promise<DashboardTilesBatchResponse> {
return {
state: "ready",
tiles: await resolveDashboardTilesBatch(tiles, options),
};
}
export async function handleDashboardRoute(): Promise<Response> {
return Response.json(await loadDashboardResponse());
}
@ -259,6 +276,23 @@ export async function handleDashboardTileRoute(
});
}
export async function handleDashboardTilesRoute(
request: Request,
options: LoadDashboardResponseOptions = {},
): Promise<Response> {
const tiles = await parseDashboardTilesBatchRequest(request);
if (!tiles) {
return Response.json(
{ ok: false, message: "Invalid dashboard tiles request" },
{ status: 400 },
);
}
return Response.json(await loadDashboardTilesResponse(tiles, options), {
headers: dashboardTileResponseHeaders,
});
}
export function dashboardTileCacheKey(tile: DashboardTileReference): string {
return JSON.stringify(tile);
}
@ -296,6 +330,98 @@ function dashboardTileErrorCategory(error: unknown): string {
return "unknown";
}
async function resolveDashboardTilesBatch(
tiles: DashboardTileReference[],
options: LoadDashboardResponseOptions,
): Promise<DashboardTileResolution[]> {
const results: DashboardTileResolution[] = new Array(tiles.length);
let nextIndex = 0;
async function worker() {
while (nextIndex < tiles.length) {
const index = nextIndex;
nextIndex += 1;
results[index] = await loadDashboardTileResponse(tiles[index], options);
}
}
await Promise.all(
Array.from(
{ length: Math.min(dashboardBatchTileConcurrency, tiles.length) },
() => worker(),
),
);
return results;
}
async function parseDashboardTilesBatchRequest(
request: Request,
): Promise<DashboardTileReference[] | null> {
try {
const body = await request.json() as unknown;
if (
typeof body !== "object" ||
body === null ||
!("tiles" in body) ||
!Array.isArray(body.tiles)
) {
return null;
}
const tiles = body.tiles.map(parseDashboardTileReference);
return tiles.every((tile): tile is DashboardTileReference => tile !== null)
? tiles
: null;
} catch {
return null;
}
}
function parseDashboardTileReference(value: unknown): DashboardTileReference | null {
if (typeof value !== "object" || value === null || !("kind" in value)) {
return null;
}
if (
(value.kind === "telemetry" || value.kind === "module") &&
"id" in value &&
typeof value.id === "string"
) {
return { kind: value.kind, id: value.id };
}
if (
value.kind === "service" &&
"groupId" in value &&
typeof value.groupId === "string" &&
"id" in value &&
typeof value.id === "string"
) {
return {
kind: "service",
groupId: value.groupId,
id: value.id,
};
}
if (
value.kind === "status" &&
"stripId" in value &&
typeof value.stripId === "string" &&
"id" in value &&
typeof value.id === "string"
) {
return {
kind: "status",
stripId: value.stripId,
id: value.id,
};
}
return null;
}
class DashboardTileCacheResolutionError extends Error {
readonly cache: "hit" | "miss";
readonly coalesced: boolean;