perf(web): cache dashboard tile responses #46

Merged
vince merged 1 commit from codex/dashboard-tile-cache into main 2026-06-20 14:09:57 +02:00
2 changed files with 386 additions and 4 deletions
Showing only changes of commit bc1f64d566 - Show all commits

View file

@ -1,6 +1,12 @@
import { describe, expect, test, vi } from "vitest"; import { describe, expect, test, vi } from "vitest";
import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab"; import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
import { loadDashboardResponse, loadDashboardTileResponse } from "./dashboard"; import {
createDashboardTileCache,
dashboardTileCacheKey,
handleDashboardTileRoute,
loadDashboardResponse,
loadDashboardTileResponse,
} from "./dashboard";
describe("dashboard API route", () => { describe("dashboard API route", () => {
test("returns the ready dashboard shell without hydrating live datasources", async () => { test("returns the ready dashboard shell without hydrating live datasources", async () => {
@ -145,6 +151,243 @@ describe("dashboard API route", () => {
}); });
}); });
test("caches ready tile responses until the tile ttl expires", async () => {
const cache = createDashboardTileCache();
let now = 1_000;
const fetch = vi.fn(async () =>
jsonResponse({
status: "UP",
ping: 42,
}),
);
const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;
const first = await loadDashboardTileResponse(tile, {
fetch,
now: () => now,
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache: cache,
});
const second = await loadDashboardTileResponse(tile, {
fetch,
now: () => now + 29_999,
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache: cache,
});
now += 30_001;
const third = await loadDashboardTileResponse(tile, {
fetch,
now: () => now,
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache: cache,
});
expect(first).toEqual(second);
expect(third.state).toBe("ready");
expect(fetch).toHaveBeenCalledTimes(2);
});
test("keeps telemetry tiles cached for fifteen seconds", async () => {
const cache = createDashboardTileCache();
let now = 1_000;
const fetch = telemetryFetch();
const tile = { kind: "telemetry", id: "infra-ram" } as const;
await loadDashboardTileResponse(tile, {
fetch,
now: () => now,
prometheusBaseUrl: "https://prometheus.example",
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache: cache,
});
await loadDashboardTileResponse(tile, {
fetch,
now: () => now + 14_999,
prometheusBaseUrl: "https://prometheus.example",
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache: cache,
});
now += 15_001;
await loadDashboardTileResponse(tile, {
fetch,
now: () => now,
prometheusBaseUrl: "https://prometheus.example",
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache: cache,
});
expect(fetch).toHaveBeenCalledTimes(4);
});
test("keeps weather module tiles cached for ten minutes", async () => {
const cache = createDashboardTileCache();
let now = 1_000;
const fetch = vi.fn(async () =>
jsonResponse({
current: {
apparent_temperature: 19,
temperature_2m: 20,
weather_code: 0,
wind_speed_10m: 11,
},
}),
);
const tile = { kind: "module", id: "weather-amsterdam" } as const;
await loadDashboardTileResponse(tile, {
fetch,
now: () => now,
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache: cache,
});
await loadDashboardTileResponse(tile, {
fetch,
now: () => now + 599_999,
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache: cache,
});
now += 600_001;
await loadDashboardTileResponse(tile, {
fetch,
now: () => now,
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache: cache,
});
expect(fetch).toHaveBeenCalledTimes(2);
});
test("coalesces concurrent tile requests for the same cache key", async () => {
const cache = createDashboardTileCache();
let resolveFetch: ((response: Response) => void) | undefined;
const fetch = vi.fn(() =>
new Promise<Response>((resolve) => {
resolveFetch = resolve;
})
);
const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;
const first = loadDashboardTileResponse(tile, {
fetch,
now: () => 1_000,
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache: cache,
});
const second = loadDashboardTileResponse(tile, {
fetch,
now: () => 1_000,
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache: cache,
});
await Promise.resolve();
expect(fetch).toHaveBeenCalledTimes(1);
resolveFetch?.(jsonResponse({ status: "UP", ping: 42 }));
expect(await first).toEqual(await second);
});
test("uses structured tile cache keys when identifiers contain delimiters", () => {
expect(
dashboardTileCacheKey({ kind: "service", groupId: "a:b", id: "c" }),
).not.toBe(
dashboardTileCacheKey({ kind: "service", groupId: "a", id: "b:c" }),
);
expect(
dashboardTileCacheKey({ kind: "status", stripId: "a:b", id: "c" }),
).not.toBe(
dashboardTileCacheKey({ kind: "status", stripId: "a", id: "b:c" }),
);
});
test("uses thirty-second status aggregate and five-minute static status ttl buckets", async () => {
const cache = createDashboardTileCache();
const fetch = vi.fn(async () => jsonResponse({ status: "UP", ping: 42 }));
const serviceCheckCount = dimensionLabDashboardFixture.serviceGroups
.flatMap((group) => group.services).length;
await loadDashboardTileResponse(
{ kind: "status", stripId: "footer-status", id: "system-status" },
{
fetch,
now: () => 1_000,
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache: cache,
},
);
await loadDashboardTileResponse(
{ kind: "status", stripId: "footer-status", id: "system-status" },
{
fetch,
now: () => 31_001,
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache: cache,
},
);
await loadDashboardTileResponse(
{ kind: "status", stripId: "footer-status", id: "auto-refresh" },
{
fetch,
now: () => 1_000,
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache: cache,
},
);
await loadDashboardTileResponse(
{ kind: "status", stripId: "footer-status", id: "auto-refresh" },
{
fetch,
now: () => 300_999,
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache: cache,
},
);
await loadDashboardTileResponse(
{ kind: "status", stripId: "footer-status", id: "auto-refresh" },
{
fetch,
now: () => 301_001,
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache: cache,
},
);
expect(fetch).toHaveBeenCalledTimes(serviceCheckCount * 2);
});
test("serves tile route responses with short private cache headers", async () => {
const response = await handleDashboardTileRoute(
"/api/dashboard/tile/service/essentials/vaultwarden",
{
fetch: vi.fn(async () => jsonResponse({ status: "UP", ping: 42 })),
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache: createDashboardTileCache(),
},
);
expect(response.headers.get("cache-control")).toBe(
"private, max-age=5, stale-while-revalidate=30",
);
});
test("does not hydrate tile routes when live datasources are disabled", async () => { test("does not hydrate tile routes when live datasources are disabled", async () => {
const previous = process.env.DISABLE_LIVE_DATASOURCES; const previous = process.env.DISABLE_LIVE_DATASOURCES;
process.env.DISABLE_LIVE_DATASOURCES = "1"; process.env.DISABLE_LIVE_DATASOURCES = "1";
@ -179,3 +422,43 @@ function jsonResponse(payload: unknown): Response {
headers: { "content-type": "application/json" }, headers: { "content-type": "application/json" },
}); });
} }
function telemetryFetch() {
return vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
if (url.startsWith("https://prometheus.example/api/v1/query_range")) {
return jsonResponse({
status: "success",
data: {
result: [
{
metric: { host: "linux-infra" },
values: [
[1771430000, "10"],
[1771430060, "20"],
[1771430120, "42"],
],
},
],
},
});
}
if (url.startsWith("https://prometheus.example/api/v1/query")) {
return jsonResponse({
status: "success",
data: {
result: [
{
metric: { host: "linux-infra" },
value: [1771430400, "42"],
},
],
},
});
}
throw new Error(`Unhandled test request: ${url}`);
});
}

View file

@ -1,3 +1,4 @@
import type { DashboardDocument } from "@dimensionlab/dashboard-model";
import { import {
loadDashboardRuntime, loadDashboardRuntime,
type DashboardRuntimeOptions, type DashboardRuntimeOptions,
@ -19,6 +20,63 @@ export interface LoadDashboardResponseOptions
DatasourceResolutionOptions { DatasourceResolutionOptions {
disableLiveDatasources?: boolean; disableLiveDatasources?: boolean;
hydrateLiveDatasources?: boolean; hydrateLiveDatasources?: boolean;
now?: () => number;
tileCache?: DashboardTileCache;
}
interface DashboardTileCacheEntry {
expiresAt: number;
response: DashboardTileResolution;
}
export interface DashboardTileCache {
resolve(
key: string,
ttlMs: number,
now: number,
load: () => Promise<DashboardTileResolution>,
): Promise<DashboardTileResolution>;
}
const dashboardTileResponseHeaders = {
"Cache-Control": "private, max-age=5, stale-while-revalidate=30",
};
const defaultDashboardTileCache = createDashboardTileCache();
export function createDashboardTileCache(): DashboardTileCache {
const entries = new Map<string, DashboardTileCacheEntry>();
const inFlight = new Map<string, Promise<DashboardTileResolution>>();
return {
async resolve(key, ttlMs, now, load) {
const cached = entries.get(key);
if (cached && cached.expiresAt > now) {
return cached.response;
}
const active = inFlight.get(key);
if (active) return active;
const request = load()
.then((response) => {
if (response.state === "ready") {
entries.set(key, {
expiresAt: now + ttlMs,
response,
});
}
return response;
})
.finally(() => {
inFlight.delete(key);
});
inFlight.set(key, request);
return request;
},
};
} }
export async function loadDashboardResponse( export async function loadDashboardResponse(
@ -88,14 +146,25 @@ export async function loadDashboardTileResponse(
}; };
} }
return resolveDashboardTile(dashboard.document, tile, options); const cache = options.tileCache || defaultDashboardTileCache;
const now = options.now?.() ?? Date.now();
return cache.resolve(
dashboardTileCacheKey(tile),
dashboardTileTtlMs(dashboard.document, tile),
now,
() => resolveDashboardTile(dashboard.document, tile, options),
);
} }
export async function handleDashboardRoute(): Promise<Response> { export async function handleDashboardRoute(): Promise<Response> {
return Response.json(await loadDashboardResponse()); return Response.json(await loadDashboardResponse());
} }
export async function handleDashboardTileRoute(pathname: string): Promise<Response> { export async function handleDashboardTileRoute(
pathname: string,
options: LoadDashboardResponseOptions = {},
): Promise<Response> {
const tile = parseDashboardTilePath(pathname); const tile = parseDashboardTilePath(pathname);
if (!tile) { if (!tile) {
return Response.json( return Response.json(
@ -104,12 +173,42 @@ export async function handleDashboardTileRoute(pathname: string): Promise<Respon
); );
} }
const response = await loadDashboardTileResponse(tile); const response = await loadDashboardTileResponse(tile, options);
return Response.json(response, { return Response.json(response, {
status: response.state === "not_found" ? 404 : 200, status: response.state === "not_found" ? 404 : 200,
headers: dashboardTileResponseHeaders,
}); });
} }
export function dashboardTileCacheKey(tile: DashboardTileReference): string {
return JSON.stringify(tile);
}
function dashboardTileTtlMs(
document: DashboardDocument,
tile: DashboardTileReference,
): number {
if (tile.kind === "telemetry") return 15_000;
if (tile.kind === "service") return 30_000;
if (tile.kind === "module") {
const module = document.modules?.find((item) => item.id === tile.id);
if (
module?.datasource?.type === "external" &&
module.datasource.adapter === "weather"
) {
return 10 * 60_000;
}
return 30_000;
}
if (["system-status", "uptime", "load-avg"].includes(tile.id)) {
return 30_000;
}
return 5 * 60_000;
}
function parseDashboardTilePath(pathname: string): DashboardTileReference | null { function parseDashboardTilePath(pathname: string): DashboardTileReference | null {
const parts = pathname.split("/").filter(Boolean); const parts = pathname.split("/").filter(Boolean);
const [, dashboard, tileRoot, kind, firstId, secondId] = parts; const [, dashboard, tileRoot, kind, firstId, secondId] = parts;