perf(web): add dashboard hydration observability
This commit is contained in:
parent
b6de67f081
commit
328f6e00bf
5 changed files with 339 additions and 22 deletions
|
|
@ -20,6 +20,7 @@ export interface LoadDashboardResponseOptions
|
|||
DatasourceResolutionOptions {
|
||||
disableLiveDatasources?: boolean;
|
||||
hydrateLiveDatasources?: boolean;
|
||||
logTileResolution?: (event: DashboardTileResolutionLogEvent) => void;
|
||||
now?: () => number;
|
||||
tileCache?: DashboardTileCache;
|
||||
}
|
||||
|
|
@ -35,7 +36,7 @@ export interface DashboardTileCache {
|
|||
ttlMs: number,
|
||||
now: number,
|
||||
load: () => Promise<DashboardTileResolution>,
|
||||
): Promise<DashboardTileResolution>;
|
||||
): Promise<DashboardTileCacheResult>;
|
||||
}
|
||||
|
||||
const dashboardTileResponseHeaders = {
|
||||
|
|
@ -44,6 +45,21 @@ const dashboardTileResponseHeaders = {
|
|||
|
||||
const defaultDashboardTileCache = createDashboardTileCache();
|
||||
|
||||
interface DashboardTileCacheResult {
|
||||
cache: "hit" | "miss";
|
||||
coalesced: boolean;
|
||||
response: DashboardTileResolution;
|
||||
}
|
||||
|
||||
export interface DashboardTileResolutionLogEvent {
|
||||
cache: "bypass" | "hit" | "miss";
|
||||
coalesced: boolean;
|
||||
durationMs: number;
|
||||
errorCategory?: string;
|
||||
status: DashboardTileResolution["state"] | "error";
|
||||
tileKey: string;
|
||||
}
|
||||
|
||||
export function createDashboardTileCache(): DashboardTileCache {
|
||||
const entries = new Map<string, DashboardTileCacheEntry>();
|
||||
const inFlight = new Map<string, Promise<DashboardTileResolution>>();
|
||||
|
|
@ -52,11 +68,21 @@ export function createDashboardTileCache(): DashboardTileCache {
|
|||
async resolve(key, ttlMs, now, load) {
|
||||
const cached = entries.get(key);
|
||||
if (cached && cached.expiresAt > now) {
|
||||
return cached.response;
|
||||
return {
|
||||
cache: "hit",
|
||||
coalesced: false,
|
||||
response: cached.response,
|
||||
};
|
||||
}
|
||||
|
||||
const active = inFlight.get(key);
|
||||
if (active) return active;
|
||||
if (active) {
|
||||
return active.then((response) => ({
|
||||
cache: "miss",
|
||||
coalesced: true,
|
||||
response,
|
||||
}));
|
||||
}
|
||||
|
||||
const request = load()
|
||||
.then((response) => {
|
||||
|
|
@ -74,7 +100,11 @@ export function createDashboardTileCache(): DashboardTileCache {
|
|||
});
|
||||
|
||||
inFlight.set(key, request);
|
||||
return request;
|
||||
return request.then((response) => ({
|
||||
cache: "miss",
|
||||
coalesced: false,
|
||||
response,
|
||||
}));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -125,13 +155,26 @@ export async function loadDashboardTileResponse(
|
|||
options.disableLiveDatasources ||
|
||||
process.env.DISABLE_LIVE_DATASOURCES === "1"
|
||||
) {
|
||||
return {
|
||||
const tileKey = dashboardTileCacheKey(tile);
|
||||
const startedAt = options.now?.() ?? Date.now();
|
||||
const response = {
|
||||
state: "disabled",
|
||||
tile,
|
||||
message: "Live datasource hydration is disabled.",
|
||||
};
|
||||
} satisfies DashboardTileResolution;
|
||||
logDashboardTileResolution(options, {
|
||||
cache: "bypass",
|
||||
coalesced: false,
|
||||
durationMs: elapsedDashboardTileMs(startedAt, options),
|
||||
status: response.state,
|
||||
tileKey,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
const tileKey = dashboardTileCacheKey(tile);
|
||||
const startedAt = options.now?.() ?? Date.now();
|
||||
|
||||
const dashboard = loadDashboardRuntime(undefined, {
|
||||
refreshSeedDocument: options.refreshSeedDocument ?? true,
|
||||
seedIfEmpty: options.seedIfEmpty ?? true,
|
||||
|
|
@ -139,22 +182,50 @@ export async function loadDashboardTileResponse(
|
|||
});
|
||||
|
||||
if (dashboard.state !== "ready") {
|
||||
return {
|
||||
const response = {
|
||||
state: "not_found",
|
||||
tile,
|
||||
message: `Dashboard is not ready: ${dashboard.state}`,
|
||||
};
|
||||
} satisfies DashboardTileResolution;
|
||||
logDashboardTileResolution(options, {
|
||||
cache: "bypass",
|
||||
coalesced: false,
|
||||
durationMs: elapsedDashboardTileMs(startedAt, options),
|
||||
status: response.state,
|
||||
tileKey,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
try {
|
||||
const result = await cache.resolve(
|
||||
tileKey,
|
||||
dashboardTileTtlMs(dashboard.document, tile),
|
||||
now,
|
||||
() => resolveDashboardTile(dashboard.document, tile, options),
|
||||
);
|
||||
logDashboardTileResolution(options, {
|
||||
cache: result.cache,
|
||||
coalesced: result.coalesced,
|
||||
durationMs: elapsedDashboardTileMs(startedAt, options),
|
||||
status: result.response.state,
|
||||
tileKey,
|
||||
});
|
||||
return result.response;
|
||||
} catch (error) {
|
||||
logDashboardTileResolution(options, {
|
||||
cache: "miss",
|
||||
coalesced: false,
|
||||
durationMs: elapsedDashboardTileMs(startedAt, options),
|
||||
errorCategory: dashboardTileErrorCategory(error),
|
||||
status: "error",
|
||||
tileKey,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleDashboardRoute(): Promise<Response> {
|
||||
|
|
@ -184,6 +255,39 @@ export function dashboardTileCacheKey(tile: DashboardTileReference): string {
|
|||
return JSON.stringify(tile);
|
||||
}
|
||||
|
||||
function logDashboardTileResolution(
|
||||
options: LoadDashboardResponseOptions,
|
||||
event: DashboardTileResolutionLogEvent,
|
||||
): void {
|
||||
if (options.logTileResolution) {
|
||||
options.logTileResolution(event);
|
||||
return;
|
||||
}
|
||||
|
||||
console.info("dashboard.tile", event);
|
||||
}
|
||||
|
||||
function elapsedDashboardTileMs(
|
||||
startedAt: number,
|
||||
options: LoadDashboardResponseOptions,
|
||||
): number {
|
||||
const now = options.now?.() ?? Date.now();
|
||||
return Math.max(0, now - startedAt);
|
||||
}
|
||||
|
||||
function dashboardTileErrorCategory(error: unknown): string {
|
||||
if (
|
||||
typeof error === "object" &&
|
||||
error !== null &&
|
||||
"name" in error &&
|
||||
typeof error.name === "string"
|
||||
) {
|
||||
return error.name;
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function dashboardTileTtlMs(
|
||||
document: DashboardDocument,
|
||||
tile: DashboardTileReference,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue