Spaces:
Running
Running
File size: 13,269 Bytes
e2ac5c3 4721a6e 2dc46fb 4721a6e 2dc46fb 4721a6e 1683f65 e2ac5c3 e6a49d5 e2ac5c3 1683f65 4721a6e df975ba 4721a6e df975ba 4721a6e df975ba 4721a6e 2dc46fb df975ba 2dc46fb 4721a6e 2dc46fb ecef386 2dc46fb 4721a6e 2dc46fb df975ba 2dc46fb df975ba 5047636 e2ac5c3 5047636 df975ba 2dc46fb df975ba ed5d4b6 df975ba 2dc46fb 4721a6e df975ba 4721a6e 2dc46fb 4721a6e 2dc46fb 4721a6e df975ba 2dc46fb 4721a6e 2dc46fb df975ba 2dc46fb df975ba 2dc46fb df975ba 4721a6e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 | 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 = `
<div class="loading-content">
<p class="loading-error">Failed to load data</p>
<p class="loading-hint">Run: <code>node scripts/build-site.js</code></p>
</div>
`;
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 <code>--consistency</code>.'
: 'No WebGPU runs in the current filter. Adjust the Backend filter to include WebGPU.';
table.innerHTML = `<div class="empty-state"><p>${reason}</p></div>`;
return;
}
chartsGrid.hidden = false;
renderCpuGpuChart(filtered, metric);
renderSpeedupChart(filtered, metric);
renderCpuGpuTable(filtered);
}
function renderHeroMeta(data) {
const el = document.getElementById('hero-meta');
const liveEl = document.getElementById('hero-live');
const liveText = document.getElementById('hero-live-text');
const generated = data?.meta?.generatedAt;
const machineCount = data?.meta?.machines?.length || 0;
const resultCount = data?.results?.length || 0;
if (el) {
const parts = [];
if (machineCount > 0) parts.push(`${machineCount} machine${machineCount === 1 ? '' : 's'}`);
if (resultCount > 0) parts.push(`${resultCount} benchmark${resultCount === 1 ? '' : 's'}`);
if (generated) parts.push(`updated ${formatRelativeTime(new Date(generated))}`);
if (parts.length > 0) {
el.textContent = parts.join(' · ');
el.hidden = false;
if (generated) el.title = new Date(generated).toLocaleString();
}
}
if (liveEl && liveText && generated) {
liveText.textContent = `Live · ${formatRelativeTime(new Date(generated))}`;
liveEl.hidden = false;
}
// Hero stat: top decode tok/s with machine + model context. Uses the
// canonical set (best per cell) so a noisy 1-iteration outlier can't
// hijack the headline number. Depth-merge first so a Study cell counts
// once at its d=N number, not twice.
const canonical = selectBestResults(mergeDepthPairs(data?.results || []));
const passed = canonical.filter(r => r.status === 'done' && r.decode_tok_s != null);
const heroStatEl = document.getElementById('hero-stat');
const heroNumEl = document.getElementById('hero-top-decode');
const heroMetaEl = document.getElementById('hero-top-meta');
if (heroStatEl && heroNumEl && heroMetaEl && passed.length > 0) {
const top = passed.reduce((a, b) => (a.decode_tok_s > b.decode_tok_s ? a : b));
heroStatEl.hidden = false;
heroMetaEl.textContent = `${top.machineSlug || top.machine || '—'} · ${top.model || ''} ${top.variant || ''}`.trim();
animateCount(heroNumEl, top.decode_tok_s, { decimals: 1, duration: 800 });
}
}
/* Tween numeric content from 0 to a target. CSS-only via @property would
need server-side @property registration to work in older Safari; keep
this 12-line JS tween for predictability. */
function animateCount(el, target, { decimals = 0, duration = 600 } = {}) {
if (!el) return;
const start = parseFloat(el.dataset.value || '0') || 0;
const end = Number(target) || 0;
if (start === end) {
el.textContent = end.toFixed(decimals);
return;
}
const startTime = performance.now();
const ease = (t) => 1 - Math.pow(1 - t, 3);
function step(now) {
const t = Math.min(1, (now - startTime) / duration);
const v = start + (end - start) * ease(t);
el.textContent = v.toFixed(decimals);
if (t < 1) requestAnimationFrame(step);
else el.dataset.value = String(end);
}
requestAnimationFrame(step);
}
function formatRelativeTime(date) {
const now = Date.now();
const diff = Math.max(0, now - date.getTime());
const min = 60_000, hr = 60 * min, day = 24 * hr;
if (diff < min) return 'just now';
if (diff < hr) return `${Math.floor(diff / min)} min ago`;
if (diff < day) return `${Math.floor(diff / hr)} h ago`;
const days = Math.floor(diff / day);
if (days < 30) return `${days} day${days === 1 ? '' : 's'} ago`;
return date.toISOString().slice(0, 10);
}
function initSectionNav() {
const nav = document.getElementById('section-nav');
if (!nav) return;
const track = nav.querySelector('.section-nav-track');
const buttons = nav.querySelectorAll('.section-nav-item');
const sections = [];
// Prefer the sticky wrapper height so the jumped-to section isn't
// obscured by the sticky head.
const stickyHead = document.querySelector('.sticky-head') || nav;
buttons.forEach(btn => {
const sectionId = btn.dataset.section;
const section = document.getElementById(sectionId);
if (section) sections.push({ btn, section });
btn.addEventListener('click', (e) => {
e.preventDefault();
if (section) {
const offset = stickyHead.offsetHeight + 8;
const top = section.getBoundingClientRect().top + window.scrollY - offset;
window.scrollTo({ top, behavior: 'smooth' });
}
});
});
// Drive the sliding indicator from the active button's geometry. Track
// is the positioned ancestor; offsetLeft/offsetWidth are relative to it.
const moveIndicator = (btn) => {
if (!track || !btn) return;
track.style.setProperty('--indicator-x', `${btn.offsetLeft}px`);
track.style.setProperty('--indicator-w', `${btn.offsetWidth}px`);
};
// Scroll spy: instead of IntersectionObserver (which fires inconsistently
// when multiple sections overlap the observer band), compute the
// currently-active section on scroll by comparing each section's top to
// the bottom of the sticky head. Cheaper and predictable.
if (sections.length === 0) return;
let ticking = false;
const updateActive = () => {
const anchor = stickyHead.offsetHeight + 16;
let active = sections[0];
for (const entry of sections) {
const top = entry.section.getBoundingClientRect().top;
if (top - anchor <= 0) active = entry;
else break;
}
buttons.forEach(b => b.classList.toggle('active', b === active.btn));
moveIndicator(active.btn);
};
const onScroll = () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => { updateActive(); ticking = false; });
};
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
updateActive();
// Re-measure once fonts settle — Bricolage Grotesque shifts widths.
document.fonts?.ready?.then(() => updateActive()).catch(() => {});
}
init();
|