/** * Trends one scale-invariant index of benchmark performance per processor on * main. Each index changes by the geometric mean of the selected benchmark * changes between consecutive runs on that processor. The all benchmarks tile * selects every product benchmark. The key benchmarks tile selects * `topic board/journey` and `topic board scale/100`. Each selected benchmark * carries the same weight regardless of size. `deno bench` samples each benchmark * to a fixed time budget, so a performance change moves the per-operation times * without materially changing the run's wall-clock time. * * Each processor has its own colored line, and the headline shows the largest * established trend among processors measured in the last twelve hours. * Orange means at least one of those processors trends up. Green means every * eligible established processor stays flat or falls. Red means the most recent * run failed, or finished successfully without readable benchmark data. * A tile in the failed state reads `failed (was )` in its headline when * cached measurements are available, including measurements older than twelve * hours. Without measurements it reads `failed`. It drops its benchmark count * and window span and names the failure in their place: how long ago the * benchmarks last worked, and how many runs have failed since. * A run under way puts a "running" badge in the header. * * A benchmark added or removed is absent from one side of an adjacent * comparison, so it does not move the index. A processor change starts another * line instead of connecting unlike machines. A processor model is not a * machine, though: the runner group hands a run whatever share of a shared host * the other tenants leave it, and hosts under one model have measured a fifth * apart on work that touches no repository code. So each step also divides out * the machine, read from the calibration benchmarks the same run carries, and a * run that landed on a busy host reads as the busy host it was. The index takes * one run per BENCH_TREND_BUCKET_MS, matching the benchmarks.yml cadence, so a * re-run or a manual dispatch does not put two samples of one moment into the * fit. Each line's recent window contains the runs in the last * BENCH_TREND_MAX_AGE_DAYS or the newest BENCH_TREND_MIN_RUNS, whichever is * larger, and the full line spans about 45 days. * * A red run is read like any other: `deno bench` exits non-zero when one * benchmark throws, having written a complete report of the rest, so a run's * color says nothing about whether it measured anything. What decides that is * the artifact, which has to parse, name a processor, and carry benchmarks the * tile trends. The failed state reads the workflow-run list and the latest * run's cached result, so it is unaffected by which runs the trend samples, and * it works when artifacts cannot be read. Without usable data, the dashboard * keeps the last completed color and values while a fetch runs, and reads * "benchmark data unavailable" after an empty fetch. A failed fetch keeps the * last-known processor lines gray and names the reason. * * Every collection pages the run list, once a minute, which is the cadence the * run state needs. The artifact history behind the tile moves with the runs * instead: a collection whose sampled runs match the last refresh downloads * nothing. * * The /bench drill-down keeps the deeper picture, and closes with a link that * hands a rerun to GitHub: this token reads, so GitHub is where a run starts. * The benchmarks.yml job runs `deno bench --json` and uploads the output as a * `bench-results` artifact with 90-day retention, and there is no committed * history. The drill-down lists recent main runs and keeps one artifact per * shortest-view time bucket. It unzips each artifact in the process and reads * every benchmark's timings and processor identity. Results for a run attempt * are immutable and persisted, so later collections fetch only new runs and * attempts. The drill-down overlays one colored line per processor for each * benchmark. The CI duration and Gantt views also live behind /bench, and this * tile's collection keeps their history warm. */ import { maxOf, minOf } from "@commonfabric/utils/math"; import type { Ctx, Route, Status, Tile, TileView } from "../types.ts"; import { isCalibrationKey, isKeyBenchmark, parseBenchmarkReport, } from "../bench-report.ts"; import { BenchmarkHistoryStore, type BenchmarkRefreshResult, type BenchmarkStats, type CachedBenchmarkRun, } from "../benchmark-history-cache.ts"; import { CI_HISTORY_DAYS, CI_HISTORY_MIN_DAYS, CI_HISTORY_POINT_TARGET, ciHistoryBucketMs, ciHistoryDays, ciHistorySource, type CiHistorySourceKey, ciJobHistoryCheckResponse, ciJobHistoryProgressResponse, ciJobHistoryResponse, } from "../ci-job-history.ts"; import { RunLists } from "../github-runs.ts"; import { dashboardCacheFile } from "../history-files.ts"; import { concDot, dashboardGitHubCredential, durationTag, escapeHtml, friendlyError, github, githubDownload, type GitHubDownload, type GitHubJson, humanSpan, jsonFromZip, multiSparkline, performanceGithub, performanceGithubDownload, runArtifactId, } from "../lib.ts"; import type { GitHubCredential } from "../github-auth.ts"; import { BENCH_HEADLINE_MAX_AGE_HOURS, BENCH_TREND_BUCKET_MS, BENCH_TREND_MAX_AGE_DAYS, BENCH_TREND_MIN_RUNS, REPO, } from "../config.ts"; import { PERFORMANCE_CHECK_MS, PERFORMANCE_HISTORY_SCALE_MIN_VALUES, PERFORMANCE_HISTORY_SCALE_TRIM, PERFORMANCE_VIEW_STYLES, performanceViewHref, performanceViewNav, } from "../performance-views.ts"; import { distinctTrendDays, trendPct, trendPctLabel, trendStatus, } from "../trend.ts"; import { ciGanttPage } from "./ci-duration.ts"; import { DASHBOARD_THEME_CLIENT, DASHBOARD_THEME_HEAD, dashboardThemeToggle, themedChartSeries, } from "../theme.ts"; export { trendPct, trendStatus } from "../trend.ts"; export function benchmarkTrend( times: number[], values: number[], ): { pct: number; status: Status; label: string } { if (distinctTrendDays(times, values) < 7) { return { pct: 0, status: "unknown", label: "new" }; } const pct = trendPct(times, values); return { pct, status: trendStatus(pct), label: trendPctLabel(pct) }; } const WORKFLOW = "benchmarks.yml"; const ARTIFACT = "bench-results"; const SPARK_DAYS = CI_HISTORY_DAYS; const COLLECTION_BUCKET_MS = ciHistoryBucketMs(CI_HISTORY_MIN_DAYS); const BENCHMARK_REFRESH_MS = 30 * 60_000; const BENCHMARK_FETCH_CONCURRENCY = 8; interface BenchmarkGitHub extends GitHubJson { download(path: string, credential: GitHubCredential): Promise; /** The file in the cache directory that keeps the runs this client read. */ runLists: string; } const ordinaryBenchmarkGitHub: BenchmarkGitHub = { json: github, download: githubDownload, runLists: "fabric-wall-run-lists-benchmarks.json", }; const performanceBenchmarkGitHub: BenchmarkGitHub = { json: performanceGithub, download: performanceGithubDownload, runLists: "fabric-wall-run-lists-benchmarks-drill-down.json", }; // deno bench reports these seven timings per benchmark (all nanoseconds). type Stats = BenchmarkStats; // Shown as a ladder from the fastest sample to the slowest. Each label names // the timing it plots: six percentiles and the mean. const STATS: { label: string; field: keyof Stats }[] = [ { label: "p0", field: "min" }, { label: "mean", field: "avg" }, { label: "p75", field: "p75" }, { label: "p99", field: "p99" }, { label: "p99.5", field: "p995" }, { label: "p99.9", field: "p999" }, { label: "p100", field: "max" }, ]; const DEFAULT_LABEL = "p99"; interface Run { id: number; run_attempt?: number; status?: string; created_at: string; conclusion: string | null; } const benchmarkStore = new BenchmarkHistoryStore(); // Assembled by collect() for the /bench drill-down: each benchmark key with its // processor-specific timings over the covered days (oldest -> newest). interface BenchmarkCpuSeries { cpu: string; points: { runId: number; runAttempt: number; at: number; stats: Stats; }[]; } interface BenchmarkSeries { key: string; cpus: BenchmarkCpuSeries[]; } let snapshot: BenchmarkSeries[] = []; // The benchmarks.yml runs, as far down as the window reaches, for each GitHub // client that reads them: the tile's, and the drill-down's, which reads on the // performance views' rate budget. Each keeps its own, so that neither waits // behind the other's reading. let benchmarkRuns = new WeakMap(); // Whether those runs are kept in the cache directory as well as in memory. let benchmarkRunsSaved = true; // The last benchmarks.yml run list a collection paged. The drill-down reads it // to name the run its rerun hand-off points at. Only a fetch that worked // replaces it, so the hand-off keeps naming the failed run while a later fetch // is in flight or has failed. The tile itself reads the list its own // collection fetched, never this one. let latestBenchmarkRuns: Run[] | undefined; // How many run list reads have started, and which of them `latestBenchmarkRuns` // came from, so that a read that finishes after a later one does not replace // what the later one found. let benchmarkReadsStarted = 0; let benchmarkReadPublished = 0; export type BenchmarkFetchPhase = | "discovering" | "fetching" | "saving" | "complete" | "error"; export interface BenchmarkFetchProgress { id: string; phase: BenchmarkFetchPhase; totalRuns: number; cachedRuns: number; requestsMade: number; responsesReceived: number; successfulResponses: number; failedResponses: number; completedRuns: number; queuedRuns: number; outstandingRequests: number; needsReload: boolean; updatedAt: number; error?: string; } interface BenchmarkProgressRecord { state: BenchmarkFetchProgress; listeners: Set<(progress: BenchmarkFetchProgress) => void>; baselines: Set; } interface BenchmarkRefresh { progress: BenchmarkFetchProgress | null; result: Promise; } type BenchmarkRefreshScope = "bench" | "dashboard"; const activeBenchmarkRefreshes = new Map; }>(); let benchmarkCollectionTail: Promise = Promise.resolve(); let benchmarkProgressSequence = 0; let benchmarkRefreshedAt = 0; let benchmarkRefreshFailedAt = 0; let benchmarkRefreshError = ""; const benchmarkProgressById = new Map(); function benchmarkVersion(value: BenchmarkSeries[]): string { const serialized = JSON.stringify(value); let hash = 0x811c9dc5; for (let index = 0; index < serialized.length; index++) { hash ^= serialized.charCodeAt(index); hash = Math.imul(hash, 0x01000193); } return (hash >>> 0).toString(36); } export function benchmarkSnapshotVersion(): string { return benchmarkVersion(snapshot); } function newBenchmarkProgress(baseline?: string): BenchmarkProgressRecord { for (const [id, record] of benchmarkProgressById) { if (record.state.phase === "complete" || record.state.phase === "error") { benchmarkProgressById.delete(id); } } const now = Date.now(); const record: BenchmarkProgressRecord = { state: { id: `runtime-${now.toString(36)}-${++benchmarkProgressSequence}`, phase: "discovering", totalRuns: 0, cachedRuns: 0, requestsMade: 0, responsesReceived: 0, successfulResponses: 0, failedResponses: 0, completedRuns: 0, queuedRuns: 0, outstandingRequests: 0, needsReload: false, updatedAt: now, }, listeners: new Set(), baselines: new Set(baseline === undefined ? [] : [baseline]), }; benchmarkProgressById.set(record.state.id, record); return record; } function updateBenchmarkProgress( record: BenchmarkProgressRecord, update: Partial, ): void { Object.assign(record.state, update); record.state.completedRuns = Math.min( record.state.totalRuns, record.state.cachedRuns + record.state.responsesReceived, ); record.state.queuedRuns = Math.max( 0, record.state.totalRuns - record.state.cachedRuns - record.state.requestsMade, ); record.state.outstandingRequests = Math.max( 0, record.state.requestsMade - record.state.responsesReceived, ); record.state.updatedAt = Date.now(); const value = { ...record.state }; for (const listener of record.listeners) { try { listener(value); } catch { record.listeners.delete(listener); } } } function benchmarkProgress(id: string): BenchmarkFetchProgress | null { const record = benchmarkProgressById.get(id); return record ? { ...record.state } : null; } export function subscribeBenchmarkProgress( id: string, listener: (progress: BenchmarkFetchProgress) => void, ): (() => void) | null { const record = benchmarkProgressById.get(id); if (!record) return null; record.listeners.add(listener); try { listener({ ...record.state }); } catch { record.listeners.delete(listener); return null; } return () => record.listeners.delete(listener); } // How many of a run's product benchmarks match the selection. Zero means the // run measured nothing the tile trends, whatever else its artifact holds. const productMetricCount = ( run: { metrics: Map }, select: (key: string) => boolean = () => true, ): number => { let count = 0; for (const key of run.metrics.keys()) { if (!isCalibrationKey(key) && select(key)) count++; } return count; }; const UNKNOWN_CPU = "Unknown CPU"; // Wall-clock span of a series (first to last point), in milliseconds. const spanMs = (points: { at: number }[]): number => points.length < 2 ? 0 : points[points.length - 1].at - points[0].at; // Nanoseconds to a short human string. export function formatNs(ns: number): string { if (!Number.isFinite(ns)) return "—"; if (ns < 1e3) return `${Math.round(ns)}ns`; if (ns < 1e6) return `${(ns / 1e3).toFixed(ns < 1e4 ? 1 : 0)}µs`; if (ns < 1e9) return `${(ns / 1e6).toFixed(ns < 1e7 ? 1 : 0)}ms`; return `${(ns / 1e9).toFixed(2)}s`; } async function fetchZip( artifactId: number, credential: GitHubCredential, github: BenchmarkGitHub, ): Promise> { // GitHub 302s to a pre-signed blob URL; fetch follows it and drops the // Authorization header on the cross-origin hop, which the signed URL expects. const res = await github.download( `repos/${REPO}/actions/artifacts/${artifactId}/zip`, credential, ); if (!res.ok) throw new Error(`artifact ${artifactId}: HTTP ${res.status}`); return res.body; } // The benchmarks.yml runs on main, newest first, back to the first run created // before `cutoff`. The workflow runs to a four-hourly schedule and on manual // dispatch, and both kinds of run land on main. async function pageBenchmarkRuns( github: BenchmarkGitHub, credential: GitHubCredential, cutoff: number, ): Promise { const read = ++benchmarkReadsStarted; let lists = benchmarkRuns.get(github); if (!lists) { lists = new RunLists( benchmarkRunsSaved ? dashboardCacheFile(github.runLists) : undefined, ); benchmarkRuns.set(github, lists); } const runs = await lists.runs( (path, options) => github.json(path, credential, options), REPO, WORKFLOW, { reader: "benchmarks", wants: (run) => run.head_branch === "main", recheck: [{ branch: "main" }], until: (run) => Date.parse(run.created_at) < cutoff, }, ); if (read > benchmarkReadPublished) { latestBenchmarkRuns = runs; benchmarkReadPublished = read; } return runs; } // Populate and persist one run. A response that establishes there is no usable // artifact is cached as an empty map. A failed read remains unknown. async function loadRun( run: Run, credential: GitHubCredential, github: BenchmarkGitHub, ): Promise<{ cached: boolean; error?: unknown }> { let cpu = UNKNOWN_CPU; let metrics = new Map(); let zip: Uint8Array | undefined; try { const artifactId = await runArtifactId({ github, runId: run.id, name: ARTIFACT, credential, }); if (artifactId !== undefined) { zip = await fetchZip(artifactId, credential, github); } } catch (error) { // The read failed, so whether this run has usable results is still unknown. // Caching the empty map here would answer that question with "no" and never ask // again: the run is only ever fetched once, so a single blip would drop the run // from the trend for the life of the process. Record nothing and retry on the // next refresh. return { cached: false, error }; } if (zip !== undefined) { try { const json = await jsonFromZip(zip); const report = json === null ? undefined : parseBenchmarkReport(json); if (report?.cpu !== undefined) { cpu = report.cpu; metrics = report.metrics; } } catch { // Reading bytes already in hand is deterministic, and a run attempt's // artifact never changes, so an artifact that will not parse now will not // parse later either. That is the same answer as a run with no artifact // at all, and it is recorded the same way: an empty map, which reads as // no benchmark data. Reporting it instead would gray the whole tile on // one bad artifact and hold back the refresh marker for as long as that // artifact stayed in the window. } } benchmarkStore.set({ runId: run.id, runAttempt: run.run_attempt ?? 1, at: Date.parse(run.created_at), cpu, metrics, }); await benchmarkStore.save(); return { cached: true }; } function currentBenchmarkRun(run: Run): CachedBenchmarkRun | undefined { const cached = benchmarkStore.get(run.id); return cached && cached.runAttempt >= (run.run_attempt ?? 1) ? cached : undefined; } function currentRunMetrics(run: Run): Map | undefined { const cached = currentBenchmarkRun(run); return cached?.cpu === undefined ? undefined : cached.metrics; } function assembleBenchmarkSeries( runs: CachedBenchmarkRun[], ): BenchmarkSeries[] { const byKey = new Map< string, Map< string, { runId: number; runAttempt: number; at: number; stats: Stats; }[] > >(); for (const run of [...runs].sort((a, b) => a.at - b.at)) { if (run.cpu === undefined) continue; for (const [key, stats] of run.metrics) { if (isCalibrationKey(key)) continue; let byCpu = byKey.get(key); if (!byCpu) { byCpu = new Map(); byKey.set(key, byCpu); } let points = byCpu.get(run.cpu); if (!points) { points = []; byCpu.set(run.cpu, points); } points.push({ runId: run.runId, runAttempt: run.runAttempt, at: run.at, stats, }); } } return [...byKey] .map(([key, byCpu]) => ({ key, cpus: [...byCpu] .map(([cpu, points]) => ({ cpu, points })) .filter((series) => series.points.length > 0) .sort((a, b) => a.cpu.localeCompare(b.cpu)), })) .filter((series) => series.cpus.length > 0) .sort((a, b) => a.key.localeCompare(b.key)); } function assembleBenchmarkSnapshot(runs: Run[]): BenchmarkSeries[] { return assembleBenchmarkSeries( runs.flatMap((run) => { const cached = currentBenchmarkRun(run); return cached?.cpu !== undefined && productMetricCount(cached) > 0 ? [cached] : []; }), ); } function assembleCachedBenchmarkSnapshot( runs: CachedBenchmarkRun[], ): BenchmarkSeries[] { return assembleBenchmarkSeries(runs); } async function loadCachedBenchmarkSnapshot(now = Date.now()): Promise { await benchmarkStore.load(); if (benchmarkStore.quarantineFuture(now)) { await benchmarkStore.save(now); } const cutoff = now - SPARK_DAYS * 86_400_000; const refreshedRuns = benchmarkStore.refreshedRuns(); const cachedRuns = refreshedRuns ?? benchmarkStore.list(cutoff); if (cachedRuns.some((run) => run.cpu === undefined)) { benchmarkStore.invalidateRefresh(now); await benchmarkStore.save(now); } benchmarkRefreshedAt = benchmarkStore.refreshedAt; if (refreshedRuns === null) { const available = cachedRuns.map((run) => ({ id: run.runId, run_attempt: run.runAttempt, created_at: new Date(run.at).toISOString(), conclusion: "success", })); snapshot = assembleBenchmarkSnapshot( sampleBenchmarkRuns(available, cutoff), ); } else { snapshot = assembleCachedBenchmarkSnapshot(refreshedRuns); } } async function markBenchmarkRefreshed( runs: Run[], result: BenchmarkRefreshResult, ): Promise { const cachedRuns = runs.map((run) => benchmarkStore.get(run.id)!); const previous = benchmarkStore.markRefreshed( Date.now(), cachedRuns, result, ); await benchmarkStore.save().catch((error) => { benchmarkStore.restoreRefresh(previous); throw error; }); const refreshedRuns = benchmarkStore.refreshedRuns(); if (refreshedRuns !== null) { snapshot = assembleCachedBenchmarkSnapshot(refreshedRuns); } benchmarkRefreshedAt = benchmarkStore.refreshedAt; } // Every completed run in the window, thinned to one per collection bucket. A // run's color does not decide whether it measured anything: `deno bench` exits // non-zero when any one benchmark throws, having already written a complete // report of the rest, and the workflow uploads that report either way. Over one // recent 45-day stretch, 26 of the 30 red runs carried an artifact // indistinguishable from a green run's, and they arrived in blocks of a dozen // or more consecutive runs — so the runs the tile lost were concentrated // exactly where a thinned line does the most damage. What makes a run usable is // the artifact, and every reader below already checks that: a report has to // parse, name a processor, and carry benchmarks the tile trends. The tile's red // state is unaffected, because it reads the run list rather than this sample. export function sampleBenchmarkRuns< T extends { created_at: string; conclusion: string | null }, >(runs: T[], cutoff: number): T[] { const perBucket = new Map(); for (const run of runs) { const at = Date.parse(run.created_at); if (run.conclusion === null || at < cutoff) continue; const bucket = Math.floor(at / COLLECTION_BUCKET_MS); const current = perBucket.get(bucket); if (!current || at > Date.parse(current.created_at)) { perBucket.set(bucket, run); } } return [...perBucket.values()].sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at) ); } const benchmarkDrill = { href: "/bench?view=runtime&repo=labs", hint: "metrics ↗", }; function benchmarkUnavailable(sub: string, aside?: string): TileView { return { ...benchmarkDrill, aside, status: "unknown", value: "—", sub, }; } const errorMessage = (error: unknown): string => error instanceof Error ? error.message : String(error); // The geometric mean of the per-benchmark ratios between two runs on the same // processor, over the benchmarks they share that match `select` (each with a // positive 75th percentile). Geometric, not arithmetic, so a benchmark that // doubles and one that halves cancel to no change. A benchmark in only one of // the two runs is not in the ratio, so adding or removing one is not a change. // `undefined` when the runs share no selected positive measurements. // // The ratio compares the 75th percentile of each benchmark rather than its // average. `deno bench` measures each benchmark for a fixed wall-clock budget, // so one stalled sample raises the reported average by the stall divided by // that budget, whatever the sample count. A stall of a fifth of a second // against a budget of half a second is a quarter added to the average, which is // the size of the moves the trend is meant to detect. The runners stall that // long often enough that reading the average reports steps no commit caused. // // The 75th percentile is far enough up the distribution to move when a change // makes some but not all of an operation's runs slower, and far enough down // that a handful of stalled samples cannot reach it. The fastest sample // survives a stall equally well but is the floor of the distribution, so it is // blind to a change that leaves the floor alone and widens everything above it // — a slow path taken only sometimes moves nothing it can see. // // Both the product ratio and the calibration ratio read this one statistic, and // have to: an index step divides the second out of the first, and two different // statistics would not cancel. // // A change confined above the 75th percentile is still invisible here, and on // user-facing timings that tail is what a person actually notices. Reading it // from these runs would need the stalls told apart from the tail the code // produces, which the current sample counts do not allow: on the busiest // processor, `p99` puts 11% of neighboring run pairs more than a quarter // apart with no change behind them, against 2% for the 75th percentile. The // drill-down plots the whole ladder from `min` to `max` in the meantime. function sharedBenchmarkRatio( previous: CachedBenchmarkRun, current: CachedBenchmarkRun, select: (key: string) => boolean, ): number | undefined { let logSum = 0, count = 0; for (const [key, stats] of current.metrics) { if (!select(key)) continue; const before = previous.metrics.get(key); if (before && before.p75 > 0 && stats.p75 > 0) { logSum += Math.log(stats.p75 / before.p75); count++; } } return count ? Math.exp(logSum / count) : undefined; } /** * Returns the selected benchmarks' change after machine calibration. A pair * without shared positive product measurements leaves the index unchanged. * A pair without shared calibration measurements reads uncorrected. */ function benchmarkStepRatio( previous: CachedBenchmarkRun, current: CachedBenchmarkRun, select: (key: string) => boolean, ): number { const product = sharedBenchmarkRatio( previous, current, (key) => !isCalibrationKey(key) && select(key), ); if (product === undefined) return 1; const machine = sharedBenchmarkRatio(previous, current, isCalibrationKey) ?? 1; return product / machine; } const CPU_COLORS = [ "#6ea8fe", "#d97757", "#10a37f", "#b58cf6", "#e0a852", "#56b6c2", "#e06c9f", "#9fb36b", "#ff9da7", "#8cd17d", "#b6992d", "#499894", "#d37295", "#fabfd2", "#b07aa1", "#86bcb6", ]; const CPU_LINE_MAX_X_GAP = 0.2; function cpuHash(cpu: string): number { let hash = 0x811c9dc5; for (let index = 0; index < cpu.length; index++) { hash ^= cpu.charCodeAt(index); hash = Math.imul(hash, 0x01000193); } return hash >>> 0; } function generatedCpuColor(hash: number): string { const red = 112 + (hash & 0x7f); const green = 112 + ((hash >>> 7) & 0x7f); const blue = 112 + ((hash >>> 14) & 0x7f); return `#${((red << 16) | (green << 8) | blue).toString(16).padStart( 6, "0", )}`; } export function availableGeneratedCpuColor( hash: number, usedColors: Set, generateColor: (hash: number) => string = generatedCpuColor, ): string { // The color uses the low 21 hash bits. Adding an odd number visits every // possible low-bit value before repeating. for (let salt = 0; salt <= usedColors.size; salt++) { const mixed = (hash + Math.imul(salt, 0x9e3779b9)) >>> 0; const candidate = generateColor(mixed); if (!usedColors.has(candidate)) return candidate; } throw new Error("Could not assign a distinct CPU color."); } function cpuColors(cpus: Iterable): Map { const colors = new Map(); const usedPaletteIndexes = new Set(); const usedColors = new Set(); const sorted = [...new Set(cpus)].sort((a, b) => a.localeCompare(b)); for (const [ordinal, cpu] of sorted.entries()) { const hash = cpuHash(cpu); let color: string; if (ordinal < CPU_COLORS.length) { const start = hash % CPU_COLORS.length; let index = start; for (let offset = 0; offset < CPU_COLORS.length; offset++) { const candidate = (start + offset) % CPU_COLORS.length; if (!usedPaletteIndexes.has(candidate)) { index = candidate; break; } } usedPaletteIndexes.add(index); color = CPU_COLORS[index]; } else { color = availableGeneratedCpuColor(hash, usedColors); } usedColors.add(color); colors.set(cpu, color); } return colors; } interface BenchmarkCpuIndex { cpu: string; color: string; points: { at: number; index: number }[]; windowCount: number; windowPoints: { at: number; index: number }[]; trend: ReturnType; } export function benchmarkHeadlineCandidates< T extends { points: { at: number }[] }, >(indices: T[], now: number): T[] { const cutoff = now - BENCH_HEADLINE_MAX_AGE_HOURS * 60 * 60_000; return indices.filter((series) => series.points.some((point) => point.at >= cutoff && point.at <= now) ); } type CpuBenchmarkRun = CachedBenchmarkRun & { cpu: string }; // One run per trend bucket on one processor, newest kept, oldest first. The // collection keeps far finer resolution than this, because the drill-down's // shortest view needs it, and the trend does not: the benchmarks run four- // hourly, so runs closer together than that are a re-run, a manual dispatch or // a schedule catching up. Counted as separate samples they let one stretch of // wall clock supply the whole of a level, and a level the headline is read off // rests on as few as three samples. export function benchmarkTrendRuns(runs: T[]): T[] { const perBucket = new Map(); for (const run of runs) { const bucket = Math.floor(run.at / BENCH_TREND_BUCKET_MS); const current = perBucket.get(bucket); if (!current || run.at > current.at) perBucket.set(bucket, run); } return [...perBucket.values()].sort((a, b) => a.at - b.at); } function benchmarkCpuIndices( cached: CpuBenchmarkRun[], now: number, select: (key: string) => boolean, ): BenchmarkCpuIndex[] { const byCpu = new Map(); for (const run of cached) { const runs = byCpu.get(run.cpu); if (runs) runs.push(run); else byCpu.set(run.cpu, [run]); } const windowCutoff = now - BENCH_TREND_MAX_AGE_DAYS * 86_400_000; const colors = cpuColors(byCpu.keys()); return [...byCpu] .sort(([a], [b]) => a.localeCompare(b)) .map(([cpu, sampled]) => { const runs = benchmarkTrendRuns(sampled); const points: { at: number; index: number }[] = []; let index = 1; for (let run = 0; run < runs.length; run++) { if (run > 0) { index *= benchmarkStepRatio(runs[run - 1], runs[run], select); } points.push({ at: runs[run].at, index }); } const inWindow = points.filter((point) => point.at >= windowCutoff ).length; const windowCount = Math.min( points.length, Math.max(inWindow, BENCH_TREND_MIN_RUNS), ); const windowPoints = points.slice(points.length - windowCount); return { cpu, color: colors.get(cpu)!, points, windowCount, windowPoints, trend: benchmarkTrend( windowPoints.map((point) => point.at), windowPoints.map((point) => point.index), ), }; }); } const runIsCompleted = (run: Run): boolean => run.status === "completed" && run.conclusion !== null; // Only a genuine failure counts (concDot's red set), so a cancelled or // superseded run is not read as one. const runFailed = (run: Run): boolean => concDot(run.conclusion, run.run_attempt ?? 1) === "red"; // The newest completed run, reading the newest-first run list. A run still in // flight is passed over until it finishes. const latestCompletedRun = (runs: Run[]): Run | undefined => runs.find(runIsCompleted); // How many of the most recent runs failed in a row, reading the newest-first run // list. Counting stops at the first completed run that did not fail. A run still // in flight is passed over. The count covers the runs this collection fetched, // so a streak reaching past the window reads as the length of the window. function failedRunStreak(runs: Run[]): number { let streak = 0; for (const run of runs) { if (!runIsCompleted(run)) continue; if (!runFailed(run)) break; streak++; } return streak; } // The newest run that passed. A run that passed on a later attempt counts: it // ran and it worked. const lastSuccessfulRun = (runs: Run[]): Run | undefined => runs.find((run) => run.conclusion === "success"); // The failed tile's line: how long ago the benchmarks last worked, and how many // runs have failed since. The age comes first because it is the size of the // outage. Without a passing run to date the outage from — every run in the // window failed — the line is the count alone. export function benchmarkFailureLabel(runs: Run[], now: number): string { const streak = failedRunStreak(runs); const failures = `${streak} run${streak === 1 ? "" : "s"} failed`; const good = lastSuccessfulRun(runs); if (good === undefined) { return streak > 1 ? `last ${streak} runs failed` : "last run failed"; } return `last good ${ humanSpan(now - Date.parse(good.created_at)) } ago · ${failures}`; } export interface BenchmarkRerunHandoff { href: string; label: string; hint: string; } // Where the drill-down's rerun control sends the viewer. The dashboard reads // GitHub with a read-only token, so starting a run is GitHub's own job: the // control is a link, and GitHub decides whether the viewer may press the button // it lands on. A failed newest run points at that run, whose "Re-run all jobs" // repeats it. Anything else points at the workflow, whose "Run workflow" starts // a fresh run. Without a run list the workflow is the target, which is one click // from the newest run either way. export function benchmarkRerunHandoff( runs: Run[] | undefined, ): BenchmarkRerunHandoff { const latest = latestCompletedRun(runs ?? []); return latest !== undefined && runFailed(latest) ? { href: `https://github.com/${REPO}/actions/runs/${latest.id}`, label: "rerun the failed benchmark run ↗", hint: "Select \"Re-run all jobs\" on GitHub to repeat the run.", } : { href: `https://github.com/${REPO}/actions/workflows/${WORKFLOW}`, label: "run benchmarks now ↗", hint: "Run workflow on GitHub starts a fresh run.", }; } // A run that has not finished, anywhere in the list. A rerun of an older run // keeps that run's place in the list rather than moving to the head, so this // looks past the newest entries. const runInFlight = (runs: Run[]): boolean => runs.some((run) => run.status !== "completed"); const RUNNING_BADGE = `running`; // The grid tile trends a scale-invariant benchmark index over about 45 days. // Each processor has its own index and line. Every index starts at one and // changes only between runs on that processor. Hardware changes therefore // never become benchmark changes. The headline shows the largest established // trend among processors measured in the last twelve hours. Orange means any // eligible processor trends up. Red means the most recent run failed or // produced no readable data. Its headline reads `failed (was )` when a // cached trend is available, and `failed` otherwise. The line below dates the // outage instead of counting the benchmarks measured. `offline` names a fetch // failure and takes that line the same way, so an unreadable collection costs // the count rather than adding a line to it. The tile then keeps its // last-known trends gray, or shows a gray dash when no history is cached. function benchmarkIndexView( runs: Run[], now: number, select: (key: string) => boolean, offline?: string, ): TileView { const cutoff = now - SPARK_DAYS * 86_400_000; // The most recent completed run sets the failed state. const latestCompleted = latestCompletedRun(runs); const failedCi = latestCompleted !== undefined && runFailed(latestCompleted); // A run under way is a header badge, and leaves the verdict to the runs that // have finished. The badge is what says the tile's color may be about to move. const aside = runInFlight(runs) ? RUNNING_BADGE : undefined; // A run that finished green on CI but whose artifact resolved to no readable // benchmark data is as good as failed: it ran and produced nothing usable. Only // a cached run with an empty result counts — a run still unread (or read-failed) // stays unknown and is retried, so a transient blip does not flash red. const latestResult = latestCompleted?.conclusion === "success" ? benchmarkStore.get(latestCompleted.id) : undefined; const noData = latestResult !== undefined && productMetricCount(latestResult, select) === 0; const failed = failedCi || noData; const failSub = failedCi ? benchmarkFailureLabel(runs, now) : "no benchmark data"; // The runs with processor identities and readable artifacts in the window, // oldest -> newest. The artifact decides, not the run's color. const cached = (benchmarkStore.refreshedRuns() ?? benchmarkStore.list(cutoff)) .filter((run): run is CpuBenchmarkRun => run.at >= cutoff && run.cpu !== undefined && productMetricCount(run, select) > 0 ) .sort((a, b) => a.at - b.at); // A failed headline uses the latest measurements to date its trend window // and determine which processors are eligible. const trendAt = failed && !offline ? cached.at(-1)?.at ?? now : now; const indices = benchmarkCpuIndices(cached, trendAt, select); if (!indices.length) { // A fetch failure with nothing cached to stand on: a gray dash and the reason. if (offline) return benchmarkUnavailable(offline, aside); if (failed) { return { ...benchmarkDrill, status: "bad", value: "failed", sub: failSub, aside, }; } // A completed fetch with runs but no readable artifacts reports the missing // data. A completed fetch with no runs reports the missing run history. return benchmarkUnavailable( runs.length ? "benchmark data unavailable" : "no benchmark runs", aside, ); } const headlineCandidates = benchmarkHeadlineCandidates(indices, trendAt); if (!headlineCandidates.length && !offline && !failed) { return benchmarkUnavailable("no recent benchmark data", aside); } const displayCandidates = headlineCandidates.length ? headlineCandidates : indices; const established = displayCandidates.filter((series) => series.trend.label !== "new" ); const headlinePool = established.length ? established : displayCandidates; const headline = headlinePool.reduce(( worst, series, ) => series.trend.pct > worst.trend.pct ? series : worst); const rising = headlineCandidates.some((series) => series.trend.status === "warn" || series.trend.status === "bad" ); // An offline collection keeps the trend but grays it: the run list it would need // to judge failed or rising could not be fetched, so it never asserts red or green. const status: Status = offline ? "unknown" : failed ? "bad" : rising ? "warn" : "good"; const trendLabel = escapeHtml(headline.trend.label); const value = status === "bad" ? `failed (was ${trendLabel})` : trendLabel; const latest = cached[cached.length - 1]; const count = productMetricCount(latest, select); // Name the highlighted window's span beside the count, like CI duration names its // median window — in days (via humanSpan), not "runs", so it does not read as the // main-CI run count. Only when a window is actually highlighted; otherwise the // whole sparkline is the window and its span already sits in the corner. const windowLabel = headline.windowCount < headline.points.length ? ` · last ${humanSpan(spanMs(headline.windowPoints))}` : ""; // One line sits under the headline. A tile with something to say about why it // cannot measure says that there, in place of the count, rather than beside // it: a second line would grow the tile past the ones it shares a row with. const sub = offline ?? (failed ? failSub : undefined); const countLine = sub !== undefined ? "" : `
${count} benchmark${ count === 1 ? "" : "s" }${windowLabel}
`; const allPoints = indices.flatMap((series) => series.points); const chartStart = minOf(allPoints.map((point) => point.at)); const chartEnd = maxOf(allPoints.map((point) => point.at)); const chartSpan = chartEnd - chartStart; const chartAxis = chartSpan || 1; const chart = multiSparkline( indices.map((series) => ({ vals: series.points.map((point) => point.index), ...themedChartSeries(series.color), xs: series.points.map((point) => (point.at - chartStart) / chartAxis), highlightCount: series.windowCount, maxXGap: CPU_LINE_MAX_X_GAP, showSinglePoint: true, })), { fade: true }, ); return { ...benchmarkDrill, status, value, valueLabel: status === "bad" ? `failed (was ${headline.trend.label})` : undefined, sub, extra: `${countLine}${chart}`, duration: chartSpan, aside, }; } interface BenchmarkCollectionOutcome { error?: unknown; } async function collectBenchmark( credential: GitHubCredential, progress: BenchmarkProgressRecord, github: BenchmarkGitHub, // The run list the caller has already paged, when it has one. The tile pages it // for its own headline moments earlier, and one list serves both. knownRuns?: Run[], ): Promise { const now = Date.now(); const cutoff = now - SPARK_DAYS * 86_400_000; try { await benchmarkStore.load(); const runs = knownRuns ?? await pageBenchmarkRuns(github, credential, cutoff); const chosen = sampleBenchmarkRuns(runs, cutoff); const priorRefresh = benchmarkStore.refresh; const chosenReferences = chosen.map((run) => ({ runId: run.id, runAttempt: run.run_attempt ?? 1, })); if ( priorRefresh && JSON.stringify(priorRefresh.runs) !== JSON.stringify(chosenReferences) ) { benchmarkStore.invalidateRefresh(now); await benchmarkStore.save(now); benchmarkRefreshedAt = benchmarkStore.refreshedAt; } const isCached = (run: Run) => currentRunMetrics(run) !== undefined; const cachedRuns = chosen.filter(isCached); const missing = chosen.filter((run) => !isCached(run)); updateBenchmarkProgress(progress, { phase: "fetching", totalRuns: chosen.length, cachedRuns: cachedRuns.length, needsReload: missing.length > 0, }); if (!chosen.length) { snapshot = []; await markBenchmarkRefreshed([], "no-runs"); return {}; } let firstReadError: unknown; for ( let index = 0; index < missing.length; index += BENCHMARK_FETCH_CONCURRENCY ) { const batch = missing.slice(index, index + BENCHMARK_FETCH_CONCURRENCY); const outcomes = await Promise.all(batch.map(async (run) => { updateBenchmarkProgress(progress, { requestsMade: progress.state.requestsMade + 1, }); let cached = false; try { const outcome = await loadRun(run, credential, github); cached = outcome.cached; return outcome.error === undefined ? null : { readError: outcome.error }; } catch (error) { return { persistenceError: error }; } finally { updateBenchmarkProgress(progress, { responsesReceived: progress.state.responsesReceived + 1, successfulResponses: progress.state.successfulResponses + (cached ? 1 : 0), failedResponses: progress.state.failedResponses + (cached ? 0 : 1), }); } })); const persistenceFailure = outcomes.find((outcome) => outcome && "persistenceError" in outcome ); if (persistenceFailure && "persistenceError" in persistenceFailure) { throw persistenceFailure.persistenceError; } const readFailure = outcomes.find((outcome) => outcome && "readError" in outcome ); if ( firstReadError === undefined && readFailure && "readError" in readFailure ) firstReadError = readFailure.readError; } updateBenchmarkProgress(progress, { phase: "saving" }); await benchmarkStore.save(now); const withData = chosen.filter((run) => (currentRunMetrics(run)?.size ?? 0) > 0 ); if (!withData.length) { if (firstReadError === undefined) snapshot = []; if (firstReadError === undefined) { await markBenchmarkRefreshed(chosen, "data-unavailable"); } return { error: firstReadError }; } const collectedSnapshot = assembleBenchmarkSnapshot(withData); if (collectedSnapshot.length || firstReadError === undefined) { snapshot = collectedSnapshot; } if (firstReadError === undefined) { await markBenchmarkRefreshed( chosen, collectedSnapshot.length ? "data" : "no-metric", ); } return { error: firstReadError }; } catch (error) { return { error }; } } function startBenchmarkRefresh( ctx: Ctx, baseline?: string, snapshotIsFresh = false, scope: BenchmarkRefreshScope = "bench", // The caller's own run list, still in flight. Passing it registers this refresh // before the list arrives, so a drill-down request landing meanwhile queues // behind this collection instead of starting a second one. The refresh then // decides whether there is anything to fetch once the list is in. knownRuns?: Promise, ): BenchmarkRefresh { const github = scope === "bench" ? performanceBenchmarkGitHub : ordinaryBenchmarkGitHub; const credential = dashboardGitHubCredential(ctx)!; const activeBenchmarkRefresh = activeBenchmarkRefreshes.get(scope); if (activeBenchmarkRefresh) { if (baseline !== undefined) { activeBenchmarkRefresh.progress.baselines.add(baseline); } return { progress: { ...activeBenchmarkRefresh.progress.state }, result: activeBenchmarkRefresh.result, }; } if (snapshotIsFresh) { return { progress: null, result: Promise.resolve({}) }; } const progress = newBenchmarkProgress(baseline); const queuedBehindOtherScope = activeBenchmarkRefreshes.size > 0; const previousCollection = benchmarkCollectionTail; let finishCollection!: () => void; benchmarkCollectionTail = new Promise((resolve) => { finishCollection = resolve; }); const collection = async (): Promise => { await previousCollection; let known: Run[] | undefined; if (knownRuns) { known = await knownRuns; // The caller's list is the only one this refresh reads. Its failure is the // caller's to report, and there is nothing here to refresh against. if (!known) return {}; // Nothing new has been sampled since the last refresh, and that refresh is // recent, so the artifact history already covers this list. if ( benchmarkSnapshotIsFresh() && benchmarkRefreshCovers(known, Date.now()) ) { return {}; } } else if (queuedBehindOtherScope && benchmarkSnapshotIsFresh()) { // A queued refresh with no run list reuses the fresh shared artifact // history. return {}; } return await collectBenchmark(credential, progress, github, known); }; let refreshFinished = false; const finishRefresh = () => { if (refreshFinished) return; refreshFinished = true; finishCollection(); if (activeBenchmarkRefreshes.get(scope)?.progress === progress) { activeBenchmarkRefreshes.delete(scope); } }; const result = collection().then((outcome) => { finishRefresh(); if (outcome.error) { const message = outcome.error instanceof Error ? outcome.error.message : String(outcome.error); if (scope === "bench") { benchmarkRefreshFailedAt = Date.now(); benchmarkRefreshError = friendlyError(message); } const version = benchmarkSnapshotVersion(); updateBenchmarkProgress(progress, { phase: "error", error: friendlyError(message), needsReload: [...progress.baselines].some((value) => value !== version), }); } else { if (scope === "bench") { benchmarkRefreshFailedAt = 0; benchmarkRefreshError = ""; } benchmarkRefreshedAt = benchmarkStore.refreshedAt; const version = benchmarkSnapshotVersion(); updateBenchmarkProgress(progress, { phase: "complete", needsReload: [...progress.baselines].some((value) => value !== version), }); } return outcome; }).finally(finishRefresh); activeBenchmarkRefreshes.set(scope, { progress, result }); return { progress: { ...progress.state }, result }; } // Whether the drill-down's last refresh already covers the runs this list names. // The refresh records which runs it sampled, and only a successful run in a new // time bucket changes that set, so an unchanged set means there is no artifact to // fetch and no reason to redo the work. function benchmarkRefreshCovers(runs: Run[], now: number): boolean { const refresh = benchmarkStore.refresh; if (!refresh) return false; const chosen = sampleBenchmarkRuns(runs, now - SPARK_DAYS * 86_400_000).map(( run, ) => ({ runId: run.id, runAttempt: run.run_attempt ?? 1 })); return JSON.stringify(refresh.runs) === JSON.stringify(chosen); } function benchmarkSnapshotIsFresh(now = Date.now()): boolean { const age = now - benchmarkRefreshedAt; return Boolean( benchmarkRefreshedAt && age >= 0 && age < BENCHMARK_REFRESH_MS, ); } function benchmarkRefreshRecentlyFailed(now = Date.now()): boolean { const age = now - benchmarkRefreshFailedAt; return Boolean( benchmarkRefreshFailedAt && !activeBenchmarkRefreshes.has("bench") && age >= 0 && age < BENCHMARK_REFRESH_MS, ); } function benchmarkLastRequestError(): string | null { return benchmarkRefreshFailedAt ? benchmarkRefreshError || "temporarily unavailable" : null; } /** * Makes the tiles hold no runs, and keep the runs they read from then on in * memory alone, so that the next list paged is read afresh rather than joined * to the runs an earlier test listed, whether in memory or in a file. */ export function forgetBenchmarkRunsForTest(): void { benchmarkRuns = new WeakMap(); benchmarkRunsSaved = false; latestBenchmarkRuns = []; } function benchmarkServerContext(): Ctx { return { runs: () => Promise.resolve([]), runsFor: () => Promise.resolve([]), env: (key) => Deno.env.get(key), }; } /** Active collections shared by tiles using the same credentials. */ const benchmarkTileCollections = new Map< GitHubCredential, ReturnType >(); /** Collects the workflow list together with its completed artifact refresh. */ async function collectBenchmarkTileRuns( ctx: Ctx, credential: GitHubCredential, ): Promise<{ runs: Run[]; offline?: string }> { await loadCachedBenchmarkSnapshot(); // Run status refreshes every collection; artifact history has its own cadence. let listError: unknown; const listing = pageBenchmarkRuns( ordinaryBenchmarkGitHub, credential, Date.now() - SPARK_DAYS * 86_400_000, ).catch((error) => { listError = error; return undefined; }); const refresh = startBenchmarkRefresh( ctx, undefined, false, "dashboard", listing, ).result; const runs = await listing; await refresh; return { runs: runs ?? [], offline: runs ? undefined : friendlyError(errorMessage(listError)), }; } /** Builds a benchmark tile over the selected product measurements. */ function makeBenchmarkTile( label: string, select: (key: string) => boolean = () => true, href = benchmarkDrill.href, ): Tile { return { label, repo: REPO, intervalMs: 60_000, showOnlyCompletedViews: true, async collect(ctx): Promise { const credential = dashboardGitHubCredential(ctx); if (!credential) return { ...benchmarkUnavailable("set GH_TOKEN"), href }; let collection = benchmarkTileCollections.get(credential); if (!collection) { collection = collectBenchmarkTileRuns(ctx, credential).finally(() => { benchmarkTileCollections.delete(credential); }); benchmarkTileCollections.set(credential, collection); } const { runs, offline } = await collection; return { ...benchmarkIndexView(runs, Date.now(), select, offline), href }; }, }; } /** All product benchmarks, with the shared performance history routes. */ export const benchmark: Tile = { ...makeBenchmarkTile("all benchmarks"), routes: [ { path: "/bench", handler: (_req, url) => { const view = url.searchParams.get("view"); if (view === "ci") { return ciJobHistoryResponse(url); } if (view === "gantt") { return new Response(ciGanttPage(url), { headers: { "content-type": "text/html; charset=utf-8" }, }); } if (view !== "runtime") { return new Response("unknown performance view", { status: 400 }); } return benchmarkHistoryResponse(url); }, }, { path: "/bench/check", handler: (_req, url) => { const view = url.searchParams.get("view"); if (view === "ci") { return ciJobHistoryCheckResponse(url); } if (view === "runtime") return benchmarkHistoryCheckResponse(); return new Response("unknown performance view", { status: 400 }); }, }, { path: "/bench/ci-progress", handler: (_req, url) => ciJobHistoryProgressResponse(url), }, { path: "/bench/runtime-progress", handler: (_req, url) => benchmarkHistoryProgressResponse(url), }, ] satisfies Route[], }; /** Topic board journey and 100-topic load measurements. */ export const keyBenchmarks: Tile = makeBenchmarkTile( "key benchmarks", isKeyBenchmark, `${benchmarkDrill.href}&key=1`, ); export async function benchmarkHistoryResponse( url: URL, ctx = benchmarkServerContext(), ): Promise { await loadCachedBenchmarkSnapshot(); const credential = dashboardGitHubCredential(ctx); const baseline = benchmarkSnapshotVersion(); let progress: BenchmarkFetchProgress | undefined; let refreshError: string | undefined; if (!credential) { refreshError = snapshot.length ? "Set GH_TOKEN to refresh runtime benchmark history." : "Set GH_TOKEN to collect runtime benchmark history."; } if (credential && !benchmarkRefreshRecentlyFailed()) { const refresh = startBenchmarkRefresh( ctx, baseline, benchmarkSnapshotIsFresh(), ); progress = refresh.progress ?? undefined; if (progress) void refresh.result; else await refresh.result; } const lastRequestError = progress ? undefined : benchmarkLastRequestError() ?? undefined; return new Response( benchPage( url.searchParams.get("stat") ?? DEFAULT_LABEL, url.searchParams.get("sort") ?? "file", ciHistoryDays(url.searchParams.get("days")), Date.now(), ciHistorySource(url.searchParams.get("repo")).key, { progress, refreshError, lastRequestError, fragment: url.searchParams.get("fragment") === "range", keyOnly: url.searchParams.get("key") === "1", }, ), { headers: { "content-type": "text/html; charset=utf-8" } }, ); } export async function benchmarkHistoryCheckResponse( ctx = benchmarkServerContext(), ): Promise { await loadCachedBenchmarkSnapshot(); const credential = dashboardGitHubCredential(ctx); let progress: BenchmarkFetchProgress | null = null; if ( credential && (!benchmarkRefreshFailedAt || activeBenchmarkRefreshes.has("bench") || Date.now() - benchmarkRefreshFailedAt >= BENCHMARK_REFRESH_MS) ) { const refresh = startBenchmarkRefresh( ctx, benchmarkSnapshotVersion(), benchmarkSnapshotIsFresh(), ); progress = refresh.progress; if (progress) void refresh.result; else await refresh.result; } const lastRequestError = progress ? null : benchmarkLastRequestError(); return Response.json( { version: benchmarkSnapshotVersion(), progress, lastRequestError }, { headers: { "cache-control": "no-store" } }, ); } interface BenchmarkProgressProvider { progress(id: string): BenchmarkFetchProgress | null; subscribe( id: string, listener: (progress: BenchmarkFetchProgress) => void, ): (() => void) | null; } const defaultBenchmarkProgressProvider: BenchmarkProgressProvider = { progress: benchmarkProgress, subscribe: subscribeBenchmarkProgress, }; export function benchmarkHistoryProgressResponse( url: URL, provider = defaultBenchmarkProgressProvider, ): Response { const id = url.searchParams.get("id"); if (!id) return new Response("missing progress id", { status: 400 }); if (!provider.progress(id)) { return new Response("unknown progress id", { status: 404 }); } const encoder = new TextEncoder(); let unsubscribe: (() => void) | undefined; let closed = false; const stream = new ReadableStream({ start(controller) { const send = (progress: BenchmarkFetchProgress) => { if (closed) return; controller.enqueue(encoder.encode( `event: progress\ndata: ${JSON.stringify(progress)}\n\n`, )); if (progress.phase === "complete" || progress.phase === "error") { closed = true; controller.close(); unsubscribe?.(); } }; unsubscribe = provider.subscribe(id, send) ?? undefined; if (closed) unsubscribe?.(); }, cancel() { closed = true; unsubscribe?.(); }, }); return new Response(stream, { headers: { "content-type": "text/event-stream", "cache-control": "no-cache", }, }); } export function pointsForWindow( points: T[], axisStart: number, bucketMs: number, axisEnd = Infinity, ): T[] { const buckets = new Map(); for (const point of points) { if (point.at < axisStart || point.at > axisEnd) continue; const bucket = Math.floor(point.at / bucketMs); const current = buckets.get(bucket); if (!current || point.at > current.at) buckets.set(bucket, point); } return [...buckets.values()].sort((a, b) => a.at - b.at); } export function representativeBenchmarkCpu< T extends { cpu: string; sampleCount: number; points: { at: number }[] }, >(series: T[]): T | undefined { let representative: T | undefined; for (const candidate of series) { if (!representative) { representative = candidate; continue; } const candidateLatest = candidate.points.at(-1)?.at ?? -Infinity; const representativeLatest = representative.points.at(-1)?.at ?? -Infinity; if ( candidate.sampleCount > representative.sampleCount || (candidate.sampleCount === representative.sampleCount && candidateLatest > representativeLatest) || (candidate.sampleCount === representative.sampleCount && candidateLatest === representativeLatest && candidate.cpu.localeCompare(representative.cpu) < 0) ) { representative = candidate; } } return representative; } const dateLabel = (at: number): string => new Date(at).toLocaleDateString("en-US", { month: "short", day: "numeric", }); // The /bench drill-down: every benchmark's processor-specific lines for the // chosen measurement, grouped by source file. interface BenchmarkPageOptions { progress?: BenchmarkFetchProgress; refreshError?: string; lastRequestError?: string; fragment?: boolean; keyOnly?: boolean; } export function benchPage( statLabel: string, sortMode: string, days: number, now = Date.now(), repo: CiHistorySourceKey = "labs", options: BenchmarkPageOptions = {}, ): string { // "p50" is what the mean column used to be called, so a link saved under the // old name still opens the column it named. const requested = statLabel === "p50" ? "mean" : statLabel; const stat = STATS.find((s) => s.label === requested) ?? STATS.find((s) => s.label === DEFAULT_LABEL)!; const sort = sortMode === "trend" || sortMode === "duration" ? sortMode : "file"; const href = (st: string, so: string) => performanceViewHref("runtime", { repo, days, sort: so, stat: st, keyOnly: options.keyOnly, }); const statSel = STATS.map((s) => `${s.label}` ).join(""); const sortSel = (["file", "duration", "trend"] as const).map((so) => `${so}` ).join(""); const viewNav = performanceViewNav("runtime", { repo, days, sort, stat: stat.label, keyOnly: options.keyOnly, }); const version = benchmarkSnapshotVersion(); const handoff = benchmarkRerunHandoff(latestBenchmarkRuns); const progress = options.progress; const progressIdle = !progress || progress.phase === "complete" || progress.phase === "error"; const lastRequestError = progress?.phase === "error" ? progress.error ?? "unknown error" : !progress ? options.lastRequestError : undefined; const progressTitle = progressIdle ? "Idle" : progress.phase === "discovering" ? "Finding benchmark runs…" : `${progress.completedRuns} of ${progress.totalRuns} artifact checks complete`; const progressTotal = progressIdle ? "0 outstanding" : `${progress.completedRuns} / ${progress.totalRuns || "?"}`; const progressDetail = lastRequestError ? `Last collection stopped: ${escapeHtml(lastRequestError)}` : !progressIdle && progress ? `${progress.cachedRuns} cached · ${progress.requestsMade} artifact checks made · ${progress.responsesReceived} responded · ${progress.outstandingRequests} outstanding · ${progress.queuedRuns} queued` : "No requests in progress."; const progressUrl = progress ? `/bench/runtime-progress?id=${ escapeHtml(encodeURIComponent(progress.id)) }` : ""; const progressHtml = `
${progressTitle}${progressTotal}

${progressDetail}

`; const refreshNotice = options.refreshError && snapshot.length ? `

${escapeHtml(options.refreshError)}

` : ""; let body: string; let cpuLegend = ""; if (!snapshot.length) { body = progress && !progressIdle ? "" : `

${ escapeHtml( options.refreshError ?? "No runtime benchmark samples were found in the history window.", ) }

`; } else { const axisEnd = now; const axisStart = axisEnd - days * 86_400_000; const axisSpan = axisEnd - axisStart || 1; const bucketMs = ciHistoryBucketMs(days); const colors = cpuColors( snapshot.flatMap((series) => series.cpus.map((cpu) => cpu.cpu)), ); const cpuKeys = new Map( [...colors.keys()].map((cpu, index) => [ cpu, { label: `CPU ${index + 1}`, anchor: `cpu-type-${index + 1}` }, ]), ); const rows = snapshot.flatMap((s) => { if (options.keyOnly && !isKeyBenchmark(s.key)) return []; const visibleCpus = s.cpus.flatMap((series) => { const sourcePoints = series.points.filter((point) => point.at >= axisStart && point.at <= axisEnd ); const points = pointsForWindow( sourcePoints, axisStart, bucketMs, axisEnd, ); if (!points.length) return []; const values = points.map((point) => point.stats[stat.field]); const trend = benchmarkTrend( points.map((point) => point.at), values, ); return [{ cpu: series.cpu, color: colors.get(series.cpu)!, sampleCount: sourcePoints.length, points, values, pct: trend.pct, status: trend.status, trend: trend.label, latest: values[values.length - 1], }]; }); if (!visibleCpus.length) return []; const cpus = visibleCpus; const representative = representativeBenchmarkCpu(cpus)!; const status = representative.status; const pct = representative.pct; const allPoints = cpus.flatMap((series) => series.points); const firstAt = minOf(allPoints.map((point) => point.at)); const lastAt = maxOf(allPoints.map((point) => point.at)); const spark = multiSparkline( cpus.map((series) => ({ vals: series.values, ...themedChartSeries(series.color), xs: series.points.map((point) => (point.at - axisStart) / axisSpan), maxXGap: CPU_LINE_MAX_X_GAP, showSinglePoint: true, })), { fade: true, scale: { trim: PERFORMANCE_HISTORY_SCALE_TRIM, minValues: PERFORMANCE_HISTORY_SCALE_MIN_VALUES, }, }, ); return [{ key: s.key, file: s.key.split(" > ")[0], pct, st: status, spark, dur: lastAt > firstAt ? durationTag(lastAt - firstAt) : "", latest: representative.latest, representative, cpus, }]; }); const cpuDetails = new Map; firstAt: number; lastAt: number; }>(); for (const row of rows) { for (const series of row.cpus) { let detail = cpuDetails.get(series.cpu); if (!detail) { detail = { color: series.color, benchmarks: 0, runs: new Set(), firstAt: series.points[0].at, lastAt: series.points[series.points.length - 1].at, }; cpuDetails.set(series.cpu, detail); } detail.benchmarks++; for (const point of series.points) { detail.runs.add(`${point.runId}:${point.runAttempt}`); detail.firstAt = Math.min(detail.firstAt, point.at); detail.lastAt = Math.max(detail.lastAt, point.at); } } } if (cpuDetails.size) { cpuLegend = `

CPU types

${ [...cpuDetails] .sort(([a], [b]) => a.localeCompare(b)) .map(([cpu, detail]) => { const displayColor = themedChartSeries(detail.color).color; const benchmarks = `${detail.benchmarks} benchmark${ detail.benchmarks === 1 ? "" : "s" }`; const runs = `${detail.runs.size} run${ detail.runs.size === 1 ? "" : "s" }`; const observed = detail.firstAt === detail.lastAt ? `observed ${dateLabel(detail.firstAt)}` : `observed ${dateLabel(detail.firstAt)}–${ dateLabel(detail.lastAt) }`; const { label: cpuId, anchor } = cpuKeys.get(cpu)!; return `
${cpuId}${ escapeHtml(cpu) }${benchmarks} · ${runs} · ${observed}
`; }).join("") }
`; } const rowHtml = (r: (typeof rows)[number], label: string) => { const series = r.representative; const displayColor = themedChartSeries(series.color).color; const { label: cpuId, anchor } = cpuKeys.get(series.cpu)!; const latest = formatNs(series.latest); const sampleCount = series.sampleCount; const samples = `${sampleCount} sample${ sampleCount === 1 ? "" : "s" } in the selected window`; return `
${r.spark}${r.dur}
` + `${escapeHtml(label)}` + `${cpuId}${latest}${ escapeHtml(series.trend) }` + `
`; }; const axis = `
${ dateLabel(axisStart) }${dateLabel(axisEnd)}
`; if (!rows.length) { body = `

No benchmark samples were found in the selected window.

`; } else if (sort === "trend" || sort === "duration") { const sorted = [...rows].sort((a, b) => { const difference = sort === "duration" ? b.latest - a.latest : b.pct - a.pct; return difference || a.key.localeCompare(b.key); }); body = `${axis}
${ sorted.map((r) => rowHtml(r, r.key)).join("") }
`; } else { // Grouped by source file, in the snapshot's alphabetical order. const groups = new Map(); for (const r of rows) { const arr = groups.get(r.file); if (arr) arr.push(r); else groups.set(r.file, [r]); } body = axis + [...groups.entries()].map(([file, rs]) => `

${escapeHtml(file)}

${ rs.map((r) => rowHtml(r, r.key.split(" > ").slice(1).join(" > ") || r.key) ).join("") }
` ).join(""); } } const rangeContent = `
${progressHtml}${refreshNotice}

Per-op time across a run's samples — p0 = the fastest, p100 = the slowest, mean = the arithmetic mean. Lower is faster. Each CPU has its own colored line. The value, trend, and row color use the CPU with the most benchmark samples in the selected ${days}-day window; a tie uses the CPU with the newest sample. Fewer than seven distinct days are marked new. Duration and trend sorting use the displayed value and trend.

${body} ${cpuLegend}

Successful main runs come from the ${WORKFLOW} runs ↗ (deno bench artifacts). Collection keeps enough samples for the shortest window, and charts reduce longer windows to about ${CI_HISTORY_POINT_TARGET} evenly spaced points.

${ escapeHtml(handoff.label) }${ escapeHtml(handoff.hint) } This board reads GitHub and does not start runs itself.

`; if (options.fragment) return rangeContent; return `Benchmarks — ${ escapeHtml(stat.label) } ${DASHBOARD_THEME_HEAD}
← dashboardPerformance history${ escapeHtml(REPO) } · ${WORKFLOW}
${viewNav}
${rangeContent} ${dashboardThemeToggle()} ${DASHBOARD_THEME_CLIENT} `; }