import type { BenchmarkRecord, Dataset, FacetGroup, FacetValue, Filters, FilterBlocker, FilterScope } from './types' import { NONE_VALUE } from './types' const KNOWN_KEYS = new Set(['date', 'host', 'hardware', 'metrics']) /** Positional dimension names per known kernel op, for shapes like "4096:4096:4096". */ const SHAPE_SPECS: Record = { matmul: ['m', 'k', 'n'], attention: ['bs', 'seq', 'heads', 'd'], 'layer-norm': ['bs', 'seq', 'features'], softmax: ['bs', 'seq', 'features'], gelu: ['bs', 'seq', 'features'], conv1d: ['bs', 'ch', 'len', 'kernel'], } /** Compact dimension value for labels: 4096 -> 4k, 131072 -> 128k. */ export function compactDim(value: string): string { const n = Number(value) if (!Number.isFinite(n) || n < 1000) return value if (n % 1024 === 0) return `${n / 1024}k` if (n % 1000 === 0) return `${n / 1000}k` return value } /** * Parse a shape tag into named dimensions. Handles "k=v" pairs * ("bs=8 seq=4096 heads=32 d=128") and the positional colon form * ("4096:4096:4096", named via the op's spec, falling back to d1..dN). */ export function parseShape(shape: string, op: string): Record | null { const dims: Record = {} if (shape.includes('=')) { for (const part of shape.split(/[\s,]+/)) { const eq = part.indexOf('=') if (eq <= 0) return null dims[part.slice(0, eq)] = part.slice(eq + 1) } return Object.keys(dims).length > 0 ? dims : null } if (shape.includes(':')) { const parts = shape.split(':') const spec = SHAPE_SPECS[op] const names = spec && spec.length === parts.length ? spec : parts.map((_, i) => `d${i + 1}`) parts.forEach((p, i) => (dims[names[i]] = p)) return dims } return null } /** * Replace a record's raw `shape` tag with one `shape.` pseudo-tag per * dimension, so every dim gets its own filter chips, group-by option and * table column. No-op when the shape string cannot be parsed. */ function expandShapeTag(r: BenchmarkRecord, op: string): void { const shape = r.tags.shape if (shape === undefined) return const dims = parseShape(shape, op) if (!dims) return delete r.tags.shape for (const [dim, value] of Object.entries(dims)) r.tags[`shape.${dim}`] = value } /** * Canonical shape signature of a record (its "workload" identity): the raw * `shape` tag, or the parsed dimensions re-joined in order * ("m=1024 k=1024 n=4096"). Powers the combined shape facet. */ export function shapeSignature(r: BenchmarkRecord): string | null { const dims = Object.entries(r.tags).filter(([k]) => k.startsWith('shape.')) if (dims.length > 0) return dims.map(([k, v]) => `${k.slice('shape.'.length)}=${v}`).join(' ') return r.tags.shape ?? null } /** Parse one JSONL line into a BenchmarkRecord. Returns null for invalid lines. */ export function parseRecord(line: string): BenchmarkRecord | null { const trimmed = line.trim() if (!trimmed) return null let raw: Record try { raw = JSON.parse(trimmed) } catch { return null } if (typeof raw.date !== 'string' || typeof raw.host !== 'string' || typeof raw.hardware !== 'string') { return null } const tags: Record = {} const metrics: Record = {} for (const [key, value] of Object.entries(raw)) { if (KNOWN_KEYS.has(key)) continue if (typeof value === 'string') tags[key] = value else if (typeof value === 'number' && Number.isFinite(value)) metrics[key] = value } if (raw.metrics && typeof raw.metrics === 'object') { for (const [key, value] of Object.entries(raw.metrics as Record)) { if (typeof value === 'number' && Number.isFinite(value)) metrics[key] = value } } return { date: raw.date, host: raw.host, hardware: raw.hardware, tags, metrics } } async function fetchJsonl(url: string): Promise { const res = await fetch(url, { cache: 'no-store' }) if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status}`) return (await res.text()).split('\n') } /** Identity used to dedupe records present in both flat and partitioned files. */ function recordId(r: BenchmarkRecord): string { return JSON.stringify([r.date, r.host, r.hardware, r.tags, r.metrics]) } /** Load the data manifest (data/index.json) then every listed dataset. */ export async function loadDatasets(base: string): Promise { // no-store: new files dropped into /data must show up without a hard refresh. const manifestRes = await fetch(`${base}data/index.json`, { cache: 'no-store' }) // A missing manifest just means "no data available yet" — show the empty state. if (manifestRes.status === 404) return [] if (!manifestRes.ok) throw new Error(`Failed to fetch data manifest: ${manifestRes.status}`) // Dev servers may SPA-fallback to index.html (200) for missing files. let manifest: { categories?: Record } try { manifest = await manifestRes.json() } catch { return [] } const categories = manifest.categories ?? {} // Group files into logical benchmarks: the partitioned layout spreads one // benchmark over ///.jsonl files (the basename // is the benchmark name), so every path sharing category+name is one dataset. const groups = new Map() for (const [category, files] of Object.entries(categories)) { for (const path of files) { const file = path.split('/').pop() ?? path const name = file.replace(/\.jsonl$/, '') const key = `${category}/${name}` let group = groups.get(key) if (!group) groups.set(key, (group = { category, name, paths: [] })) group.paths.push(path) } } const datasets = await Promise.all( [...groups.values()].map(async ({ category, name, paths }) => { const lineSets = await Promise.all(paths.map((path) => fetchJsonl(`${base}data/${path}`))) const seen = new Set() const records = lineSets .flat() .map((line) => { const r = parseRecord(line) if (r) expandShapeTag(r, name) return r }) .filter((r): r is BenchmarkRecord => r !== null) .filter((r) => { const id = recordId(r) if (seen.has(id)) return false seen.add(id) return true }) .sort((a, b) => a.date.localeCompare(b.date)) return { name, category, records } }) ) return datasets } /** * Does one record pass the filter set? `skip` ignores one constraint group, * which is how facet counts are computed: a chip's count is the number of * records matching every OTHER group's current selection. */ export function matchesFilters(r: BenchmarkRecord, f: Filters, skip?: FilterScope): boolean { if (skip !== 'dates') { if (f.dateFrom && r.date < f.dateFrom) return false if (f.dateTo && r.date > f.dateTo) return false } if (skip !== 'hosts' && f.hosts.length > 0 && !f.hosts.includes(r.host)) return false if (skip !== 'hardware' && f.hardware.length > 0 && !f.hardware.includes(r.hardware)) return false for (const [key, values] of Object.entries(f.tags)) { if (!values || values.length === 0) continue if (skip === `tag:${key}`) continue // The virtual 'shape' key filters on the full shape signature (workload); // records missing the tag match the (none) facet value — never vanish silently. const value = (key === 'shape' ? shapeSignature(r) : r.tags[key]) ?? NONE_VALUE if (!values.includes(value)) return false } return true } export function applyFilters(records: BenchmarkRecord[], f: Filters): BenchmarkRecord[] { return records.filter((r) => matchesFilters(r, f)) } /** Numeric-aware ordering so shape dims sort 512 < 4096, not "4096" < "512". */ export function valueSort(values: Iterable): string[] { const unique = [...new Set(values)] const real = unique.filter((v) => v !== NONE_VALUE) const numeric = real.length > 0 && real.every((v) => v.trim() !== '' && !isNaN(Number(v))) real.sort(numeric ? (a, b) => Number(a) - Number(b) : (a, b) => a.localeCompare(b)) // The "missing" facet always sorts last. return unique.includes(NONE_VALUE) ? [...real, NONE_VALUE] : real } export function uniqueSorted(values: string[]): string[] { return [...new Set(values)].sort((a, b) => a.localeCompare(b)) } const GROUP_LABELS: Record = { hosts: 'Host', hardware: 'Hardware' } /** Display label: commit/rev abbreviated, shape signatures and dims compact. */ export function facetLabel(key: string, value: string): string { if ((key === 'commit' || key === 'rev') && value.length > 12) return value.slice(0, 8) if (key === 'shape' && value.includes('=')) { return value .split(/\s+/) .map((kv) => { const eq = kv.indexOf('=') return eq > 0 ? `${kv.slice(0, eq)}=${compactDim(kv.slice(eq + 1))}` : kv }) .join(' ') } if (key.startsWith('shape.')) return compactDim(value) return value } /** Tag values of one record, with missing tags exposed as NONE_VALUE. */ function tagFacetValue(r: BenchmarkRecord, key: string): string { return r.tags[key] ?? NONE_VALUE } /** * Faceted chip groups: for every group (hosts, hardware, each tag key) list * all dataset values with the count of records matching the OTHER groups' * current selections. Zero-count values are rendered disabled by the UI. */ export function buildFacets(records: BenchmarkRecord[], f: Filters): FacetGroup[] { const groups: FacetGroup[] = [] const make = ( key: string, label: string, scope: FilterScope, valueOf: (r: BenchmarkRecord) => string ): void => { const pool = records.filter((r) => matchesFilters(r, f, scope)) const counts = new Map() for (const r of pool) { const v = valueOf(r) counts.set(v, (counts.get(v) ?? 0) + 1) } const selected = key === 'hosts' ? f.hosts : key === 'hardware' ? f.hardware : (f.tags[key.slice('tag:'.length)] ?? []) const allValues = valueSort(records.map(valueOf)) const values: FacetValue[] = allValues.map((v) => ({ value: v, label: v === NONE_VALUE ? NONE_VALUE : facetLabel(key.slice('tag:'.length), v), count: counts.get(v) ?? 0, selected: selected.includes(v), })) groups.push({ key, label, values, selectedCount: selected.length, allCount: pool.length }) } make('hosts', GROUP_LABELS.hosts, 'hosts', (r) => r.host) make('hardware', GROUP_LABELS.hardware, 'hardware', (r) => r.hardware) // The combined 'shape' facet (workload identity) leads; per-dimension // groups follow as "shape · " so dims never lose their context. const hasShape = records.some((r) => shapeSignature(r) !== null) const tagKeys = valueSort([...records.flatMap((r) => Object.keys(r.tags)), ...(hasShape ? ['shape'] : [])]) for (const tagKey of tagKeys) { const valueOf = tagKey === 'shape' ? (r: BenchmarkRecord) => shapeSignature(r) ?? NONE_VALUE : (r: BenchmarkRecord) => tagFacetValue(r, tagKey) const distinct = new Set(records.map(valueOf)) // A single unambiguous value (and no missing records) is not filterable. if (distinct.size < 2) continue const label = tagKey === 'shape' ? 'shape' : tagKey.startsWith('shape.') ? `shape · ${tagKey.slice('shape.'.length)}` : tagKey.replace(/_/g, ' ') make(`tag:${tagKey}`, label, `tag:${tagKey}`, valueOf) } return groups } /** * Swap inverted date bounds (From > To). Keeps inputs valid as the user types. */ export function normalizeFilterDates(f: Filters): Filters { if (f.dateFrom && f.dateTo && f.dateFrom > f.dateTo) { return { ...f, dateFrom: f.dateTo, dateTo: f.dateFrom } } return f } /** * Drop filter values that no longer exist in the dataset (hot reload may * remove or replace files), so stale selections can never blank the view. * Returns the same object when nothing needed pruning. */ export function pruneFilters(f: Filters, records: BenchmarkRecord[]): Filters { const hostSet = new Set(records.map((r) => r.host)) const hwSet = new Set(records.map((r) => r.hardware)) const hosts = f.hosts.filter((h) => hostSet.has(h)) const hardware = f.hardware.filter((h) => hwSet.has(h)) const tags: Record = {} let tagsChanged = false for (const [key, values] of Object.entries(f.tags)) { const valid = new Set(records.map((r) => tagFacetValue(r, key))) const kept = (values ?? []).filter((v) => valid.has(v)) if (kept.length !== (values ?? []).length) tagsChanged = true tags[key] = kept } const changed = hosts.length !== f.hosts.length || hardware.length !== f.hardware.length || tagsChanged return changed ? normalizeFilterDates({ ...f, hosts, hardware, tags }) : normalizeFilterDates(f) } /** Drop group-by dimensions that vanished; always keep at least 'host'. */ export function pruneGroupBy(groupBy: string[], records: BenchmarkRecord[]): string[] { const tagKeys = new Set(records.flatMap((r) => Object.keys(r.tags))) const valid = (k: string) => k === 'host' || k === 'hardware' || tagKeys.has(k) const kept = groupBy.filter(valid) return kept.length > 0 ? kept : ['host'] } /** * Explain why a filter set matches zero records: every active constraint * group is listed with what it restricts ("date range … has no data", * "hosts: hf-trx50 only"), so the user sees exactly what blocks. Each entry * doubles as a one-click recovery target (clear that group). */ export function analyzeFilters(records: BenchmarkRecord[], f: Filters): FilterBlocker[] { const active: { scope: FilterScope; label: string }[] = [] if (f.dateFrom || f.dateTo) active.push({ scope: 'dates', label: 'dates' }) if (f.hosts.length > 0) active.push({ scope: 'hosts', label: 'hosts' }) if (f.hardware.length > 0) active.push({ scope: 'hardware', label: 'hardware' }) for (const [key, values] of Object.entries(f.tags)) { if (values && values.length > 0) active.push({ scope: `tag:${key}`, label: key.replace(/^shape\./, '').replace(/_/g, ' ') }) } const minDate = records[0]?.date ?? '' const maxDate = records[records.length - 1]?.date ?? '' const blockers: FilterBlocker[] = [] for (const { scope, label } of active) { // Records matching every OTHER group — used to say whether the range // itself has no data or just combines badly with the other selections. const pool = records.filter((r) => matchesFilters(r, f, scope)) let detail: string if (scope === 'dates') { const inRange = pool.filter((r) => (!f.dateFrom || r.date >= f.dateFrom) && (!f.dateTo || r.date <= f.dateTo)) detail = `date range ${f.dateFrom || minDate}..${f.dateTo || maxDate}${inRange.length === 0 ? ' has no data' : ''}` } else if (scope === 'hosts' || scope === 'hardware') { const sel = scope === 'hosts' ? f.hosts : f.hardware detail = `${label}: ${sel.join(', ')}${sel.length === 1 ? ' only' : ''}` } else { const values = f.tags[scope.slice('tag:'.length)] ?? [] detail = `${label}: ${values.join(', ')}${values.length === 1 ? ' only' : ''}` } blockers.push({ key: scope, label, detail }) } if (blockers.length === 0 && active.length > 0) { blockers.push({ key: 'all', label: 'all filters', detail: 'the active filters combine to exclude every run' }) } return blockers } /** All metric keys present in a record set, sorted for stable display. */ export function metricKeys(records: BenchmarkRecord[]): string[] { const keys = new Set() for (const r of records) for (const k of Object.keys(r.metrics)) keys.add(k) return [...keys].sort() } export interface SeriesPoint { date: string t: number y: number /** Commits of the runs averaged into this point (when tagged). */ commits?: string[] } export interface Series { name: string points: SeriesPoint[] } /** Latest vs. previous value of one metric within one series. */ export interface MetricChange { latest: number latestDate: string previous: number | null previousDate: string | null /** Signed percent change (latest vs. previous), null when there is no previous. */ deltaPct: number | null /** Commits behind the compared runs, when records carry a commit tag. */ latestCommits?: string[] previousCommits?: string[] } export interface TrendRow { /** Series name, from the active group-by dimensions. */ name: string /** Metric key -> latest vs. previous change. */ changes: Record } /** * One row per series (per groupBy combo) with the latest and previous value * of every metric — the change matrix for spotting regressions at a glance. */ export function buildTrends(records: BenchmarkRecord[], metrics: string[], groupBy: string[] = ['host']): TrendRow[] { const rows = new Map() for (const metric of metrics) { for (const s of buildSeries(records, metric, groupBy)) { const last = s.points[s.points.length - 1] const prev = s.points[s.points.length - 2] ?? null let row = rows.get(s.name) if (!row) rows.set(s.name, (row = { name: s.name, changes: {} })) row.changes[metric] = { latest: last.y, latestDate: last.date, previous: prev?.y ?? null, previousDate: prev?.date ?? null, deltaPct: prev && prev.y !== 0 ? ((last.y - prev.y) / prev.y) * 100 : null, ...(last.commits ? { latestCommits: last.commits } : {}), ...(prev?.commits ? { previousCommits: prev.commits } : {}), } } } return [...rows.values()] } /** Series display name from the selected group-by dimensions. */ function seriesName(r: BenchmarkRecord, groupBy: string[]): string { const parts: string[] = [] for (const key of groupBy) { if (key.startsWith('shape.')) { // Shape dims read best as labeled, compact pairs: "seq=4k". const value = r.tags[key] if (value) parts.push(`${key.slice('shape.'.length)}=${compactDim(value)}`) continue } const value = key === 'host' ? r.host : key === 'hardware' ? r.hardware : r.tags[key] // Long commit hashes read better abbreviated. const hash = key === 'commit' || key === 'rev' if (value) parts.push(hash && value.length > 12 ? value.slice(0, 8) : value) } return parts.length > 0 ? parts.join(' · ') : r.host } /** * Group records into time series for a given metric, one series per unique * combination of the groupBy dimensions ('host', 'hardware', or any tag key). * When several runs share series+date, the average is used. */ export function buildSeries(records: BenchmarkRecord[], metric: string, groupBy: string[] = ['host']): Series[] { const groups = new Map }>>() for (const r of records) { const y = r.metrics[metric] if (y === undefined) continue const name = seriesName(r, groupBy) if (!groups.has(name)) groups.set(name, new Map()) const byDate = groups.get(name)! if (!byDate.has(r.date)) byDate.set(r.date, { ys: [], commits: new Set() }) const bucket = byDate.get(r.date)! bucket.ys.push(y) const commit = r.tags.commit ?? r.tags.rev if (commit) bucket.commits.add(commit) } return [...groups.entries()] .map(([name, byDate]) => ({ name, points: [...byDate.entries()] .map(([date, b]) => ({ date, t: new Date(`${date}T00:00:00Z`).getTime(), y: b.ys.reduce((a, c) => a + c, 0) / b.ys.length, ...(b.commits.size > 0 ? { commits: [...b.commits] } : {}), })) .sort((a, b) => a.t - b.t), })) .sort((a, b) => a.name.localeCompare(b.name)) }