import { loadData, filterResults, selectBestResults, expandCpuRows, attachCpuBaselineFromCpuRecords, mergeDepthPairs } from './data.js';
import { initFilters, populateQuantOptions, getFilters, resetFilters } from './filters.js';
import { renderDecodeChart, renderPrefillChart, renderSizeChart, renderMachineChart, renderCpuGpuChart, renderSpeedupChart } from './charts.js';
import { renderResultsTable, renderErrorTable, renderMachineInfo, renderCpuGpuTable } from './tables.js';
let appData = null;
async function init() {
try {
appData = await loadData();
} catch (e) {
const loading = document.getElementById('loading');
loading.className = 'loading-state';
loading.innerHTML = `
Failed to load data
Run: node scripts/build-site.js
`;
return;
}
// Hide loading, show dashboard with entrance animation
const loading = document.getElementById('loading');
const dashboard = document.getElementById('dashboard');
loading.style.display = 'none';
dashboard.style.display = '';
requestAnimationFrame(() => dashboard.classList.add('animate-in'));
// Populate quant options from actual data
populateQuantOptions(appData.results);
// Surface the dataset's last-updated time so users know data freshness.
renderHeroMeta(appData);
// Init filter dropdowns
initFilters(appData.meta, () => render());
// Wire theme toggle
document.getElementById('theme-toggle')?.addEventListener('click', () => {
const next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
if (appData) render();
});
// Wire reset button
const resetBtn = document.getElementById('filter-reset');
if (resetBtn) {
resetBtn.addEventListener('click', () => {
resetFilters();
render();
});
}
// Wire metric selector for CPU vs GPU section
const metricSelect = document.getElementById('cpu-gpu-metric');
if (metricSelect) {
metricSelect.addEventListener('change', () => render());
}
// Init section navigation
initSectionNav();
// Initial render
render();
}
function render() {
// Sync Chart.js defaults with current theme
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
Chart.defaults.color = isDark ? '#a1a1aa' : '#71717a';
Chart.defaults.plugins.tooltip.backgroundColor = isDark ? 'rgba(15,15,18,0.95)' : 'rgba(255,255,255,0.95)';
Chart.defaults.plugins.tooltip.borderColor = isDark ? '#27272a' : '#e4e4e7';
Chart.defaults.plugins.tooltip.titleColor = isDark ? '#e4e4e7' : '#09090b';
Chart.defaults.plugins.tooltip.bodyColor = isDark ? '#a1a1aa' : '#71717a';
const filters = getFilters();
// Filter, attach CPU baseline values (folds CLI-flow CPU records onto
// their GPU sibling so both submission paths produce one row per cell),
// fold the (d=0, d=N) study pair into a single GPU row carrying both
// depths, collapse to one canonical row per (machine, browser, model,
// variant, backend), then drop the now-redundant CPU rows. CPU numbers
// stay visible via the cpu_baseline_* columns on each GPU row.
const filtered = selectBestResults(
mergeDepthPairs(
attachCpuBaselineFromCpuRecords(filterResults(appData.results, filters)),
),
).filter(r => r.nGpuLayers !== 0);
// Summary cards — counts tween from previous value to new on filter changes
// and from 0 on first paint (since `data-value` defaults to "0").
const passed = filtered.filter(r => r.status === 'done');
animateCount(document.getElementById('stat-machines'), appData.meta.machines.length, { decimals: 0 });
animateCount(document.getElementById('stat-benchmarks'), filtered.length, { decimals: 0 });
const passRate = filtered.length > 0 ? (passed.length / filtered.length) * 100 : 0;
animateCount(document.getElementById('stat-pass-rate'), passRate, { decimals: 0 });
const decodeVals = passed.map(r => r.decode_tok_s).filter(v => v != null);
const bestDecode = decodeVals.length ? Math.max(...decodeVals) : 0;
animateCount(document.getElementById('stat-best-decode'), bestDecode, { decimals: 1 });
const sizes = passed.map(r => r.sizeMB).filter(v => v != null);
const largest = sizes.length ? Math.max(...sizes) : 0;
animateCount(document.getElementById('stat-largest'), largest, { decimals: 0 });
// Results count
const countEl = document.getElementById('results-count');
if (countEl) {
const total = appData.results.length;
countEl.textContent = filtered.length === total
? `${total} total`
: `${filtered.length} of ${total}`;
}
// Reset button — only present when at least one filter is active. Hiding
// (rather than disabling) removes a permanent ghost button from the bar
// and makes the appearance signal "you can undo your filter."
const resetBtn = document.getElementById('filter-reset');
if (resetBtn) {
const activeCount = (filters.machine !== 'all' ? 1 : 0) + (filters.browser !== 'all' ? 1 : 0) +
(filters.model !== 'all' ? 1 : 0) + (filters.backend !== 'all' ? 1 : 0) +
(filters.status !== 'all' ? 1 : 0) + (filters.quants.size > 0 ? 1 : 0);
resetBtn.disabled = activeCount === 0;
resetBtn.hidden = activeCount === 0;
const label = resetBtn.querySelector('.filter-reset-label') || resetBtn;
if (label !== resetBtn) {
label.textContent = activeCount ? `Reset (${activeCount})` : 'Reset';
}
}
// Tables
renderResultsTable(filtered);
renderErrorTable(filtered);
renderMachineInfo(appData.meta.machines);
// Charts
renderDecodeChart(filtered);
renderPrefillChart(filtered);
renderSizeChart(filtered);
renderMachineChart(filtered, appData.meta.machines);
// CPU vs GPU comparison
const metric = document.getElementById('cpu-gpu-metric')?.value || 'decode_tok_s';
renderCpuGpuSection(filtered, metric);
}
/* Consolidate the 3-part CPU-vs-GPU block (two charts + table). When there
is no CPU baseline or no overlapping GPU data, render a single inline
empty state and hide the charts+table so the user doesn't see the same
message repeated three times. */
function renderCpuGpuSection(filtered, metric) {
const chartsGrid = document.querySelector('#performance-section .charts-grid:nth-of-type(2)');
const table = document.getElementById('cpu-gpu-table');
const passed = filtered.filter(r => r.status === 'done');
// Same expansion the chart/table renderers do — see expandCpuRows().
const cpuResults = expandCpuRows(passed);
const gpuResults = passed.filter(r => r.nGpuLayers !== 0);
if (!chartsGrid || !table) {
renderCpuGpuChart(filtered, metric);
renderSpeedupChart(filtered, metric);
renderCpuGpuTable(filtered);
return;
}
if (cpuResults.length === 0 || gpuResults.length === 0) {
chartsGrid.hidden = true;
const reason = cpuResults.length === 0
? 'No CPU baseline in the current filter. Select "All Backends" or enable CPU baselines when benchmarking with