Spaces:
Running
Running
GitHub Actions commited on
Commit ·
bc8e1d3
1
Parent(s): f221926
sync from abhijitramesh/webgpu-bench@f3ee9fc069
Browse files- build/asyncify/bench.js +0 -0
- build/asyncify/bench.wasm +2 -2
- build/asyncify/build-info.json +1 -1
- build/jspi/bench.js +0 -0
- build/jspi/bench.wasm +2 -2
- build/jspi/build-info.json +1 -1
- css/style.css +44 -0
- js/run/bench-worker.js +16 -0
- js/run/controller.js +132 -0
- run.html +31 -0
build/asyncify/bench.js
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
build/asyncify/bench.wasm
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1ba4381b1a8c3a34d003bae0837e684515174b8c1e24b470b7013eedafc359e4
|
| 3 |
+
size 5235854
|
build/asyncify/build-info.json
CHANGED
|
@@ -2,5 +2,5 @@
|
|
| 2 |
"llamaCppCommit": "f22c8021d213567942a3d0134692e70f02f28f3a",
|
| 3 |
"llamaCppDescribe": "b8981-3-gf22c8021d",
|
| 4 |
"dawnTag": "v20260317.182325",
|
| 5 |
-
"builtAt": "2026-
|
| 6 |
}
|
|
|
|
| 2 |
"llamaCppCommit": "f22c8021d213567942a3d0134692e70f02f28f3a",
|
| 3 |
"llamaCppDescribe": "b8981-3-gf22c8021d",
|
| 4 |
"dawnTag": "v20260317.182325",
|
| 5 |
+
"builtAt": "2026-05-01T08:36:58Z"
|
| 6 |
}
|
build/jspi/bench.js
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
build/jspi/bench.wasm
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:39eea46a9d02b044c9d143cf8243c3e05f8bf89d94bb5bcd804b6e43755b958d
|
| 3 |
+
size 3614207
|
build/jspi/build-info.json
CHANGED
|
@@ -2,5 +2,5 @@
|
|
| 2 |
"llamaCppCommit": "f22c8021d213567942a3d0134692e70f02f28f3a",
|
| 3 |
"llamaCppDescribe": "b8981-3-gf22c8021d",
|
| 4 |
"dawnTag": "v20260317.182325",
|
| 5 |
-
"builtAt": "2026-
|
| 6 |
}
|
|
|
|
| 2 |
"llamaCppCommit": "f22c8021d213567942a3d0134692e70f02f28f3a",
|
| 3 |
"llamaCppDescribe": "b8981-3-gf22c8021d",
|
| 4 |
"dawnTag": "v20260317.182325",
|
| 5 |
+
"builtAt": "2026-05-01T08:33:19Z"
|
| 6 |
}
|
css/style.css
CHANGED
|
@@ -1723,6 +1723,50 @@ a:hover { color: var(--info); }
|
|
| 1723 |
.run-device-row-value { color: var(--foreground); font-weight: 600; }
|
| 1724 |
.run-device-note { font-size: 11px; color: var(--foreground-muted); margin-top: 2px; }
|
| 1725 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1726 |
/* Hide filters + iterations + action buttons — stacks with .filter-bar tokens. */
|
| 1727 |
.run-filters { align-items: center; }
|
| 1728 |
.run-filters-checks {
|
|
|
|
| 1723 |
.run-device-row-value { color: var(--foreground); font-weight: 600; }
|
| 1724 |
.run-device-note { font-size: 11px; color: var(--foreground-muted); margin-top: 2px; }
|
| 1725 |
|
| 1726 |
+
/* User-reported machine card — appears between the auto-detected device
|
| 1727 |
+
stats and the filter bar. <details>/<summary> so it can collapse once
|
| 1728 |
+
the user has filled it in. */
|
| 1729 |
+
.run-machine-card { margin-bottom: 24px; }
|
| 1730 |
+
.run-machine-card > summary { cursor: pointer; }
|
| 1731 |
+
.run-machine-card > summary > strong { font-weight: 600; }
|
| 1732 |
+
.run-machine-grid {
|
| 1733 |
+
display: grid;
|
| 1734 |
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
| 1735 |
+
gap: 12px;
|
| 1736 |
+
margin-top: 12px;
|
| 1737 |
+
}
|
| 1738 |
+
.run-machine-field { display: flex; flex-direction: column; gap: 6px; }
|
| 1739 |
+
.run-machine-label {
|
| 1740 |
+
font-size: 12px;
|
| 1741 |
+
color: var(--foreground-muted);
|
| 1742 |
+
font-weight: 500;
|
| 1743 |
+
}
|
| 1744 |
+
.run-machine-req { color: var(--danger, #d04545); font-weight: 700; }
|
| 1745 |
+
.run-machine-opt { color: var(--foreground-muted); font-weight: 400; }
|
| 1746 |
+
.run-machine-input {
|
| 1747 |
+
height: 34px;
|
| 1748 |
+
padding: 0 10px;
|
| 1749 |
+
background: var(--surface-1);
|
| 1750 |
+
border: 1px solid var(--border);
|
| 1751 |
+
border-radius: var(--radius-md);
|
| 1752 |
+
color: var(--foreground);
|
| 1753 |
+
font-family: var(--font-mono);
|
| 1754 |
+
font-size: 13px;
|
| 1755 |
+
}
|
| 1756 |
+
.run-machine-input:focus {
|
| 1757 |
+
outline: none;
|
| 1758 |
+
border-color: var(--brand, #2b6df0);
|
| 1759 |
+
}
|
| 1760 |
+
.run-machine-input.is-missing {
|
| 1761 |
+
border-color: var(--danger, #d04545);
|
| 1762 |
+
}
|
| 1763 |
+
.run-machine-hint {
|
| 1764 |
+
margin-top: 10px;
|
| 1765 |
+
font-size: 11px;
|
| 1766 |
+
color: var(--foreground-muted);
|
| 1767 |
+
}
|
| 1768 |
+
.run-machine-hint.is-warn { color: var(--danger, #d04545); }
|
| 1769 |
+
|
| 1770 |
/* Hide filters + iterations + action buttons — stacks with .filter-bar tokens. */
|
| 1771 |
.run-filters { align-items: center; }
|
| 1772 |
.run-filters-checks {
|
js/run/bench-worker.js
CHANGED
|
@@ -340,6 +340,22 @@ async function runOne({ params, opfsPath }) {
|
|
| 340 |
if (loadResult !== 0) throw new Error(`bench_load failed: ${loadResult}`);
|
| 341 |
log('Model loaded');
|
| 342 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
// ─── Consistency phase ───
|
| 344 |
// Soft-fail: a failure here logs and falls through to the perf phase
|
| 345 |
// rather than aborting the whole run. Some devices/models can't survive
|
|
|
|
| 340 |
if (loadResult !== 0) throw new Error(`bench_load failed: ${loadResult}`);
|
| 341 |
log('Model loaded');
|
| 342 |
|
| 343 |
+
// ─── Memory snapshot from llama.cpp ───
|
| 344 |
+
// Captured immediately after bench_load so model_size reflects the loaded
|
| 345 |
+
// model and per-device free counters reflect post-allocation state. Wrapped
|
| 346 |
+
// in try/catch — if the C side or a backend errors, the run can still
|
| 347 |
+
// produce perf numbers, just without memoryInfo on the record.
|
| 348 |
+
try {
|
| 349 |
+
const raw = await Module.ccall('bench_memory_info', 'string', [], [], { async: true });
|
| 350 |
+
result.memoryInfo = parseBenchResult('bench_memory_info', raw);
|
| 351 |
+
const dev = (result.memoryInfo.devices || [])
|
| 352 |
+
.map(d => `${d.name}(${d.type}) free=${(d.free / (1024 * 1024)).toFixed(0)}MB total=${(d.total / (1024 * 1024)).toFixed(0)}MB`)
|
| 353 |
+
.join(' | ') || 'none';
|
| 354 |
+
log(`Memory: model=${(result.memoryInfo.model_size / (1024 * 1024)).toFixed(0)}MB state=${(result.memoryInfo.state_size / (1024 * 1024)).toFixed(0)}MB | ${dev}`);
|
| 355 |
+
} catch (err) {
|
| 356 |
+
log(`bench_memory_info failed: ${err.message} — continuing without memoryInfo`);
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
// ─── Consistency phase ───
|
| 360 |
// Soft-fail: a failure here logs and falls through to the perf phase
|
| 361 |
// rather than aborting the whole run. Some devices/models can't survive
|
js/run/controller.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
| 12 |
import { isHubConfigured, HF_DATASET_REPO, CONSISTENCY_PROMPT } from './config.js';
|
| 13 |
|
| 14 |
const RUN_INTENT_STORAGE_KEY = 'webgpu-bench:runIntent';
|
|
|
|
| 15 |
const CRASH_STALE_MS = 10_000;
|
| 16 |
|
| 17 |
const DEFAULT_N_PREDICT = 128;
|
|
@@ -62,8 +63,33 @@ const state = {
|
|
| 62 |
// versions. JSPI and Asyncify variants are built from the same source
|
| 63 |
// tree, so a single fetch is enough; both files would be identical.
|
| 64 |
buildInfo: null,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
};
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
// Register an abort callback for an in-flight async op (worker terminate,
|
| 68 |
// fetch signal abort, etc.). Returns an unregister fn the caller MUST
|
| 69 |
// invoke when the op settles, so we don't accumulate stale handlers across
|
|
@@ -943,6 +969,91 @@ function slugify(s) {
|
|
| 943 |
return String(s).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'unknown';
|
| 944 |
}
|
| 945 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 946 |
async function machineInfo() {
|
| 947 |
const ua = navigator.userAgent;
|
| 948 |
const platform = /Mac/.test(ua) ? 'darwin'
|
|
@@ -1511,6 +1622,15 @@ function makeRecord(v, vr, machine, browser, wallTimeMs) {
|
|
| 1511 |
cpu_baseline: cpuBaseline,
|
| 1512 |
output: gpu?.output || '',
|
| 1513 |
machine,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1514 |
source: `webgpu-bench/site (${state.surface})`,
|
| 1515 |
};
|
| 1516 |
}
|
|
@@ -1754,6 +1874,17 @@ function wireHubHandlers() {
|
|
| 1754 |
if (!state.hfSession) return;
|
| 1755 |
const eligible = submittableResults();
|
| 1756 |
if (eligible.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1757 |
submitBtn.disabled = true;
|
| 1758 |
const original = submitBtn.textContent;
|
| 1759 |
submitBtn.textContent = 'Submitting…';
|
|
@@ -1845,6 +1976,7 @@ export async function mountRunSection() {
|
|
| 1845 |
wirePurgeHandler();
|
| 1846 |
wireHubHandlers();
|
| 1847 |
wireOutputHandlers();
|
|
|
|
| 1848 |
// Restore the last completed run from localStorage so it survives a page
|
| 1849 |
// reload — including the OAuth redirect taking the user to HF and back.
|
| 1850 |
// Must run before updateButtons/renderOutput/hideProgress so they pick up
|
|
|
|
| 12 |
import { isHubConfigured, HF_DATASET_REPO, CONSISTENCY_PROMPT } from './config.js';
|
| 13 |
|
| 14 |
const RUN_INTENT_STORAGE_KEY = 'webgpu-bench:runIntent';
|
| 15 |
+
const USER_REPORTED_STORAGE_KEY = 'webgpu-bench:userReported';
|
| 16 |
const CRASH_STALE_MS = 10_000;
|
| 17 |
|
| 18 |
const DEFAULT_N_PREDICT = 128;
|
|
|
|
| 63 |
// versions. JSPI and Asyncify variants are built from the same source
|
| 64 |
// tree, so a single fetch is enough; both files would be identical.
|
| 65 |
buildInfo: null,
|
| 66 |
+
// User-reported machine identity (Machine Name / GPU Name / Browser /
|
| 67 |
+
// OS). Filled by the "Your machine" form on the Run page, persisted to
|
| 68 |
+
// localStorage between visits, and stamped onto every result record so
|
| 69 |
+
// the leaderboard can attribute submissions even when UA / WebGPU
|
| 70 |
+
// adapter info is missing or wrong. machineName/browser/os are required
|
| 71 |
+
// before submission; gpuName is optional.
|
| 72 |
+
userReported: { machineName: '', gpuName: '', browser: '', os: '' },
|
| 73 |
};
|
| 74 |
|
| 75 |
+
const USER_REPORTED_REQUIRED = ['machineName', 'browser', 'os'];
|
| 76 |
+
|
| 77 |
+
function loadUserReported() {
|
| 78 |
+
try {
|
| 79 |
+
const raw = localStorage.getItem(USER_REPORTED_STORAGE_KEY);
|
| 80 |
+
if (!raw) return null;
|
| 81 |
+
const parsed = JSON.parse(raw);
|
| 82 |
+
if (parsed && typeof parsed === 'object') return parsed;
|
| 83 |
+
} catch { /* corrupt storage */ }
|
| 84 |
+
return null;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
function saveUserReported() {
|
| 88 |
+
try {
|
| 89 |
+
localStorage.setItem(USER_REPORTED_STORAGE_KEY, JSON.stringify(state.userReported));
|
| 90 |
+
} catch { /* quota / disabled */ }
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
// Register an abort callback for an in-flight async op (worker terminate,
|
| 94 |
// fetch signal abort, etc.). Returns an unregister fn the caller MUST
|
| 95 |
// invoke when the op settles, so we don't accumulate stale handlers across
|
|
|
|
| 969 |
return String(s).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'unknown';
|
| 970 |
}
|
| 971 |
|
| 972 |
+
// ──────────────── user-reported submission fields ────────────────
|
| 973 |
+
|
| 974 |
+
// Best-effort default for the four user-reported inputs, derived from the
|
| 975 |
+
// auto-detected device + browser data. The user is expected to edit these
|
| 976 |
+
// before running — defaults exist only so the form isn't empty on first
|
| 977 |
+
// visit. Returns { machineName, gpuName, browser, os }.
|
| 978 |
+
function autoDetectedUserReported() {
|
| 979 |
+
const d = state.device || {};
|
| 980 |
+
const gpu = d.gpu || {};
|
| 981 |
+
const gpuStr = [gpu.vendor, gpu.architecture, gpu.device, gpu.description]
|
| 982 |
+
.filter(Boolean).join(' ').trim();
|
| 983 |
+
const memGB = state.budget?.memGB;
|
| 984 |
+
const browser = formatBrowser(d);
|
| 985 |
+
const os = formatPlatform(d);
|
| 986 |
+
// machineName default: "<gpu> · <memGB> GB" if both known, else either,
|
| 987 |
+
// else the OS string. The user is encouraged to replace with a friendly
|
| 988 |
+
// label like "MacBook Pro M3 16GB".
|
| 989 |
+
let machineName = '';
|
| 990 |
+
if (gpuStr && memGB) machineName = `${gpuStr} · ${memGB} GB`;
|
| 991 |
+
else if (gpuStr) machineName = gpuStr;
|
| 992 |
+
else if (memGB) machineName = `${memGB} GB device`;
|
| 993 |
+
else machineName = os;
|
| 994 |
+
return { machineName, gpuName: gpuStr, browser, os };
|
| 995 |
+
}
|
| 996 |
+
|
| 997 |
+
function readUserReportedFromInputs() {
|
| 998 |
+
return {
|
| 999 |
+
machineName: ($('ur-machine-name')?.value ?? '').trim(),
|
| 1000 |
+
gpuName: ($('ur-gpu-name')?.value ?? '').trim(),
|
| 1001 |
+
browser: ($('ur-browser')?.value ?? '').trim(),
|
| 1002 |
+
os: ($('ur-os')?.value ?? '').trim(),
|
| 1003 |
+
};
|
| 1004 |
+
}
|
| 1005 |
+
|
| 1006 |
+
function refreshUserReportedValidation() {
|
| 1007 |
+
const hint = $('ur-hint');
|
| 1008 |
+
const missing = USER_REPORTED_REQUIRED.filter(k => !state.userReported[k]);
|
| 1009 |
+
for (const k of USER_REPORTED_REQUIRED) {
|
| 1010 |
+
const id = { machineName: 'ur-machine-name', browser: 'ur-browser', os: 'ur-os' }[k];
|
| 1011 |
+
const el = $(id);
|
| 1012 |
+
if (el) el.classList.toggle('is-missing', !state.userReported[k]);
|
| 1013 |
+
}
|
| 1014 |
+
if (hint) {
|
| 1015 |
+
if (missing.length === 0) {
|
| 1016 |
+
hint.textContent = 'Looks good — these labels will be attached to every result you submit.';
|
| 1017 |
+
hint.classList.remove('is-warn');
|
| 1018 |
+
} else {
|
| 1019 |
+
hint.textContent = `Required: ${missing.join(', ')}. We'll still let you run, but submissions need these filled in.`;
|
| 1020 |
+
hint.classList.add('is-warn');
|
| 1021 |
+
}
|
| 1022 |
+
}
|
| 1023 |
+
}
|
| 1024 |
+
|
| 1025 |
+
function wireUserReported() {
|
| 1026 |
+
// Pre-fill: stored values win, fall back to auto-detected defaults so
|
| 1027 |
+
// first-time users see something rather than an empty form.
|
| 1028 |
+
const stored = loadUserReported();
|
| 1029 |
+
const auto = autoDetectedUserReported();
|
| 1030 |
+
state.userReported = {
|
| 1031 |
+
machineName: stored?.machineName?.trim() || auto.machineName,
|
| 1032 |
+
gpuName: stored?.gpuName?.trim() || auto.gpuName,
|
| 1033 |
+
browser: stored?.browser?.trim() || auto.browser,
|
| 1034 |
+
os: stored?.os?.trim() || auto.os,
|
| 1035 |
+
};
|
| 1036 |
+
for (const [id, key] of [
|
| 1037 |
+
['ur-machine-name', 'machineName'],
|
| 1038 |
+
['ur-gpu-name', 'gpuName'],
|
| 1039 |
+
['ur-browser', 'browser'],
|
| 1040 |
+
['ur-os', 'os'],
|
| 1041 |
+
]) {
|
| 1042 |
+
const el = $(id);
|
| 1043 |
+
if (!el) continue;
|
| 1044 |
+
el.value = state.userReported[key] || '';
|
| 1045 |
+
el.addEventListener('input', () => {
|
| 1046 |
+
state.userReported = readUserReportedFromInputs();
|
| 1047 |
+
saveUserReported();
|
| 1048 |
+
refreshUserReportedValidation();
|
| 1049 |
+
});
|
| 1050 |
+
}
|
| 1051 |
+
// Persist whatever the auto-detect filled in so the user doesn't lose
|
| 1052 |
+
// it on reload before they touch anything.
|
| 1053 |
+
saveUserReported();
|
| 1054 |
+
refreshUserReportedValidation();
|
| 1055 |
+
}
|
| 1056 |
+
|
| 1057 |
async function machineInfo() {
|
| 1058 |
const ua = navigator.userAgent;
|
| 1059 |
const platform = /Mac/.test(ua) ? 'darwin'
|
|
|
|
| 1622 |
cpu_baseline: cpuBaseline,
|
| 1623 |
output: gpu?.output || '',
|
| 1624 |
machine,
|
| 1625 |
+
// Memory snapshot llama.cpp captured immediately after bench_load —
|
| 1626 |
+
// model_size, state_size, and per-device {free,total} from every ggml
|
| 1627 |
+
// backend. Useful for spotting memory-pressured runs and for sanity-
|
| 1628 |
+
// checking GPU memory headroom across machines.
|
| 1629 |
+
memoryInfo: gpu?.memoryInfo ?? null,
|
| 1630 |
+
// User-typed labels that override (or supplement) the auto-detected
|
| 1631 |
+
// machine/browser fields. Auto-detection is unreliable across UA-string
|
| 1632 |
+
// anonymization, deviceMemory rounding, and missing WebGPU adapter info.
|
| 1633 |
+
userReported: { ...state.userReported },
|
| 1634 |
source: `webgpu-bench/site (${state.surface})`,
|
| 1635 |
};
|
| 1636 |
}
|
|
|
|
| 1874 |
if (!state.hfSession) return;
|
| 1875 |
const eligible = submittableResults();
|
| 1876 |
if (eligible.length === 0) return;
|
| 1877 |
+
// Required user-reported fields gate the submission so the leaderboard
|
| 1878 |
+
// doesn't accumulate anonymous rows. The Run buttons stay enabled
|
| 1879 |
+
// even when these are blank — we only block at submit time.
|
| 1880 |
+
const missing = USER_REPORTED_REQUIRED.filter(k => !state.userReported[k]);
|
| 1881 |
+
if (missing.length > 0) {
|
| 1882 |
+
const card = $('user-reported-card');
|
| 1883 |
+
if (card) { card.open = true; card.scrollIntoView({ behavior: 'smooth', block: 'center' }); }
|
| 1884 |
+
refreshUserReportedValidation();
|
| 1885 |
+
logLine(`Submit blocked: fill in ${missing.join(', ')} in "Your machine".`);
|
| 1886 |
+
return;
|
| 1887 |
+
}
|
| 1888 |
submitBtn.disabled = true;
|
| 1889 |
const original = submitBtn.textContent;
|
| 1890 |
submitBtn.textContent = 'Submitting…';
|
|
|
|
| 1976 |
wirePurgeHandler();
|
| 1977 |
wireHubHandlers();
|
| 1978 |
wireOutputHandlers();
|
| 1979 |
+
wireUserReported();
|
| 1980 |
// Restore the last completed run from localStorage so it survives a page
|
| 1981 |
// reload — including the OAuth redirect taking the user to HF and back.
|
| 1982 |
// Must run before updateButtons/renderOutput/hideProgress so they pick up
|
run.html
CHANGED
|
@@ -166,6 +166,37 @@
|
|
| 166 |
</div>
|
| 167 |
</div>
|
| 168 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
<!-- Hide filters, iterations, actions -->
|
| 170 |
<div class="filter-bar run-controls">
|
| 171 |
<div class="filter-bar-inner run-filters">
|
|
|
|
| 166 |
</div>
|
| 167 |
</div>
|
| 168 |
|
| 169 |
+
<!-- User-reported machine identity. The auto-detected values in the
|
| 170 |
+
cards above are unreliable (UA strings lie, deviceMemory is
|
| 171 |
+
coarse, GPU adapter info is often empty). We ship these
|
| 172 |
+
user-typed fields alongside the auto-detected ones so the
|
| 173 |
+
leaderboard can attribute submissions correctly. Persisted to
|
| 174 |
+
localStorage between visits. -->
|
| 175 |
+
<details class="card run-machine-card" id="user-reported-card" open>
|
| 176 |
+
<summary>
|
| 177 |
+
<strong>Your machine</strong> — labels the auto-detected device data on submission. Saved between visits.
|
| 178 |
+
</summary>
|
| 179 |
+
<div class="run-machine-grid">
|
| 180 |
+
<label class="run-machine-field">
|
| 181 |
+
<span class="run-machine-label">Machine name <span class="run-machine-req" aria-hidden="true">*</span></span>
|
| 182 |
+
<input type="text" id="ur-machine-name" class="run-machine-input" placeholder="e.g. MacBook Pro M3 16GB" autocomplete="off" spellcheck="false">
|
| 183 |
+
</label>
|
| 184 |
+
<label class="run-machine-field">
|
| 185 |
+
<span class="run-machine-label">GPU name <span class="run-machine-opt">(optional)</span></span>
|
| 186 |
+
<input type="text" id="ur-gpu-name" class="run-machine-input" placeholder="e.g. Apple M3 Pro" autocomplete="off" spellcheck="false">
|
| 187 |
+
</label>
|
| 188 |
+
<label class="run-machine-field">
|
| 189 |
+
<span class="run-machine-label">Browser <span class="run-machine-req" aria-hidden="true">*</span></span>
|
| 190 |
+
<input type="text" id="ur-browser" class="run-machine-input" placeholder="e.g. Chrome 138 dev" autocomplete="off" spellcheck="false">
|
| 191 |
+
</label>
|
| 192 |
+
<label class="run-machine-field">
|
| 193 |
+
<span class="run-machine-label">Operating system <span class="run-machine-req" aria-hidden="true">*</span></span>
|
| 194 |
+
<input type="text" id="ur-os" class="run-machine-input" placeholder="e.g. macOS 15.4" autocomplete="off" spellcheck="false">
|
| 195 |
+
</label>
|
| 196 |
+
</div>
|
| 197 |
+
<p class="run-machine-hint" id="ur-hint">Required fields marked <span class="run-machine-req">*</span>. Defaults are filled in from your browser; edit anything that's wrong before running.</p>
|
| 198 |
+
</details>
|
| 199 |
+
|
| 200 |
<!-- Hide filters, iterations, actions -->
|
| 201 |
<div class="filter-bar run-controls">
|
| 202 |
<div class="filter-bar-inner run-filters">
|