From bc1f64d5661abb522004bc1faabf4932fbbffe66 Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 14:03:24 +0200 Subject: [PATCH] perf(web): cache dashboard tile responses --- apps/web/src/server/routes/dashboard.test.ts | 285 ++++++++++++++++++- apps/web/src/server/routes/dashboard.ts | 105 ++++++- 2 files changed, 386 insertions(+), 4 deletions(-) diff --git a/apps/web/src/server/routes/dashboard.test.ts b/apps/web/src/server/routes/dashboard.test.ts index 095ee48..9b11079 100644 --- a/apps/web/src/server/routes/dashboard.test.ts +++ b/apps/web/src/server/routes/dashboard.test.ts @@ -1,6 +1,12 @@ import { describe, expect, test, vi } from "vitest"; import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab"; -import { loadDashboardResponse, loadDashboardTileResponse } from "./dashboard"; +import { + createDashboardTileCache, + dashboardTileCacheKey, + handleDashboardTileRoute, + loadDashboardResponse, + loadDashboardTileResponse, +} from "./dashboard"; describe("dashboard API route", () => { 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((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 () => { const previous = process.env.DISABLE_LIVE_DATASOURCES; process.env.DISABLE_LIVE_DATASOURCES = "1"; @@ -179,3 +422,43 @@ function jsonResponse(payload: unknown): Response { 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}`); + }); +} diff --git a/apps/web/src/server/routes/dashboard.ts b/apps/web/src/server/routes/dashboard.ts index fd0196e..ce9c4e6 100644 --- a/apps/web/src/server/routes/dashboard.ts +++ b/apps/web/src/server/routes/dashboard.ts @@ -1,3 +1,4 @@ +import type { DashboardDocument } from "@dimensionlab/dashboard-model"; import { loadDashboardRuntime, type DashboardRuntimeOptions, @@ -19,6 +20,63 @@ export interface LoadDashboardResponseOptions DatasourceResolutionOptions { disableLiveDatasources?: 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, + ): Promise; +} + +const dashboardTileResponseHeaders = { + "Cache-Control": "private, max-age=5, stale-while-revalidate=30", +}; + +const defaultDashboardTileCache = createDashboardTileCache(); + +export function createDashboardTileCache(): DashboardTileCache { + const entries = new Map(); + const inFlight = new Map>(); + + 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( @@ -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 { return Response.json(await loadDashboardResponse()); } -export async function handleDashboardTileRoute(pathname: string): Promise { +export async function handleDashboardTileRoute( + pathname: string, + options: LoadDashboardResponseOptions = {}, +): Promise { const tile = parseDashboardTilePath(pathname); if (!tile) { return Response.json( @@ -104,12 +173,42 @@ export async function handleDashboardTileRoute(pathname: string): Promise 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 { const parts = pathname.split("/").filter(Boolean); const [, dashboard, tileRoot, kind, firstId, secondId] = parts;