Spaces:
Running
Running
GitHub Actions commited on
Commit ·
4721a6e
1
Parent(s): cfcfe07
sync from abhijitramesh/webgpu-bench@9730846cc0
Browse files- README.md +1 -1
- bench.css +0 -217
- bench.html +0 -99
- css/style.css +1242 -0
- harness.js +3 -3
- index.html +186 -83
- js/app.js +159 -0
- js/charts.js +440 -0
- js/data.js +26 -0
- js/filters.js +209 -0
- bench-config.js → js/run/config.js +0 -0
- bench-app.js → js/run/controller.js +229 -129
- bench-core.js → js/run/core.js +2 -2
- bench-device.js → js/run/device.js +0 -0
- bench-hub.js → js/run/hub.js +4 -4
- bench-source.js → js/run/source.js +0 -0
- js/tables.js +332 -0
- js/utils.js +54 -0
- methodology.html +112 -0
- run/index.html +171 -0
README.md
CHANGED
|
@@ -29,4 +29,4 @@ Per-variant: prefill tokens/sec, decode tokens/sec, wall clock, optional CPU-vs-
|
|
| 29 |
- **Bonsai-1.7B Q1_0** needs `Q1_0` quantization support. The base (non-Q1_0) variant loads regardless.
|
| 30 |
|
| 31 |
## Privacy
|
| 32 |
-
No data is sent anywhere unless you click **Submit to leaderboard dataset**, which pushes to the dataset configured in `
|
|
|
|
| 29 |
- **Bonsai-1.7B Q1_0** needs `Q1_0` quantization support. The base (non-Q1_0) variant loads regardless.
|
| 30 |
|
| 31 |
## Privacy
|
| 32 |
+
No data is sent anywhere unless you click **Submit to leaderboard dataset**, which pushes to the dataset configured in `site/js/run/config.js` (`js/run/config.js` on the Space). Models and logs stay in your browser.
|
bench.css
DELETED
|
@@ -1,217 +0,0 @@
|
|
| 1 |
-
:root {
|
| 2 |
-
color-scheme: dark;
|
| 3 |
-
--bg: #0f1115;
|
| 4 |
-
--bg-raised: #161922;
|
| 5 |
-
--fg: #e6e8ec;
|
| 6 |
-
--muted: #8892a0;
|
| 7 |
-
--border: #252a33;
|
| 8 |
-
--border-strong: #3a414d;
|
| 9 |
-
--accent: #5eead4;
|
| 10 |
-
--accent-fg: #0a0b0e;
|
| 11 |
-
--warn: #f5a524;
|
| 12 |
-
--err: #ef4444;
|
| 13 |
-
--ok: #22c55e;
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
* { box-sizing: border-box; }
|
| 17 |
-
|
| 18 |
-
body {
|
| 19 |
-
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
| 20 |
-
background: var(--bg);
|
| 21 |
-
color: var(--fg);
|
| 22 |
-
margin: 0;
|
| 23 |
-
padding: 24px;
|
| 24 |
-
max-width: 1200px;
|
| 25 |
-
margin-inline: auto;
|
| 26 |
-
font-size: 14px;
|
| 27 |
-
line-height: 1.45;
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
h1 { font-size: 20px; margin: 0; font-weight: 600; }
|
| 31 |
-
h2 { font-size: 14px; margin: 16px 0 8px; font-weight: 600; }
|
| 32 |
-
.muted { color: var(--muted); font-size: 13px; }
|
| 33 |
-
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; background: var(--bg-raised); padding: 1px 4px; border-radius: 3px; }
|
| 34 |
-
|
| 35 |
-
header {
|
| 36 |
-
border-bottom: 1px solid var(--border);
|
| 37 |
-
padding-bottom: 16px;
|
| 38 |
-
margin-bottom: 16px;
|
| 39 |
-
}
|
| 40 |
-
.title-row { display: flex; align-items: center; gap: 12px; margin-bottom: 4px; }
|
| 41 |
-
#device-line, #budget-line { font-size: 12px; }
|
| 42 |
-
|
| 43 |
-
.mode-badge {
|
| 44 |
-
display: inline-block;
|
| 45 |
-
padding: 2px 8px;
|
| 46 |
-
border: 1px solid var(--border-strong);
|
| 47 |
-
border-radius: 4px;
|
| 48 |
-
font-size: 11px;
|
| 49 |
-
text-transform: uppercase;
|
| 50 |
-
color: var(--accent);
|
| 51 |
-
letter-spacing: 0.04em;
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
.filters {
|
| 55 |
-
display: flex;
|
| 56 |
-
gap: 14px;
|
| 57 |
-
align-items: center;
|
| 58 |
-
margin: 12px 0;
|
| 59 |
-
font-size: 12px;
|
| 60 |
-
color: var(--muted);
|
| 61 |
-
flex-wrap: wrap;
|
| 62 |
-
}
|
| 63 |
-
.filters label { display: inline-flex; gap: 4px; align-items: center; cursor: pointer; }
|
| 64 |
-
.filter-label { font-weight: 600; }
|
| 65 |
-
.filter-hint { font-size: 11px; opacity: 0.7; }
|
| 66 |
-
|
| 67 |
-
details.family {
|
| 68 |
-
border: 1px solid var(--border);
|
| 69 |
-
border-radius: 6px;
|
| 70 |
-
margin-bottom: 8px;
|
| 71 |
-
padding: 8px 12px;
|
| 72 |
-
background: var(--bg-raised);
|
| 73 |
-
}
|
| 74 |
-
details.family > summary {
|
| 75 |
-
font-weight: 600;
|
| 76 |
-
cursor: pointer;
|
| 77 |
-
list-style: none;
|
| 78 |
-
display: flex;
|
| 79 |
-
align-items: center;
|
| 80 |
-
gap: 8px;
|
| 81 |
-
}
|
| 82 |
-
details.family > summary::marker { display: none; }
|
| 83 |
-
details.family > summary::before { content: "▸ "; color: var(--muted); font-weight: 400; }
|
| 84 |
-
details.family[open] > summary::before { content: "▾ "; }
|
| 85 |
-
.family-stats { color: var(--muted); font-size: 12px; font-weight: 400; margin-left: 4px; }
|
| 86 |
-
.family-warnings { color: var(--warn); font-size: 11px; margin-left: auto; }
|
| 87 |
-
|
| 88 |
-
.variant-row {
|
| 89 |
-
display: grid;
|
| 90 |
-
grid-template-columns: 24px minmax(0, 1fr) 70px auto;
|
| 91 |
-
gap: 10px;
|
| 92 |
-
align-items: center;
|
| 93 |
-
padding: 4px 4px;
|
| 94 |
-
font-size: 13px;
|
| 95 |
-
cursor: pointer;
|
| 96 |
-
border-radius: 4px;
|
| 97 |
-
}
|
| 98 |
-
.variant-row:hover { background: rgba(94, 234, 212, 0.04); }
|
| 99 |
-
.variant-row.non-fit { opacity: 0.4; }
|
| 100 |
-
.variant-row .variant-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
| 101 |
-
.variant-row .size { color: var(--muted); font-variant-numeric: tabular-nums; text-align: right; }
|
| 102 |
-
.variant-row .badges { display: inline-flex; gap: 4px; flex-wrap: wrap; justify-content: flex-end; }
|
| 103 |
-
|
| 104 |
-
.cache-badge {
|
| 105 |
-
font-size: 10px;
|
| 106 |
-
padding: 1px 6px;
|
| 107 |
-
border-radius: 3px;
|
| 108 |
-
background: rgba(34, 197, 94, 0.15);
|
| 109 |
-
color: var(--ok);
|
| 110 |
-
font-weight: 500;
|
| 111 |
-
}
|
| 112 |
-
.warn-badge {
|
| 113 |
-
font-size: 10px;
|
| 114 |
-
padding: 1px 6px;
|
| 115 |
-
border-radius: 3px;
|
| 116 |
-
background: rgba(245, 165, 36, 0.12);
|
| 117 |
-
color: var(--warn);
|
| 118 |
-
font-weight: 500;
|
| 119 |
-
}
|
| 120 |
-
|
| 121 |
-
.controls {
|
| 122 |
-
display: flex;
|
| 123 |
-
gap: 8px;
|
| 124 |
-
align-items: center;
|
| 125 |
-
margin: 20px 0;
|
| 126 |
-
flex-wrap: wrap;
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
.iterations-control {
|
| 130 |
-
display: inline-flex;
|
| 131 |
-
align-items: center;
|
| 132 |
-
gap: 6px;
|
| 133 |
-
font-size: 12px;
|
| 134 |
-
color: var(--muted);
|
| 135 |
-
margin-right: 4px;
|
| 136 |
-
}
|
| 137 |
-
.iterations-control input[type="number"] {
|
| 138 |
-
width: 56px;
|
| 139 |
-
background: var(--bg-raised);
|
| 140 |
-
color: var(--fg);
|
| 141 |
-
border: 1px solid var(--border-strong);
|
| 142 |
-
border-radius: 4px;
|
| 143 |
-
padding: 4px 6px;
|
| 144 |
-
font-family: inherit;
|
| 145 |
-
font-size: 13px;
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
button {
|
| 149 |
-
background: var(--accent);
|
| 150 |
-
color: var(--accent-fg);
|
| 151 |
-
border: none;
|
| 152 |
-
padding: 8px 16px;
|
| 153 |
-
border-radius: 4px;
|
| 154 |
-
font-weight: 600;
|
| 155 |
-
font-size: 13px;
|
| 156 |
-
cursor: pointer;
|
| 157 |
-
font-family: inherit;
|
| 158 |
-
}
|
| 159 |
-
button:disabled { background: var(--border); color: var(--muted); cursor: not-allowed; }
|
| 160 |
-
button:hover:not(:disabled) { filter: brightness(1.08); }
|
| 161 |
-
button.secondary {
|
| 162 |
-
background: transparent;
|
| 163 |
-
color: var(--fg);
|
| 164 |
-
border: 1px solid var(--border-strong);
|
| 165 |
-
}
|
| 166 |
-
button.danger { background: var(--err); color: white; }
|
| 167 |
-
#queue-status { font-size: 12px; }
|
| 168 |
-
|
| 169 |
-
#progress-table {
|
| 170 |
-
width: 100%;
|
| 171 |
-
border-collapse: collapse;
|
| 172 |
-
font-size: 12px;
|
| 173 |
-
font-variant-numeric: tabular-nums;
|
| 174 |
-
}
|
| 175 |
-
#progress-table th, #progress-table td {
|
| 176 |
-
text-align: left;
|
| 177 |
-
padding: 6px 8px;
|
| 178 |
-
border-bottom: 1px solid var(--border);
|
| 179 |
-
}
|
| 180 |
-
#progress-table th.num, #progress-table td.num { text-align: right; }
|
| 181 |
-
#progress-table tr.row-queued { color: var(--muted); }
|
| 182 |
-
#progress-table tr.row-running { background: rgba(94, 234, 212, 0.06); }
|
| 183 |
-
#progress-table tr.row-ok { color: var(--fg); }
|
| 184 |
-
#progress-table tr.row-error { color: var(--err); }
|
| 185 |
-
|
| 186 |
-
#output-panel { margin-top: 24px; }
|
| 187 |
-
.output-actions { margin-bottom: 8px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
| 188 |
-
#output-textarea {
|
| 189 |
-
width: 100%;
|
| 190 |
-
height: 320px;
|
| 191 |
-
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
| 192 |
-
font-size: 12px;
|
| 193 |
-
background: var(--bg-raised);
|
| 194 |
-
color: var(--fg);
|
| 195 |
-
border: 1px solid var(--border);
|
| 196 |
-
border-radius: 4px;
|
| 197 |
-
padding: 10px;
|
| 198 |
-
resize: vertical;
|
| 199 |
-
}
|
| 200 |
-
.output-buttons { margin-top: 8px; display: flex; gap: 8px; }
|
| 201 |
-
|
| 202 |
-
#log-panel {
|
| 203 |
-
margin-top: 24px;
|
| 204 |
-
border-top: 1px solid var(--border);
|
| 205 |
-
padding-top: 12px;
|
| 206 |
-
}
|
| 207 |
-
#log-panel summary { cursor: pointer; color: var(--muted); font-size: 12px; }
|
| 208 |
-
#log-output {
|
| 209 |
-
max-height: 280px;
|
| 210 |
-
overflow-y: auto;
|
| 211 |
-
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
| 212 |
-
font-size: 11px;
|
| 213 |
-
color: var(--muted);
|
| 214 |
-
margin: 8px 0 0;
|
| 215 |
-
white-space: pre-wrap;
|
| 216 |
-
word-break: break-word;
|
| 217 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bench.html
DELETED
|
@@ -1,99 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="utf-8">
|
| 5 |
-
<title>WebGPU Benchmark — One Click</title>
|
| 6 |
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 7 |
-
<link rel="stylesheet" href="bench.css">
|
| 8 |
-
<!-- Import map so `@huggingface/hub` resolves in the browser via esm.sh.
|
| 9 |
-
Must appear before any <script type="module">. -->
|
| 10 |
-
<script type="importmap">
|
| 11 |
-
{
|
| 12 |
-
"imports": {
|
| 13 |
-
"@huggingface/hub": "https://esm.sh/@huggingface/hub"
|
| 14 |
-
}
|
| 15 |
-
}
|
| 16 |
-
</script>
|
| 17 |
-
</head>
|
| 18 |
-
<body>
|
| 19 |
-
<header>
|
| 20 |
-
<div class="title-row">
|
| 21 |
-
<h1>WebGPU Benchmark <span class="muted">— One Click</span></h1>
|
| 22 |
-
<span class="mode-badge" id="mode-badge" title="Detection source">…</span>
|
| 23 |
-
</div>
|
| 24 |
-
<div id="device-line" class="muted">Detecting device…</div>
|
| 25 |
-
<div id="budget-line" class="muted"></div>
|
| 26 |
-
</header>
|
| 27 |
-
|
| 28 |
-
<section class="filters" aria-label="Variant visibility filters">
|
| 29 |
-
<span class="filter-label">Hide:</span>
|
| 30 |
-
<label><input type="checkbox" id="hide-ud"> UD</label>
|
| 31 |
-
<label><input type="checkbox" id="hide-iq"> IQ</label>
|
| 32 |
-
<label><input type="checkbox" id="hide-hifp"> BF16/F16</label>
|
| 33 |
-
<span class="filter-hint">(visual only, does not uncheck)</span>
|
| 34 |
-
</section>
|
| 35 |
-
|
| 36 |
-
<section id="models-panel" aria-label="Models">
|
| 37 |
-
<p id="models-loading" class="muted">Loading models…</p>
|
| 38 |
-
</section>
|
| 39 |
-
|
| 40 |
-
<section id="hub-section" class="output-actions" hidden aria-label="Hugging Face dataset">
|
| 41 |
-
<button id="btn-signin" class="secondary">Sign in with Hugging Face</button>
|
| 42 |
-
<button id="btn-submit" disabled>Submit to leaderboard dataset</button>
|
| 43 |
-
<span id="hf-user" class="muted"></span>
|
| 44 |
-
</section>
|
| 45 |
-
|
| 46 |
-
<section class="controls">
|
| 47 |
-
<label class="iterations-control">
|
| 48 |
-
Iterations per variant:
|
| 49 |
-
<input type="number" id="iterations-input" value="5" min="1" max="50" step="1">
|
| 50 |
-
<span class="filter-hint">min 5 to submit</span>
|
| 51 |
-
</label>
|
| 52 |
-
<button id="btn-download" disabled>Download selected</button>
|
| 53 |
-
<button id="btn-run" disabled>Run benchmarks</button>
|
| 54 |
-
<button id="btn-abort" class="danger" disabled>Abort</button>
|
| 55 |
-
<button id="btn-purge" class="secondary" hidden>Purge OPFS cache</button>
|
| 56 |
-
<span id="queue-status" class="muted"></span>
|
| 57 |
-
</section>
|
| 58 |
-
|
| 59 |
-
<section id="progress-panel" hidden aria-label="Run progress">
|
| 60 |
-
<h2>Progress</h2>
|
| 61 |
-
<table id="progress-table">
|
| 62 |
-
<thead>
|
| 63 |
-
<tr>
|
| 64 |
-
<th>Model</th>
|
| 65 |
-
<th>Variant</th>
|
| 66 |
-
<th>Status</th>
|
| 67 |
-
<th class="num">Prefill tok/s</th>
|
| 68 |
-
<th class="num">Decode tok/s</th>
|
| 69 |
-
<th class="num">Wall s</th>
|
| 70 |
-
<th>Error</th>
|
| 71 |
-
</tr>
|
| 72 |
-
</thead>
|
| 73 |
-
<tbody></tbody>
|
| 74 |
-
</table>
|
| 75 |
-
</section>
|
| 76 |
-
|
| 77 |
-
<section id="output-panel" hidden aria-label="Output">
|
| 78 |
-
<h2>Output</h2>
|
| 79 |
-
<div id="output-actions-local" class="output-actions">
|
| 80 |
-
<label>
|
| 81 |
-
<input type="checkbox" id="save-local" checked>
|
| 82 |
-
Save to <code>results/results.json</code> on this server
|
| 83 |
-
</label>
|
| 84 |
-
</div>
|
| 85 |
-
<textarea id="output-textarea" readonly spellcheck="false"></textarea>
|
| 86 |
-
<div class="output-buttons">
|
| 87 |
-
<button id="btn-copy" class="secondary">Copy</button>
|
| 88 |
-
<button id="btn-download-json" class="secondary">Download JSON</button>
|
| 89 |
-
</div>
|
| 90 |
-
</section>
|
| 91 |
-
|
| 92 |
-
<details id="log-panel">
|
| 93 |
-
<summary>Run log</summary>
|
| 94 |
-
<pre id="log-output"></pre>
|
| 95 |
-
</details>
|
| 96 |
-
|
| 97 |
-
<script type="module" src="bench-app.js"></script>
|
| 98 |
-
</body>
|
| 99 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
css/style.css
ADDED
|
@@ -0,0 +1,1242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* =============================================================
|
| 2 |
+
WebGPU Bench Dashboard
|
| 3 |
+
Design: Precision Dark (shadcn-inspired)
|
| 4 |
+
============================================================= */
|
| 5 |
+
|
| 6 |
+
/* === RESET === */
|
| 7 |
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 8 |
+
|
| 9 |
+
/* === DESIGN TOKENS === */
|
| 10 |
+
:root {
|
| 11 |
+
/* Light theme (default) */
|
| 12 |
+
color-scheme: light;
|
| 13 |
+
|
| 14 |
+
--background: #ffffff;
|
| 15 |
+
--surface-0: #f4f4f5;
|
| 16 |
+
--surface-1: #fafafa;
|
| 17 |
+
--surface-2: #f0f0f1;
|
| 18 |
+
--surface-3: #e4e4e7;
|
| 19 |
+
|
| 20 |
+
--border: #e4e4e7;
|
| 21 |
+
--border-hover: #d4d4d8;
|
| 22 |
+
--ring: #a1a1aa;
|
| 23 |
+
|
| 24 |
+
--foreground: #09090b;
|
| 25 |
+
--foreground-secondary: #3f3f46;
|
| 26 |
+
--foreground-muted: #71717a;
|
| 27 |
+
--foreground-subtle: #a1a1aa;
|
| 28 |
+
|
| 29 |
+
--success: #16a34a;
|
| 30 |
+
--success-bg: rgba(22, 163, 74, 0.1);
|
| 31 |
+
--error: #dc2626;
|
| 32 |
+
--error-bg: rgba(220, 38, 38, 0.1);
|
| 33 |
+
--warning: #d97706;
|
| 34 |
+
--info: #2563eb;
|
| 35 |
+
|
| 36 |
+
/* Accent colors (theme-aware) */
|
| 37 |
+
--accent-blue: #2563eb;
|
| 38 |
+
--accent-blue-bg: rgba(37, 99, 235, 0.1);
|
| 39 |
+
--accent-purple: #7c3aed;
|
| 40 |
+
--accent-purple-bg: rgba(124, 58, 237, 0.1);
|
| 41 |
+
--accent-amber: #d97706;
|
| 42 |
+
--accent-amber-bg: rgba(217, 119, 6, 0.1);
|
| 43 |
+
--accent-violet: #6d28d9;
|
| 44 |
+
--accent-violet-bg: rgba(109, 40, 217, 0.1);
|
| 45 |
+
|
| 46 |
+
--shadow-dropdown: 0 8px 24px rgba(0, 0, 0, 0.12);
|
| 47 |
+
|
| 48 |
+
/* Non-color tokens (same in both themes) */
|
| 49 |
+
--font-sans: 'Manrope', system-ui, -apple-system, sans-serif;
|
| 50 |
+
--font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
|
| 51 |
+
--radius-sm: 6px;
|
| 52 |
+
--radius-md: 8px;
|
| 53 |
+
--radius-lg: 12px;
|
| 54 |
+
--transition-fast: 150ms ease;
|
| 55 |
+
--transition-base: 200ms ease;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
[data-theme="dark"] {
|
| 59 |
+
color-scheme: dark;
|
| 60 |
+
|
| 61 |
+
--background: #09090b;
|
| 62 |
+
--surface-0: #0f0f12;
|
| 63 |
+
--surface-1: #18181b;
|
| 64 |
+
--surface-2: #1c1c20;
|
| 65 |
+
--surface-3: #27272a;
|
| 66 |
+
|
| 67 |
+
--border: #27272a;
|
| 68 |
+
--border-hover: #3f3f46;
|
| 69 |
+
--ring: #52525b;
|
| 70 |
+
|
| 71 |
+
--foreground: #fafafa;
|
| 72 |
+
--foreground-secondary: #d4d4d8;
|
| 73 |
+
--foreground-muted: #a1a1aa;
|
| 74 |
+
--foreground-subtle: #71717a;
|
| 75 |
+
|
| 76 |
+
--success: #22c55e;
|
| 77 |
+
--success-bg: rgba(34, 197, 94, 0.12);
|
| 78 |
+
--error: #ef4444;
|
| 79 |
+
--error-bg: rgba(239, 68, 68, 0.12);
|
| 80 |
+
--warning: #eab308;
|
| 81 |
+
--info: #3b82f6;
|
| 82 |
+
|
| 83 |
+
--accent-blue: #60a5fa;
|
| 84 |
+
--accent-blue-bg: rgba(59, 130, 246, 0.12);
|
| 85 |
+
--accent-purple: #a78bfa;
|
| 86 |
+
--accent-purple-bg: rgba(139, 92, 246, 0.12);
|
| 87 |
+
--accent-amber: #fbbf24;
|
| 88 |
+
--accent-amber-bg: rgba(245, 158, 11, 0.12);
|
| 89 |
+
--accent-violet: #c084fc;
|
| 90 |
+
--accent-violet-bg: rgba(168, 85, 247, 0.12);
|
| 91 |
+
|
| 92 |
+
--shadow-dropdown: 0 8px 24px rgba(0, 0, 0, 0.4);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/* === GLOBAL === */
|
| 96 |
+
html { scroll-behavior: smooth; }
|
| 97 |
+
|
| 98 |
+
body {
|
| 99 |
+
background: var(--background);
|
| 100 |
+
color: var(--foreground);
|
| 101 |
+
font-family: var(--font-sans);
|
| 102 |
+
font-size: 14px;
|
| 103 |
+
line-height: 1.6;
|
| 104 |
+
-webkit-font-smoothing: antialiased;
|
| 105 |
+
-moz-osx-font-smoothing: grayscale;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
a {
|
| 109 |
+
color: var(--info);
|
| 110 |
+
text-decoration: none;
|
| 111 |
+
transition: color var(--transition-fast);
|
| 112 |
+
}
|
| 113 |
+
a:hover { color: #60a5fa; }
|
| 114 |
+
|
| 115 |
+
/* === SCROLLBAR === */
|
| 116 |
+
::-webkit-scrollbar { width: 8px; height: 8px; }
|
| 117 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 118 |
+
::-webkit-scrollbar-thumb { background: var(--surface-3); border-radius: 4px; }
|
| 119 |
+
::-webkit-scrollbar-thumb:hover { background: var(--ring); }
|
| 120 |
+
|
| 121 |
+
/* =============================================================
|
| 122 |
+
HEADER
|
| 123 |
+
============================================================= */
|
| 124 |
+
.header {
|
| 125 |
+
border-bottom: 1px solid var(--border);
|
| 126 |
+
background: var(--background);
|
| 127 |
+
position: relative;
|
| 128 |
+
z-index: 50;
|
| 129 |
+
}
|
| 130 |
+
.header-inner {
|
| 131 |
+
display: flex;
|
| 132 |
+
align-items: center;
|
| 133 |
+
justify-content: space-between;
|
| 134 |
+
padding: 0 32px;
|
| 135 |
+
height: 56px;
|
| 136 |
+
}
|
| 137 |
+
.header-brand {
|
| 138 |
+
display: flex;
|
| 139 |
+
align-items: center;
|
| 140 |
+
gap: 10px;
|
| 141 |
+
color: var(--foreground);
|
| 142 |
+
text-decoration: none;
|
| 143 |
+
}
|
| 144 |
+
.header-brand:hover { color: var(--foreground); text-decoration: none; }
|
| 145 |
+
.header-logo { color: var(--foreground-muted); flex-shrink: 0; }
|
| 146 |
+
.header-title {
|
| 147 |
+
font-size: 15px;
|
| 148 |
+
font-weight: 700;
|
| 149 |
+
letter-spacing: -0.01em;
|
| 150 |
+
}
|
| 151 |
+
.header-nav {
|
| 152 |
+
display: flex;
|
| 153 |
+
align-items: center;
|
| 154 |
+
gap: 4px;
|
| 155 |
+
}
|
| 156 |
+
.header-link {
|
| 157 |
+
display: inline-flex;
|
| 158 |
+
align-items: center;
|
| 159 |
+
gap: 6px;
|
| 160 |
+
padding: 6px 12px;
|
| 161 |
+
border-radius: var(--radius-sm);
|
| 162 |
+
font-size: 13px;
|
| 163 |
+
font-weight: 500;
|
| 164 |
+
color: var(--foreground-muted);
|
| 165 |
+
transition: color var(--transition-fast), background var(--transition-fast);
|
| 166 |
+
}
|
| 167 |
+
.header-link:hover {
|
| 168 |
+
color: var(--foreground);
|
| 169 |
+
background: var(--surface-2);
|
| 170 |
+
text-decoration: none;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
/* =============================================================
|
| 174 |
+
LOADING STATE
|
| 175 |
+
============================================================= */
|
| 176 |
+
.loading-state {
|
| 177 |
+
display: flex;
|
| 178 |
+
align-items: center;
|
| 179 |
+
justify-content: center;
|
| 180 |
+
min-height: 60vh;
|
| 181 |
+
}
|
| 182 |
+
.loading-content { text-align: center; }
|
| 183 |
+
.loading-spinner {
|
| 184 |
+
width: 28px;
|
| 185 |
+
height: 28px;
|
| 186 |
+
margin: 0 auto 16px;
|
| 187 |
+
border: 2.5px solid var(--surface-3);
|
| 188 |
+
border-top-color: var(--foreground);
|
| 189 |
+
border-radius: 50%;
|
| 190 |
+
animation: spin 0.7s linear infinite;
|
| 191 |
+
}
|
| 192 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 193 |
+
.loading-state p {
|
| 194 |
+
color: var(--foreground-muted);
|
| 195 |
+
font-size: 14px;
|
| 196 |
+
}
|
| 197 |
+
.loading-error {
|
| 198 |
+
color: var(--error);
|
| 199 |
+
font-weight: 600;
|
| 200 |
+
margin-bottom: 8px;
|
| 201 |
+
}
|
| 202 |
+
.loading-hint {
|
| 203 |
+
color: var(--foreground-muted);
|
| 204 |
+
font-size: 13px;
|
| 205 |
+
}
|
| 206 |
+
.loading-hint code {
|
| 207 |
+
background: var(--surface-2);
|
| 208 |
+
padding: 2px 8px;
|
| 209 |
+
border-radius: var(--radius-sm);
|
| 210 |
+
font-family: var(--font-mono);
|
| 211 |
+
font-size: 12px;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
/* =============================================================
|
| 215 |
+
FILTER BAR
|
| 216 |
+
============================================================= */
|
| 217 |
+
.filter-bar {
|
| 218 |
+
background: var(--surface-0);
|
| 219 |
+
border-bottom: 1px solid var(--border);
|
| 220 |
+
padding: 16px 32px;
|
| 221 |
+
position: relative;
|
| 222 |
+
z-index: 45;
|
| 223 |
+
}
|
| 224 |
+
.filter-bar-inner {
|
| 225 |
+
display: flex;
|
| 226 |
+
align-items: flex-end;
|
| 227 |
+
gap: 16px;
|
| 228 |
+
flex-wrap: wrap;
|
| 229 |
+
}
|
| 230 |
+
.filter-group {
|
| 231 |
+
display: flex;
|
| 232 |
+
flex-direction: column;
|
| 233 |
+
gap: 6px;
|
| 234 |
+
}
|
| 235 |
+
.filter-label {
|
| 236 |
+
font-size: 11px;
|
| 237 |
+
font-weight: 600;
|
| 238 |
+
color: var(--foreground-subtle);
|
| 239 |
+
text-transform: uppercase;
|
| 240 |
+
letter-spacing: 0.05em;
|
| 241 |
+
}
|
| 242 |
+
.filter-select {
|
| 243 |
+
appearance: none;
|
| 244 |
+
background: var(--surface-1);
|
| 245 |
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2371717a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
| 246 |
+
background-repeat: no-repeat;
|
| 247 |
+
background-position: right 10px center;
|
| 248 |
+
background-size: 12px;
|
| 249 |
+
border: 1px solid var(--border);
|
| 250 |
+
border-radius: var(--radius-md);
|
| 251 |
+
color: var(--foreground);
|
| 252 |
+
padding: 0 32px 0 12px;
|
| 253 |
+
font-family: var(--font-sans);
|
| 254 |
+
font-size: 13px;
|
| 255 |
+
font-weight: 500;
|
| 256 |
+
cursor: pointer;
|
| 257 |
+
height: 36px;
|
| 258 |
+
min-width: 150px;
|
| 259 |
+
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
| 260 |
+
}
|
| 261 |
+
.filter-select:hover { border-color: var(--border-hover); }
|
| 262 |
+
.filter-select:focus {
|
| 263 |
+
outline: none;
|
| 264 |
+
border-color: var(--ring);
|
| 265 |
+
box-shadow: 0 0 0 2px rgba(82, 82, 91, 0.3);
|
| 266 |
+
}
|
| 267 |
+
.filter-select option {
|
| 268 |
+
background: var(--surface-1);
|
| 269 |
+
color: var(--foreground);
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
[data-theme="dark"] .filter-select {
|
| 273 |
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%23a1a1aa' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
/* Theme toggle button */
|
| 277 |
+
.theme-toggle-btn { padding: 6px 10px; }
|
| 278 |
+
.icon-sun { display: none; }
|
| 279 |
+
.icon-moon { display: block; }
|
| 280 |
+
[data-theme="dark"] .icon-sun { display: block; }
|
| 281 |
+
[data-theme="dark"] .icon-moon { display: none; }
|
| 282 |
+
|
| 283 |
+
.filter-actions { display: flex; align-items: flex-end; }
|
| 284 |
+
.filter-reset-btn {
|
| 285 |
+
display: inline-flex;
|
| 286 |
+
align-items: center;
|
| 287 |
+
gap: 6px;
|
| 288 |
+
height: 36px;
|
| 289 |
+
padding: 0 14px;
|
| 290 |
+
background: transparent;
|
| 291 |
+
border: 1px solid var(--border);
|
| 292 |
+
border-radius: var(--radius-md);
|
| 293 |
+
color: var(--foreground-muted);
|
| 294 |
+
font-family: var(--font-sans);
|
| 295 |
+
font-size: 13px;
|
| 296 |
+
font-weight: 500;
|
| 297 |
+
cursor: pointer;
|
| 298 |
+
transition: all var(--transition-fast);
|
| 299 |
+
}
|
| 300 |
+
.filter-reset-btn:hover {
|
| 301 |
+
background: var(--surface-2);
|
| 302 |
+
color: var(--foreground);
|
| 303 |
+
border-color: var(--border-hover);
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
/* =============================================================
|
| 307 |
+
QUANT MULTI-SELECT DROPDOWN
|
| 308 |
+
============================================================= */
|
| 309 |
+
.quant-dropdown { position: relative; }
|
| 310 |
+
.quant-dropdown-btn {
|
| 311 |
+
display: inline-flex;
|
| 312 |
+
align-items: center;
|
| 313 |
+
justify-content: space-between;
|
| 314 |
+
gap: 8px;
|
| 315 |
+
height: 36px;
|
| 316 |
+
min-width: 150px;
|
| 317 |
+
padding: 0 12px;
|
| 318 |
+
background: var(--surface-1);
|
| 319 |
+
border: 1px solid var(--border);
|
| 320 |
+
border-radius: var(--radius-md);
|
| 321 |
+
color: var(--foreground);
|
| 322 |
+
font-family: var(--font-sans);
|
| 323 |
+
font-size: 13px;
|
| 324 |
+
font-weight: 500;
|
| 325 |
+
cursor: pointer;
|
| 326 |
+
text-align: left;
|
| 327 |
+
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
| 328 |
+
}
|
| 329 |
+
.quant-dropdown-btn:hover { border-color: var(--border-hover); }
|
| 330 |
+
.quant-dropdown-btn:focus {
|
| 331 |
+
outline: none;
|
| 332 |
+
border-color: var(--ring);
|
| 333 |
+
box-shadow: 0 0 0 2px rgba(82, 82, 91, 0.3);
|
| 334 |
+
}
|
| 335 |
+
.quant-dropdown-btn svg {
|
| 336 |
+
flex-shrink: 0;
|
| 337 |
+
opacity: 0.6;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
.quant-dropdown-list {
|
| 341 |
+
display: none;
|
| 342 |
+
position: absolute;
|
| 343 |
+
top: calc(100% + 4px);
|
| 344 |
+
left: 0;
|
| 345 |
+
z-index: 100;
|
| 346 |
+
background: var(--surface-1);
|
| 347 |
+
border: 1px solid var(--border);
|
| 348 |
+
border-radius: var(--radius-md);
|
| 349 |
+
box-shadow: var(--shadow-dropdown);
|
| 350 |
+
max-height: 320px;
|
| 351 |
+
overflow-y: auto;
|
| 352 |
+
min-width: 180px;
|
| 353 |
+
padding: 4px;
|
| 354 |
+
animation: dropdownIn 0.15s ease-out;
|
| 355 |
+
}
|
| 356 |
+
@keyframes dropdownIn {
|
| 357 |
+
from { opacity: 0; transform: translateY(-4px); }
|
| 358 |
+
to { opacity: 1; transform: translateY(0); }
|
| 359 |
+
}
|
| 360 |
+
.quant-dropdown.open .quant-dropdown-list { display: block; }
|
| 361 |
+
|
| 362 |
+
.quant-option {
|
| 363 |
+
display: flex;
|
| 364 |
+
align-items: center;
|
| 365 |
+
gap: 8px;
|
| 366 |
+
padding: 6px 10px;
|
| 367 |
+
cursor: pointer;
|
| 368 |
+
font-size: 13px;
|
| 369 |
+
border-radius: var(--radius-sm);
|
| 370 |
+
transition: background var(--transition-fast);
|
| 371 |
+
color: var(--foreground-secondary);
|
| 372 |
+
}
|
| 373 |
+
.quant-option:hover { background: var(--surface-2); }
|
| 374 |
+
.quant-option.select-all {
|
| 375 |
+
border-bottom: 1px solid var(--border);
|
| 376 |
+
margin-bottom: 4px;
|
| 377 |
+
padding-bottom: 8px;
|
| 378 |
+
border-radius: 0;
|
| 379 |
+
font-weight: 600;
|
| 380 |
+
color: var(--foreground);
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
/* Custom checkbox */
|
| 384 |
+
.quant-option input[type="checkbox"] {
|
| 385 |
+
appearance: none;
|
| 386 |
+
-webkit-appearance: none;
|
| 387 |
+
width: 16px;
|
| 388 |
+
height: 16px;
|
| 389 |
+
border: 1.5px solid var(--ring);
|
| 390 |
+
border-radius: 4px;
|
| 391 |
+
background: var(--surface-0);
|
| 392 |
+
cursor: pointer;
|
| 393 |
+
position: relative;
|
| 394 |
+
flex-shrink: 0;
|
| 395 |
+
transition: all var(--transition-fast);
|
| 396 |
+
}
|
| 397 |
+
.quant-option input[type="checkbox"]:checked {
|
| 398 |
+
background: var(--foreground);
|
| 399 |
+
border-color: var(--foreground);
|
| 400 |
+
}
|
| 401 |
+
.quant-option input[type="checkbox"]:checked::after {
|
| 402 |
+
content: '';
|
| 403 |
+
position: absolute;
|
| 404 |
+
top: 1px;
|
| 405 |
+
left: 4.5px;
|
| 406 |
+
width: 4.5px;
|
| 407 |
+
height: 8px;
|
| 408 |
+
border: solid var(--background);
|
| 409 |
+
border-width: 0 2px 2px 0;
|
| 410 |
+
transform: rotate(45deg);
|
| 411 |
+
}
|
| 412 |
+
.quant-option input[type="checkbox"]:indeterminate {
|
| 413 |
+
background: var(--foreground);
|
| 414 |
+
border-color: var(--foreground);
|
| 415 |
+
}
|
| 416 |
+
.quant-option input[type="checkbox"]:indeterminate::after {
|
| 417 |
+
content: '';
|
| 418 |
+
position: absolute;
|
| 419 |
+
top: 5.5px;
|
| 420 |
+
left: 3px;
|
| 421 |
+
width: 8px;
|
| 422 |
+
height: 2px;
|
| 423 |
+
background: var(--background);
|
| 424 |
+
border-radius: 1px;
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
/* =============================================================
|
| 428 |
+
SECTION NAVIGATION (Sticky Tabs)
|
| 429 |
+
============================================================= */
|
| 430 |
+
.section-nav {
|
| 431 |
+
position: sticky;
|
| 432 |
+
top: 0;
|
| 433 |
+
z-index: 40;
|
| 434 |
+
background: var(--background);
|
| 435 |
+
border-bottom: 1px solid var(--border);
|
| 436 |
+
}
|
| 437 |
+
.section-nav-track {
|
| 438 |
+
display: flex;
|
| 439 |
+
padding: 0 32px;
|
| 440 |
+
overflow-x: auto;
|
| 441 |
+
scrollbar-width: none;
|
| 442 |
+
-webkit-overflow-scrolling: touch;
|
| 443 |
+
}
|
| 444 |
+
.section-nav-track::-webkit-scrollbar { display: none; }
|
| 445 |
+
|
| 446 |
+
.section-nav-item {
|
| 447 |
+
padding: 10px 16px;
|
| 448 |
+
background: none;
|
| 449 |
+
border: none;
|
| 450 |
+
border-bottom: 2px solid transparent;
|
| 451 |
+
color: var(--foreground-muted);
|
| 452 |
+
font-family: var(--font-sans);
|
| 453 |
+
font-size: 13px;
|
| 454 |
+
font-weight: 500;
|
| 455 |
+
cursor: pointer;
|
| 456 |
+
white-space: nowrap;
|
| 457 |
+
transition: color var(--transition-fast), border-color var(--transition-fast);
|
| 458 |
+
}
|
| 459 |
+
.section-nav-item:hover { color: var(--foreground); }
|
| 460 |
+
.section-nav-item.active {
|
| 461 |
+
color: var(--foreground);
|
| 462 |
+
border-bottom-color: var(--foreground);
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
/* =============================================================
|
| 466 |
+
SECTIONS & LAYOUT
|
| 467 |
+
============================================================= */
|
| 468 |
+
.container { padding: 0 32px; }
|
| 469 |
+
|
| 470 |
+
.dash-section { padding: 32px 0; }
|
| 471 |
+
.dash-section + .dash-section { border-top: 1px solid var(--border); }
|
| 472 |
+
|
| 473 |
+
.section-header {
|
| 474 |
+
display: flex;
|
| 475 |
+
align-items: baseline;
|
| 476 |
+
justify-content: space-between;
|
| 477 |
+
margin-bottom: 20px;
|
| 478 |
+
}
|
| 479 |
+
.section-header h2 {
|
| 480 |
+
font-size: 16px;
|
| 481 |
+
font-weight: 700;
|
| 482 |
+
color: var(--foreground);
|
| 483 |
+
letter-spacing: -0.01em;
|
| 484 |
+
}
|
| 485 |
+
.results-count {
|
| 486 |
+
font-size: 13px;
|
| 487 |
+
color: var(--foreground-muted);
|
| 488 |
+
font-weight: 500;
|
| 489 |
+
font-family: var(--font-mono);
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
/* =============================================================
|
| 493 |
+
SUMMARY / STAT CARDS
|
| 494 |
+
============================================================= */
|
| 495 |
+
.summary-grid {
|
| 496 |
+
display: grid;
|
| 497 |
+
grid-template-columns: repeat(3, 1fr);
|
| 498 |
+
gap: 16px;
|
| 499 |
+
}
|
| 500 |
+
.stat-card {
|
| 501 |
+
display: flex;
|
| 502 |
+
align-items: flex-start;
|
| 503 |
+
gap: 16px;
|
| 504 |
+
background: var(--surface-1);
|
| 505 |
+
border: 1px solid var(--border);
|
| 506 |
+
border-radius: var(--radius-lg);
|
| 507 |
+
padding: 20px 24px;
|
| 508 |
+
transition: border-color var(--transition-base);
|
| 509 |
+
}
|
| 510 |
+
.stat-card:hover { border-color: var(--border-hover); }
|
| 511 |
+
|
| 512 |
+
.stat-card-icon {
|
| 513 |
+
display: flex;
|
| 514 |
+
align-items: center;
|
| 515 |
+
justify-content: center;
|
| 516 |
+
width: 40px;
|
| 517 |
+
height: 40px;
|
| 518 |
+
border-radius: var(--radius-md);
|
| 519 |
+
flex-shrink: 0;
|
| 520 |
+
}
|
| 521 |
+
.stat-card-icon--machines { background: var(--accent-blue-bg); color: var(--accent-blue); }
|
| 522 |
+
.stat-card-icon--benchmarks { background: var(--accent-violet-bg); color: var(--accent-violet); }
|
| 523 |
+
.stat-card-icon--pass { background: var(--success-bg); color: var(--success); }
|
| 524 |
+
|
| 525 |
+
.stat-card-label {
|
| 526 |
+
display: block;
|
| 527 |
+
font-size: 12px;
|
| 528 |
+
font-weight: 500;
|
| 529 |
+
color: var(--foreground-muted);
|
| 530 |
+
margin-bottom: 2px;
|
| 531 |
+
}
|
| 532 |
+
.stat-card-value {
|
| 533 |
+
display: block;
|
| 534 |
+
font-size: 28px;
|
| 535 |
+
font-weight: 800;
|
| 536 |
+
font-family: var(--font-mono);
|
| 537 |
+
color: var(--foreground);
|
| 538 |
+
letter-spacing: -0.02em;
|
| 539 |
+
line-height: 1.1;
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
/* =============================================================
|
| 543 |
+
TABLES (Results + Error)
|
| 544 |
+
============================================================= */
|
| 545 |
+
.table-card {
|
| 546 |
+
border: 1px solid var(--border);
|
| 547 |
+
border-radius: var(--radius-lg);
|
| 548 |
+
overflow: hidden;
|
| 549 |
+
}
|
| 550 |
+
.results-wrapper {
|
| 551 |
+
overflow: auto;
|
| 552 |
+
max-height: 600px;
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.results-table,
|
| 556 |
+
.data-table {
|
| 557 |
+
width: 100%;
|
| 558 |
+
border-collapse: separate;
|
| 559 |
+
border-spacing: 0;
|
| 560 |
+
font-size: 13px;
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
/* Table headers */
|
| 564 |
+
.results-table thead th,
|
| 565 |
+
.data-table thead th {
|
| 566 |
+
position: sticky;
|
| 567 |
+
top: 0;
|
| 568 |
+
z-index: 10;
|
| 569 |
+
background: var(--surface-1);
|
| 570 |
+
text-align: left;
|
| 571 |
+
padding: 10px 12px;
|
| 572 |
+
color: var(--foreground-muted);
|
| 573 |
+
font-family: var(--font-sans);
|
| 574 |
+
font-weight: 600;
|
| 575 |
+
font-size: 11px;
|
| 576 |
+
text-transform: uppercase;
|
| 577 |
+
letter-spacing: 0.04em;
|
| 578 |
+
white-space: nowrap;
|
| 579 |
+
border-bottom: 1px solid var(--border);
|
| 580 |
+
user-select: none;
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
/* Sortable headers (results table only) */
|
| 584 |
+
.results-table thead th {
|
| 585 |
+
cursor: pointer;
|
| 586 |
+
transition: color var(--transition-fast), background var(--transition-fast);
|
| 587 |
+
}
|
| 588 |
+
.results-table thead th:hover {
|
| 589 |
+
color: var(--foreground);
|
| 590 |
+
background: var(--surface-2);
|
| 591 |
+
}
|
| 592 |
+
.results-table thead th.sorted {
|
| 593 |
+
color: var(--foreground);
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
/* Table cells */
|
| 597 |
+
.results-table td,
|
| 598 |
+
.data-table td {
|
| 599 |
+
padding: 8px 12px;
|
| 600 |
+
border-bottom: 1px solid var(--border);
|
| 601 |
+
white-space: nowrap;
|
| 602 |
+
color: var(--foreground-secondary);
|
| 603 |
+
}
|
| 604 |
+
.results-table tbody tr {
|
| 605 |
+
transition: background var(--transition-fast);
|
| 606 |
+
}
|
| 607 |
+
.results-table tbody tr:hover,
|
| 608 |
+
.data-table tbody tr:hover {
|
| 609 |
+
background: var(--surface-2);
|
| 610 |
+
}
|
| 611 |
+
.results-table tbody tr:last-child td,
|
| 612 |
+
.data-table tbody tr:last-child td {
|
| 613 |
+
border-bottom: none;
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
/* Row status indicator */
|
| 617 |
+
.row-pass td:first-child { box-shadow: inset 3px 0 0 var(--success); }
|
| 618 |
+
.row-fail td:first-child { box-shadow: inset 3px 0 0 var(--error); }
|
| 619 |
+
|
| 620 |
+
/* =============================================================
|
| 621 |
+
BADGES
|
| 622 |
+
============================================================= */
|
| 623 |
+
.badge {
|
| 624 |
+
display: inline-flex;
|
| 625 |
+
align-items: center;
|
| 626 |
+
padding: 2px 8px;
|
| 627 |
+
border-radius: 9999px;
|
| 628 |
+
font-size: 11px;
|
| 629 |
+
font-weight: 600;
|
| 630 |
+
text-transform: uppercase;
|
| 631 |
+
letter-spacing: 0.02em;
|
| 632 |
+
}
|
| 633 |
+
.badge--pass { background: var(--success-bg); color: var(--success); }
|
| 634 |
+
.badge--fail { background: var(--error-bg); color: var(--error); }
|
| 635 |
+
.badge--yes { background: var(--accent-blue-bg); color: var(--accent-blue); }
|
| 636 |
+
.badge--no { background: var(--surface-3); color: var(--foreground-subtle); }
|
| 637 |
+
.badge--webgpu { background: var(--accent-purple-bg); color: var(--accent-purple); }
|
| 638 |
+
.badge--cpu { background: var(--accent-amber-bg); color: var(--accent-amber); }
|
| 639 |
+
|
| 640 |
+
/* =============================================================
|
| 641 |
+
ERROR CELLS & CATEGORIES
|
| 642 |
+
============================================================= */
|
| 643 |
+
.error-cell {
|
| 644 |
+
color: var(--error);
|
| 645 |
+
font-size: 12px;
|
| 646 |
+
}
|
| 647 |
+
.error-cat {
|
| 648 |
+
display: inline-block;
|
| 649 |
+
padding: 1px 6px;
|
| 650 |
+
border-radius: 4px;
|
| 651 |
+
background: var(--error-bg);
|
| 652 |
+
color: var(--error);
|
| 653 |
+
font-size: 10px;
|
| 654 |
+
font-weight: 600;
|
| 655 |
+
text-transform: uppercase;
|
| 656 |
+
margin-right: 4px;
|
| 657 |
+
vertical-align: middle;
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
/* =============================================================
|
| 661 |
+
CHARTS
|
| 662 |
+
============================================================= */
|
| 663 |
+
.charts-grid {
|
| 664 |
+
display: grid;
|
| 665 |
+
grid-template-columns: 1fr 1fr;
|
| 666 |
+
gap: 16px;
|
| 667 |
+
}
|
| 668 |
+
.chart-box {
|
| 669 |
+
background: var(--surface-1);
|
| 670 |
+
border: 1px solid var(--border);
|
| 671 |
+
border-radius: var(--radius-lg);
|
| 672 |
+
padding: 20px;
|
| 673 |
+
position: relative;
|
| 674 |
+
height: 340px;
|
| 675 |
+
transition: border-color var(--transition-base);
|
| 676 |
+
}
|
| 677 |
+
.chart-box:hover { border-color: var(--border-hover); }
|
| 678 |
+
|
| 679 |
+
.subsection-title { font-size: 16px; font-weight: 600; color: var(--foreground); margin: 0; }
|
| 680 |
+
.metric-selector { display: flex; align-items: center; gap: 8px; }
|
| 681 |
+
.metric-selector-label { font-size: 13px; color: var(--foreground-subtle); }
|
| 682 |
+
.col-sublabel { font-size: 10px; font-weight: 400; color: var(--foreground-subtle); }
|
| 683 |
+
.th-group { text-align: center; font-weight: 700; color: var(--foreground); background: var(--surface-2); }
|
| 684 |
+
.th-sub { font-size: 11px; font-weight: 500; color: var(--foreground-subtle); white-space: nowrap; }
|
| 685 |
+
.th-group-border { border-right: 1px solid var(--border-hover); }
|
| 686 |
+
.results-table td:has(+ .th-group-border) { border-right: 1px solid var(--border-hover); }
|
| 687 |
+
|
| 688 |
+
.chart-empty {
|
| 689 |
+
position: absolute;
|
| 690 |
+
top: 50%;
|
| 691 |
+
left: 50%;
|
| 692 |
+
transform: translate(-50%, -50%);
|
| 693 |
+
color: var(--foreground-subtle);
|
| 694 |
+
font-size: 13px;
|
| 695 |
+
}
|
| 696 |
+
|
| 697 |
+
/* =============================================================
|
| 698 |
+
MACHINE CARDS
|
| 699 |
+
============================================================= */
|
| 700 |
+
.machine-grid {
|
| 701 |
+
display: grid;
|
| 702 |
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
| 703 |
+
gap: 16px;
|
| 704 |
+
}
|
| 705 |
+
.machine-card {
|
| 706 |
+
background: var(--surface-1);
|
| 707 |
+
border: 1px solid var(--border);
|
| 708 |
+
border-radius: var(--radius-lg);
|
| 709 |
+
padding: 20px 24px;
|
| 710 |
+
transition: border-color var(--transition-base);
|
| 711 |
+
}
|
| 712 |
+
.machine-card:hover { border-color: var(--border-hover); }
|
| 713 |
+
|
| 714 |
+
.machine-card-header {
|
| 715 |
+
display: flex;
|
| 716 |
+
align-items: center;
|
| 717 |
+
gap: 10px;
|
| 718 |
+
margin-bottom: 16px;
|
| 719 |
+
padding-bottom: 12px;
|
| 720 |
+
border-bottom: 1px solid var(--border);
|
| 721 |
+
}
|
| 722 |
+
.machine-card-header h3 {
|
| 723 |
+
font-size: 14px;
|
| 724 |
+
font-weight: 600;
|
| 725 |
+
color: var(--foreground);
|
| 726 |
+
line-height: 1.3;
|
| 727 |
+
}
|
| 728 |
+
.machine-card-icon { color: var(--foreground-muted); flex-shrink: 0; }
|
| 729 |
+
|
| 730 |
+
.machine-card-specs {
|
| 731 |
+
display: flex;
|
| 732 |
+
flex-direction: column;
|
| 733 |
+
gap: 8px;
|
| 734 |
+
}
|
| 735 |
+
.spec-row {
|
| 736 |
+
display: flex;
|
| 737 |
+
justify-content: space-between;
|
| 738 |
+
align-items: center;
|
| 739 |
+
font-size: 13px;
|
| 740 |
+
}
|
| 741 |
+
.spec-label { color: var(--foreground-muted); }
|
| 742 |
+
.spec-value { color: var(--foreground-secondary); font-weight: 500; font-family: var(--font-mono); font-size: 12px; }
|
| 743 |
+
|
| 744 |
+
/* =============================================================
|
| 745 |
+
EMPTY STATES
|
| 746 |
+
============================================================= */
|
| 747 |
+
.empty-state {
|
| 748 |
+
padding: 48px 24px;
|
| 749 |
+
text-align: center;
|
| 750 |
+
color: var(--foreground-subtle);
|
| 751 |
+
font-size: 14px;
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
/* =============================================================
|
| 755 |
+
UTILITY CLASSES
|
| 756 |
+
============================================================= */
|
| 757 |
+
.mono { font-family: var(--font-mono); font-size: 12px; }
|
| 758 |
+
.text-success { color: var(--success); }
|
| 759 |
+
.text-error { color: var(--error); }
|
| 760 |
+
.text-muted { color: var(--foreground-subtle); }
|
| 761 |
+
|
| 762 |
+
/* =============================================================
|
| 763 |
+
ENTRANCE ANIMATIONS
|
| 764 |
+
============================================================= */
|
| 765 |
+
@keyframes fadeInUp {
|
| 766 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 767 |
+
to { opacity: 1; transform: translateY(0); }
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
#dashboard.animate-in .filter-bar { animation: fadeInUp 0.35s ease-out both; }
|
| 771 |
+
#dashboard.animate-in .section-nav { animation: fadeInUp 0.35s ease-out 0.04s both; }
|
| 772 |
+
#dashboard.animate-in .dash-section:nth-of-type(1) { animation: fadeInUp 0.4s ease-out 0.08s both; }
|
| 773 |
+
#dashboard.animate-in .dash-section:nth-of-type(2) { animation: fadeInUp 0.4s ease-out 0.14s both; }
|
| 774 |
+
#dashboard.animate-in .dash-section:nth-of-type(3) { animation: fadeInUp 0.4s ease-out 0.20s both; }
|
| 775 |
+
#dashboard.animate-in .dash-section:nth-of-type(4) { animation: fadeInUp 0.4s ease-out 0.26s both; }
|
| 776 |
+
#dashboard.animate-in .dash-section:nth-of-type(5) { animation: fadeInUp 0.4s ease-out 0.32s both; }
|
| 777 |
+
|
| 778 |
+
/* =============================================================
|
| 779 |
+
RESPONSIVE
|
| 780 |
+
============================================================= */
|
| 781 |
+
@media (max-width: 1024px) {
|
| 782 |
+
.charts-grid { grid-template-columns: 1fr; }
|
| 783 |
+
.summary-grid { grid-template-columns: 1fr 1fr; }
|
| 784 |
+
}
|
| 785 |
+
|
| 786 |
+
@media (max-width: 768px) {
|
| 787 |
+
.header-inner { padding: 0 16px; height: 48px; }
|
| 788 |
+
.header-title { font-size: 14px; }
|
| 789 |
+
|
| 790 |
+
.filter-bar { padding: 16px; }
|
| 791 |
+
.filter-bar-inner { flex-direction: column; align-items: stretch; gap: 12px; }
|
| 792 |
+
.filter-select { width: 100%; min-width: auto; }
|
| 793 |
+
.quant-dropdown-btn { width: 100%; }
|
| 794 |
+
.filter-actions { justify-content: flex-start; }
|
| 795 |
+
|
| 796 |
+
.section-nav-track { padding: 0 16px; }
|
| 797 |
+
|
| 798 |
+
.container { padding: 0 16px; }
|
| 799 |
+
.section-header { margin-bottom: 16px; }
|
| 800 |
+
.table-card { border-radius: var(--radius-md); }
|
| 801 |
+
.results-wrapper { max-height: 450px; }
|
| 802 |
+
|
| 803 |
+
.summary-grid { grid-template-columns: 1fr; gap: 12px; }
|
| 804 |
+
.stat-card { padding: 16px 20px; }
|
| 805 |
+
.stat-card-value { font-size: 24px; }
|
| 806 |
+
|
| 807 |
+
.charts-grid { gap: 12px; }
|
| 808 |
+
.chart-box { height: 280px; padding: 16px; }
|
| 809 |
+
|
| 810 |
+
.machine-grid { grid-template-columns: 1fr; }
|
| 811 |
+
|
| 812 |
+
.dash-section { padding: 24px 0; }
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
@media (max-width: 480px) {
|
| 816 |
+
.stat-card { flex-direction: column; gap: 10px; }
|
| 817 |
+
.stat-card-icon { width: 32px; height: 32px; }
|
| 818 |
+
.stat-card-icon svg { width: 16px; height: 16px; }
|
| 819 |
+
}
|
| 820 |
+
|
| 821 |
+
/* =============================================================
|
| 822 |
+
METHODOLOGY PAGE
|
| 823 |
+
============================================================= */
|
| 824 |
+
.methodology-content {
|
| 825 |
+
max-width: 800px;
|
| 826 |
+
margin: 0 auto;
|
| 827 |
+
padding: 40px 32px 80px;
|
| 828 |
+
}
|
| 829 |
+
.methodology-content .back-link {
|
| 830 |
+
display: inline-flex;
|
| 831 |
+
align-items: center;
|
| 832 |
+
gap: 6px;
|
| 833 |
+
margin-bottom: 32px;
|
| 834 |
+
font-size: 13px;
|
| 835 |
+
font-weight: 500;
|
| 836 |
+
color: var(--foreground-muted);
|
| 837 |
+
transition: color var(--transition-fast);
|
| 838 |
+
}
|
| 839 |
+
.methodology-content .back-link:hover { color: var(--foreground); text-decoration: none; }
|
| 840 |
+
|
| 841 |
+
.methodology-content h2 {
|
| 842 |
+
margin-top: 48px;
|
| 843 |
+
margin-bottom: 16px;
|
| 844 |
+
font-size: 20px;
|
| 845 |
+
font-weight: 700;
|
| 846 |
+
color: var(--foreground);
|
| 847 |
+
letter-spacing: -0.01em;
|
| 848 |
+
}
|
| 849 |
+
.methodology-content h2:first-of-type { margin-top: 0; }
|
| 850 |
+
|
| 851 |
+
.methodology-content h3 {
|
| 852 |
+
margin-top: 28px;
|
| 853 |
+
margin-bottom: 8px;
|
| 854 |
+
font-size: 15px;
|
| 855 |
+
font-weight: 600;
|
| 856 |
+
color: var(--foreground-muted);
|
| 857 |
+
}
|
| 858 |
+
.methodology-content p,
|
| 859 |
+
.methodology-content li {
|
| 860 |
+
line-height: 1.8;
|
| 861 |
+
margin-bottom: 8px;
|
| 862 |
+
color: var(--foreground-secondary);
|
| 863 |
+
font-size: 14px;
|
| 864 |
+
}
|
| 865 |
+
.methodology-content ol,
|
| 866 |
+
.methodology-content ul {
|
| 867 |
+
padding-left: 24px;
|
| 868 |
+
}
|
| 869 |
+
.methodology-content strong {
|
| 870 |
+
color: var(--foreground);
|
| 871 |
+
font-weight: 600;
|
| 872 |
+
}
|
| 873 |
+
.methodology-content code {
|
| 874 |
+
background: var(--surface-2);
|
| 875 |
+
padding: 2px 8px;
|
| 876 |
+
border-radius: var(--radius-sm);
|
| 877 |
+
font-family: var(--font-mono);
|
| 878 |
+
font-size: 12px;
|
| 879 |
+
color: var(--foreground-secondary);
|
| 880 |
+
}
|
| 881 |
+
.methodology-content table {
|
| 882 |
+
width: 100%;
|
| 883 |
+
margin: 16px 0;
|
| 884 |
+
border-collapse: separate;
|
| 885 |
+
border-spacing: 0;
|
| 886 |
+
border: 1px solid var(--border);
|
| 887 |
+
border-radius: var(--radius-md);
|
| 888 |
+
overflow: hidden;
|
| 889 |
+
}
|
| 890 |
+
.methodology-content th {
|
| 891 |
+
text-align: left;
|
| 892 |
+
padding: 10px 16px;
|
| 893 |
+
background: var(--surface-1);
|
| 894 |
+
color: var(--foreground-muted);
|
| 895 |
+
font-size: 11px;
|
| 896 |
+
font-weight: 600;
|
| 897 |
+
text-transform: uppercase;
|
| 898 |
+
letter-spacing: 0.04em;
|
| 899 |
+
border-bottom: 1px solid var(--border);
|
| 900 |
+
}
|
| 901 |
+
.methodology-content td {
|
| 902 |
+
padding: 10px 16px;
|
| 903 |
+
border-bottom: 1px solid var(--border);
|
| 904 |
+
font-size: 13px;
|
| 905 |
+
color: var(--foreground-secondary);
|
| 906 |
+
}
|
| 907 |
+
.methodology-content tbody tr:last-child td { border-bottom: none; }
|
| 908 |
+
.methodology-content tbody tr:hover { background: var(--surface-2); }
|
| 909 |
+
|
| 910 |
+
/* =============================================================
|
| 911 |
+
RUN TAB (one-click benchmark)
|
| 912 |
+
============================================================= */
|
| 913 |
+
/* Global [hidden] override — the attribute's UA default (display:none) loses
|
| 914 |
+
to any component rule that sets display explicitly (e.g. .btn → inline-flex,
|
| 915 |
+
.run-pages-banner → flex). Restore the invariant with one rule. */
|
| 916 |
+
[hidden] { display: none !important; }
|
| 917 |
+
|
| 918 |
+
.card {
|
| 919 |
+
background: var(--surface-1);
|
| 920 |
+
border: 1px solid var(--border);
|
| 921 |
+
border-radius: var(--radius-lg);
|
| 922 |
+
padding: 16px 20px;
|
| 923 |
+
}
|
| 924 |
+
|
| 925 |
+
/* Shared button primitives. Stay consistent with .filter-reset-btn height. */
|
| 926 |
+
.btn {
|
| 927 |
+
display: inline-flex;
|
| 928 |
+
align-items: center;
|
| 929 |
+
gap: 6px;
|
| 930 |
+
height: 36px;
|
| 931 |
+
padding: 0 14px;
|
| 932 |
+
border-radius: var(--radius-md);
|
| 933 |
+
border: 1px solid transparent;
|
| 934 |
+
font-family: var(--font-sans);
|
| 935 |
+
font-size: 13px;
|
| 936 |
+
font-weight: 600;
|
| 937 |
+
cursor: pointer;
|
| 938 |
+
transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast), opacity var(--transition-fast);
|
| 939 |
+
white-space: nowrap;
|
| 940 |
+
}
|
| 941 |
+
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
| 942 |
+
|
| 943 |
+
.btn-primary {
|
| 944 |
+
background: var(--foreground);
|
| 945 |
+
color: var(--background);
|
| 946 |
+
}
|
| 947 |
+
.btn-primary:hover:not(:disabled) { background: var(--foreground-secondary); }
|
| 948 |
+
|
| 949 |
+
.btn-secondary {
|
| 950 |
+
background: transparent;
|
| 951 |
+
color: var(--foreground-secondary);
|
| 952 |
+
border-color: var(--border);
|
| 953 |
+
}
|
| 954 |
+
.btn-secondary:hover:not(:disabled) {
|
| 955 |
+
background: var(--surface-2);
|
| 956 |
+
color: var(--foreground);
|
| 957 |
+
border-color: var(--border-hover);
|
| 958 |
+
}
|
| 959 |
+
|
| 960 |
+
.btn-danger {
|
| 961 |
+
background: var(--error-bg);
|
| 962 |
+
color: var(--error);
|
| 963 |
+
border-color: var(--error);
|
| 964 |
+
}
|
| 965 |
+
.btn-danger:hover:not(:disabled) { background: var(--error); color: var(--background); }
|
| 966 |
+
|
| 967 |
+
/* Run-mode badge colored by surface. */
|
| 968 |
+
.run-mode-badge {
|
| 969 |
+
text-transform: lowercase;
|
| 970 |
+
letter-spacing: 0.04em;
|
| 971 |
+
font-weight: 600;
|
| 972 |
+
}
|
| 973 |
+
.run-mode-localhost { background: var(--accent-blue-bg); color: var(--accent-blue); }
|
| 974 |
+
.run-mode-space { background: var(--accent-purple-bg); color: var(--accent-purple); }
|
| 975 |
+
.run-mode-pages { background: var(--surface-3); color: var(--foreground-subtle); }
|
| 976 |
+
.run-mode-file { background: var(--accent-amber-bg); color: var(--accent-amber); }
|
| 977 |
+
|
| 978 |
+
/* Pages-mode read-only banner. */
|
| 979 |
+
.run-pages-banner {
|
| 980 |
+
display: flex;
|
| 981 |
+
align-items: center;
|
| 982 |
+
gap: 12px;
|
| 983 |
+
margin-bottom: 20px;
|
| 984 |
+
padding: 12px 16px;
|
| 985 |
+
background: var(--accent-blue-bg);
|
| 986 |
+
color: var(--foreground-secondary);
|
| 987 |
+
border: 1px solid var(--border);
|
| 988 |
+
border-radius: var(--radius-md);
|
| 989 |
+
font-size: 13px;
|
| 990 |
+
}
|
| 991 |
+
.run-pages-banner a { font-weight: 600; }
|
| 992 |
+
|
| 993 |
+
/* HF hub sign-in/submit row (space surface only). */
|
| 994 |
+
.hub-row { margin-bottom: 20px; }
|
| 995 |
+
.hub-row-inner {
|
| 996 |
+
display: flex;
|
| 997 |
+
align-items: center;
|
| 998 |
+
justify-content: space-between;
|
| 999 |
+
gap: 16px;
|
| 1000 |
+
flex-wrap: wrap;
|
| 1001 |
+
}
|
| 1002 |
+
.hub-row-info {
|
| 1003 |
+
display: flex;
|
| 1004 |
+
align-items: center;
|
| 1005 |
+
gap: 8px;
|
| 1006 |
+
color: var(--foreground-muted);
|
| 1007 |
+
font-size: 13px;
|
| 1008 |
+
font-family: var(--font-mono);
|
| 1009 |
+
}
|
| 1010 |
+
.hub-row-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
| 1011 |
+
|
| 1012 |
+
/* Device / budget stat cards (reuses .summary-grid + .stat-card shells). */
|
| 1013 |
+
.run-device-grid {
|
| 1014 |
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
| 1015 |
+
margin-bottom: 24px;
|
| 1016 |
+
}
|
| 1017 |
+
.run-device-card { flex-direction: column; align-items: stretch; gap: 10px; }
|
| 1018 |
+
.run-device-card .stat-card-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; }
|
| 1019 |
+
.run-device-rows { display: flex; flex-direction: column; gap: 4px; }
|
| 1020 |
+
.run-device-row {
|
| 1021 |
+
display: flex;
|
| 1022 |
+
justify-content: space-between;
|
| 1023 |
+
font-size: 13px;
|
| 1024 |
+
font-family: var(--font-mono);
|
| 1025 |
+
}
|
| 1026 |
+
.run-device-row-label { color: var(--foreground-muted); }
|
| 1027 |
+
.run-device-row-value { color: var(--foreground); font-weight: 600; }
|
| 1028 |
+
.run-device-note { font-size: 11px; color: var(--foreground-subtle); margin-top: 2px; }
|
| 1029 |
+
|
| 1030 |
+
/* Hide filters + iterations + action buttons — stacks with .filter-bar tokens. */
|
| 1031 |
+
.run-filters { align-items: center; }
|
| 1032 |
+
.run-filters-checks {
|
| 1033 |
+
display: flex;
|
| 1034 |
+
align-items: center;
|
| 1035 |
+
gap: 12px;
|
| 1036 |
+
flex-wrap: wrap;
|
| 1037 |
+
}
|
| 1038 |
+
.run-hide-label {
|
| 1039 |
+
display: inline-flex;
|
| 1040 |
+
align-items: center;
|
| 1041 |
+
gap: 6px;
|
| 1042 |
+
font-size: 13px;
|
| 1043 |
+
color: var(--foreground-secondary);
|
| 1044 |
+
cursor: pointer;
|
| 1045 |
+
}
|
| 1046 |
+
.run-hide-label input[type="checkbox"] { accent-color: var(--foreground); }
|
| 1047 |
+
.run-iter-input {
|
| 1048 |
+
width: 72px;
|
| 1049 |
+
min-width: 0;
|
| 1050 |
+
padding: 0 10px;
|
| 1051 |
+
background: var(--surface-1);
|
| 1052 |
+
background-image: none;
|
| 1053 |
+
}
|
| 1054 |
+
.run-actions {
|
| 1055 |
+
display: flex;
|
| 1056 |
+
gap: 8px;
|
| 1057 |
+
align-items: center;
|
| 1058 |
+
flex-wrap: wrap;
|
| 1059 |
+
margin-left: auto;
|
| 1060 |
+
}
|
| 1061 |
+
#queue-status { font-size: 12px; color: var(--foreground-muted); font-family: var(--font-mono); }
|
| 1062 |
+
|
| 1063 |
+
/* Family cards + variant rows. */
|
| 1064 |
+
.run-models-stack {
|
| 1065 |
+
display: flex;
|
| 1066 |
+
flex-direction: column;
|
| 1067 |
+
gap: 12px;
|
| 1068 |
+
margin-bottom: 24px;
|
| 1069 |
+
}
|
| 1070 |
+
.run-family { padding: 0; overflow: hidden; }
|
| 1071 |
+
.run-family > summary {
|
| 1072 |
+
list-style: none;
|
| 1073 |
+
cursor: pointer;
|
| 1074 |
+
display: flex;
|
| 1075 |
+
align-items: center;
|
| 1076 |
+
gap: 10px;
|
| 1077 |
+
padding: 12px 16px;
|
| 1078 |
+
background: var(--surface-1);
|
| 1079 |
+
border-bottom: 1px solid transparent;
|
| 1080 |
+
transition: background var(--transition-fast), border-color var(--transition-fast);
|
| 1081 |
+
}
|
| 1082 |
+
.run-family[open] > summary {
|
| 1083 |
+
border-bottom-color: var(--border);
|
| 1084 |
+
background: var(--surface-2);
|
| 1085 |
+
}
|
| 1086 |
+
.run-family > summary::-webkit-details-marker { display: none; }
|
| 1087 |
+
.run-family-chevron {
|
| 1088 |
+
display: inline-block;
|
| 1089 |
+
width: 10px;
|
| 1090 |
+
height: 10px;
|
| 1091 |
+
border-right: 1.5px solid var(--foreground-muted);
|
| 1092 |
+
border-bottom: 1.5px solid var(--foreground-muted);
|
| 1093 |
+
transform: rotate(-45deg);
|
| 1094 |
+
transition: transform var(--transition-fast);
|
| 1095 |
+
flex-shrink: 0;
|
| 1096 |
+
}
|
| 1097 |
+
.run-family[open] > summary .run-family-chevron { transform: rotate(45deg); }
|
| 1098 |
+
.run-family-select-all { margin: 0; accent-color: var(--foreground); cursor: pointer; }
|
| 1099 |
+
.run-family-name { font-size: 14px; font-weight: 700; color: var(--foreground); }
|
| 1100 |
+
.run-family-stats {
|
| 1101 |
+
color: var(--foreground-muted);
|
| 1102 |
+
font-size: 12px;
|
| 1103 |
+
font-family: var(--font-mono);
|
| 1104 |
+
}
|
| 1105 |
+
.run-family-warning {
|
| 1106 |
+
margin-left: auto;
|
| 1107 |
+
color: var(--accent-amber);
|
| 1108 |
+
font-size: 11px;
|
| 1109 |
+
font-weight: 600;
|
| 1110 |
+
}
|
| 1111 |
+
|
| 1112 |
+
.run-variant-list {
|
| 1113 |
+
display: flex;
|
| 1114 |
+
flex-direction: column;
|
| 1115 |
+
}
|
| 1116 |
+
.run-variant-row {
|
| 1117 |
+
display: grid;
|
| 1118 |
+
grid-template-columns: 20px minmax(60px, max-content) minmax(0, 1fr) 80px auto;
|
| 1119 |
+
gap: 12px;
|
| 1120 |
+
align-items: center;
|
| 1121 |
+
padding: 8px 16px;
|
| 1122 |
+
font-size: 13px;
|
| 1123 |
+
cursor: pointer;
|
| 1124 |
+
border-bottom: 1px solid var(--border);
|
| 1125 |
+
transition: background var(--transition-fast);
|
| 1126 |
+
}
|
| 1127 |
+
.run-variant-row:last-child { border-bottom: none; }
|
| 1128 |
+
.run-variant-row:hover { background: var(--surface-2); }
|
| 1129 |
+
.run-variant-row.is-non-fit { opacity: 0.45; }
|
| 1130 |
+
.run-variant-select { margin: 0; accent-color: var(--foreground); cursor: pointer; }
|
| 1131 |
+
.run-variant-quant {
|
| 1132 |
+
font-weight: 700;
|
| 1133 |
+
color: var(--foreground);
|
| 1134 |
+
font-family: var(--font-mono);
|
| 1135 |
+
font-size: 12px;
|
| 1136 |
+
}
|
| 1137 |
+
.run-variant-file {
|
| 1138 |
+
background: transparent;
|
| 1139 |
+
padding: 0;
|
| 1140 |
+
font-size: 12px;
|
| 1141 |
+
color: var(--foreground-muted);
|
| 1142 |
+
overflow: hidden;
|
| 1143 |
+
text-overflow: ellipsis;
|
| 1144 |
+
white-space: nowrap;
|
| 1145 |
+
}
|
| 1146 |
+
.run-variant-size {
|
| 1147 |
+
color: var(--foreground-muted);
|
| 1148 |
+
font-variant-numeric: tabular-nums;
|
| 1149 |
+
font-family: var(--font-mono);
|
| 1150 |
+
font-size: 12px;
|
| 1151 |
+
text-align: right;
|
| 1152 |
+
}
|
| 1153 |
+
.run-variant-badges {
|
| 1154 |
+
display: inline-flex;
|
| 1155 |
+
gap: 4px;
|
| 1156 |
+
flex-wrap: wrap;
|
| 1157 |
+
justify-content: flex-end;
|
| 1158 |
+
}
|
| 1159 |
+
|
| 1160 |
+
.badge--cached { background: var(--success-bg); color: var(--success); }
|
| 1161 |
+
.badge--warn { background: var(--accent-amber-bg); color: var(--accent-amber); }
|
| 1162 |
+
|
| 1163 |
+
/* Progress table — piggybacks on .results-table styling. */
|
| 1164 |
+
.run-progress-table { font-variant-numeric: tabular-nums; }
|
| 1165 |
+
.run-progress-table th.num,
|
| 1166 |
+
.run-progress-table td.num { text-align: right; }
|
| 1167 |
+
.run-row-queued { color: var(--foreground-subtle); }
|
| 1168 |
+
.run-row-running td { background: var(--accent-blue-bg); }
|
| 1169 |
+
.run-row-ok { color: var(--foreground-secondary); }
|
| 1170 |
+
.run-row-error td { color: var(--error); }
|
| 1171 |
+
.run-row-error td.err { font-family: var(--font-mono); font-size: 12px; }
|
| 1172 |
+
|
| 1173 |
+
/* Output block. */
|
| 1174 |
+
.run-output { display: flex; flex-direction: column; gap: 12px; padding: 16px; }
|
| 1175 |
+
.run-output-toggle {
|
| 1176 |
+
display: inline-flex;
|
| 1177 |
+
align-items: center;
|
| 1178 |
+
gap: 8px;
|
| 1179 |
+
font-size: 13px;
|
| 1180 |
+
color: var(--foreground-secondary);
|
| 1181 |
+
cursor: pointer;
|
| 1182 |
+
}
|
| 1183 |
+
.run-output-toggle input[type="checkbox"] { accent-color: var(--foreground); }
|
| 1184 |
+
.run-output-textarea {
|
| 1185 |
+
width: 100%;
|
| 1186 |
+
min-height: 320px;
|
| 1187 |
+
padding: 12px;
|
| 1188 |
+
background: var(--surface-0);
|
| 1189 |
+
color: var(--foreground);
|
| 1190 |
+
border: 1px solid var(--border);
|
| 1191 |
+
border-radius: var(--radius-md);
|
| 1192 |
+
font-family: var(--font-mono);
|
| 1193 |
+
font-size: 12px;
|
| 1194 |
+
line-height: 1.6;
|
| 1195 |
+
resize: vertical;
|
| 1196 |
+
}
|
| 1197 |
+
.run-output-textarea:focus {
|
| 1198 |
+
outline: none;
|
| 1199 |
+
border-color: var(--ring);
|
| 1200 |
+
box-shadow: 0 0 0 2px rgba(82, 82, 91, 0.3);
|
| 1201 |
+
}
|
| 1202 |
+
.run-output-buttons { display: flex; gap: 8px; flex-wrap: wrap; }
|
| 1203 |
+
|
| 1204 |
+
/* Log panel — collapsible details inside a .card. */
|
| 1205 |
+
.run-log { padding: 0; }
|
| 1206 |
+
.run-log > summary {
|
| 1207 |
+
list-style: none;
|
| 1208 |
+
cursor: pointer;
|
| 1209 |
+
padding: 12px 16px;
|
| 1210 |
+
color: var(--foreground-muted);
|
| 1211 |
+
font-size: 13px;
|
| 1212 |
+
font-weight: 500;
|
| 1213 |
+
transition: color var(--transition-fast);
|
| 1214 |
+
}
|
| 1215 |
+
.run-log > summary::-webkit-details-marker { display: none; }
|
| 1216 |
+
.run-log > summary:hover { color: var(--foreground); }
|
| 1217 |
+
.run-log[open] > summary { border-bottom: 1px solid var(--border); }
|
| 1218 |
+
.run-log-pre {
|
| 1219 |
+
max-height: 300px;
|
| 1220 |
+
overflow-y: auto;
|
| 1221 |
+
margin: 0;
|
| 1222 |
+
padding: 12px 16px;
|
| 1223 |
+
background: var(--surface-0);
|
| 1224 |
+
color: var(--foreground-muted);
|
| 1225 |
+
font-family: var(--font-mono);
|
| 1226 |
+
font-size: 11px;
|
| 1227 |
+
line-height: 1.6;
|
| 1228 |
+
white-space: pre-wrap;
|
| 1229 |
+
word-break: break-word;
|
| 1230 |
+
}
|
| 1231 |
+
|
| 1232 |
+
@media (max-width: 768px) {
|
| 1233 |
+
.run-actions { margin-left: 0; width: 100%; }
|
| 1234 |
+
.run-variant-row {
|
| 1235 |
+
grid-template-columns: 20px minmax(50px, max-content) minmax(0, 1fr);
|
| 1236 |
+
row-gap: 4px;
|
| 1237 |
+
}
|
| 1238 |
+
.run-variant-size { grid-column: 2; text-align: left; }
|
| 1239 |
+
.run-variant-badges { grid-column: 3; justify-content: flex-start; }
|
| 1240 |
+
.hub-row-inner { flex-direction: column; align-items: stretch; }
|
| 1241 |
+
.hub-row-actions { justify-content: flex-start; }
|
| 1242 |
+
}
|
harness.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
// Thin adapter: reads URL params, calls runBenchmarkCore(), writes to
|
| 2 |
// window.__BENCH so runner.js (Playwright) can poll. Inference logic lives
|
| 3 |
-
// in
|
| 4 |
|
| 5 |
-
import { runBenchmarkCore } from './
|
| 6 |
-
import { localSource } from './
|
| 7 |
|
| 8 |
// Global error handlers — catch Emscripten abort() which may not throw.
|
| 9 |
window.addEventListener('error', (e) => {
|
|
|
|
| 1 |
// Thin adapter: reads URL params, calls runBenchmarkCore(), writes to
|
| 2 |
// window.__BENCH so runner.js (Playwright) can poll. Inference logic lives
|
| 3 |
+
// in site/js/run/core.js, shared with the interactive Run-tab controller.
|
| 4 |
|
| 5 |
+
import { runBenchmarkCore } from './js/run/core.js';
|
| 6 |
+
import { localSource } from './js/run/source.js';
|
| 7 |
|
| 8 |
// Global error handlers — catch Emscripten abort() which may not throw.
|
| 9 |
window.addEventListener('error', (e) => {
|
index.html
CHANGED
|
@@ -1,99 +1,202 @@
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
<head>
|
| 4 |
-
<meta charset="
|
| 5 |
-
<
|
| 6 |
-
<meta name="
|
| 7 |
-
<
|
| 8 |
-
<
|
| 9 |
-
|
| 10 |
-
<
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
}
|
| 15 |
-
}
|
| 16 |
-
</script>
|
| 17 |
</head>
|
| 18 |
<body>
|
| 19 |
-
<header>
|
| 20 |
-
<div class="
|
| 21 |
-
<
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
</div>
|
| 24 |
-
<div id="device-line" class="muted">Detecting device…</div>
|
| 25 |
-
<div id="budget-line" class="muted"></div>
|
| 26 |
</header>
|
| 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 |
-
</tr>
|
| 72 |
-
</thead>
|
| 73 |
-
<tbody></tbody>
|
| 74 |
-
</table>
|
| 75 |
-
</section>
|
| 76 |
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
</div>
|
| 90 |
-
</section>
|
| 91 |
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
-
<script type="module" src="
|
| 98 |
</body>
|
| 99 |
</html>
|
|
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" data-theme="light">
|
| 3 |
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<meta name="color-scheme" content="light dark">
|
| 7 |
+
<script>(function(){var t=localStorage.getItem('theme')||'light';document.documentElement.setAttribute('data-theme',t);})();</script>
|
| 8 |
+
<title>WebGPU Bench</title>
|
| 9 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 11 |
+
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
| 12 |
+
<link rel="stylesheet" href="css/style.css">
|
| 13 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
|
|
|
|
|
|
|
|
|
| 14 |
</head>
|
| 15 |
<body>
|
| 16 |
+
<header class="header">
|
| 17 |
+
<div class="header-inner">
|
| 18 |
+
<a href="/" class="header-brand">
|
| 19 |
+
<svg class="header-logo" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/></svg>
|
| 20 |
+
<span class="header-title">WebGPU Bench</span>
|
| 21 |
+
</a>
|
| 22 |
+
<nav class="header-nav">
|
| 23 |
+
<a href="run/" class="header-link">Run</a>
|
| 24 |
+
<a href="methodology.html" class="header-link">Methodology</a>
|
| 25 |
+
<button id="theme-toggle" class="header-link theme-toggle-btn" type="button" title="Toggle theme">
|
| 26 |
+
<svg class="icon-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
| 27 |
+
<svg class="icon-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
| 28 |
+
</button>
|
| 29 |
+
<a href="https://github.com/abhijitramesh/webgpu-bench" target="_blank" rel="noopener" class="header-link">
|
| 30 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.44 9.8 8.2 11.39.6.11.82-.26.82-.58v-2.03c-3.34.73-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.09-.74.08-.73.08-.73 1.2.09 1.84 1.24 1.84 1.24 1.07 1.83 2.81 1.3 3.5 1 .11-.78.42-1.3.76-1.6-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.13-.3-.54-1.52.12-3.18 0 0 1.01-.32 3.3 1.23a11.5 11.5 0 0 1 6.02 0c2.28-1.55 3.29-1.23 3.29-1.23.66 1.66.25 2.88.12 3.18.77.84 1.24 1.91 1.24 3.22 0 4.61-2.81 5.63-5.48 5.92.43.37.81 1.1.81 2.22v3.29c0 .32.22.7.82.58C20.57 21.8 24 17.3 24 12c0-6.63-5.37-12-12-12z"/></svg>
|
| 31 |
+
GitHub
|
| 32 |
+
</a>
|
| 33 |
+
</nav>
|
| 34 |
</div>
|
|
|
|
|
|
|
| 35 |
</header>
|
| 36 |
|
| 37 |
+
<main>
|
| 38 |
+
<div id="loading" class="loading-state">
|
| 39 |
+
<div class="loading-content">
|
| 40 |
+
<div class="loading-spinner"></div>
|
| 41 |
+
<p>Loading benchmark data...</p>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
|
| 45 |
+
<div id="dashboard" style="display: none;">
|
| 46 |
+
<!-- Filter Bar -->
|
| 47 |
+
<div class="filter-bar">
|
| 48 |
+
<div class="filter-bar-inner">
|
| 49 |
+
<div class="filter-group">
|
| 50 |
+
<label class="filter-label" for="filter-machine">Machine</label>
|
| 51 |
+
<select id="filter-machine" class="filter-select"></select>
|
| 52 |
+
</div>
|
| 53 |
+
<div class="filter-group">
|
| 54 |
+
<label class="filter-label" for="filter-browser">Browser</label>
|
| 55 |
+
<select id="filter-browser" class="filter-select"></select>
|
| 56 |
+
</div>
|
| 57 |
+
<div class="filter-group">
|
| 58 |
+
<label class="filter-label" for="filter-model">Model</label>
|
| 59 |
+
<select id="filter-model" class="filter-select"></select>
|
| 60 |
+
</div>
|
| 61 |
+
<div class="filter-group">
|
| 62 |
+
<label class="filter-label" for="filter-backend">Backend</label>
|
| 63 |
+
<select id="filter-backend" class="filter-select"></select>
|
| 64 |
+
</div>
|
| 65 |
+
<div class="filter-group">
|
| 66 |
+
<label class="filter-label" for="filter-status">Status</label>
|
| 67 |
+
<select id="filter-status" class="filter-select"></select>
|
| 68 |
+
</div>
|
| 69 |
+
<div class="filter-group">
|
| 70 |
+
<label class="filter-label">Quantization</label>
|
| 71 |
+
<div class="quant-dropdown" id="quant-dropdown">
|
| 72 |
+
<button class="quant-dropdown-btn" id="quant-dropdown-btn" type="button">
|
| 73 |
+
<span id="quant-dropdown-text">All Quants</span>
|
| 74 |
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
| 75 |
+
</button>
|
| 76 |
+
<div class="quant-dropdown-list" id="quant-options"></div>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
<div class="filter-actions">
|
| 80 |
+
<button class="filter-reset-btn" id="filter-reset" type="button" style="display: none;" title="Reset all filters">
|
| 81 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
|
| 82 |
+
Reset
|
| 83 |
+
</button>
|
| 84 |
+
</div>
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
|
| 88 |
+
<!-- Section Navigation -->
|
| 89 |
+
<nav class="section-nav" id="section-nav">
|
| 90 |
+
<div class="section-nav-track">
|
| 91 |
+
<button class="section-nav-item active" data-section="overview">Overview</button>
|
| 92 |
+
<button class="section-nav-item" data-section="results-section">Results</button>
|
| 93 |
+
<button class="section-nav-item" data-section="performance-section">Performance</button>
|
| 94 |
+
<button class="section-nav-item" data-section="errors-section">Errors</button>
|
| 95 |
+
<button class="section-nav-item" data-section="machines-section">Machines</button>
|
| 96 |
+
</div>
|
| 97 |
+
</nav>
|
| 98 |
|
| 99 |
+
<!-- Overview -->
|
| 100 |
+
<section id="overview" class="dash-section">
|
| 101 |
+
<div class="container">
|
| 102 |
+
<div class="summary-grid">
|
| 103 |
+
<div class="stat-card">
|
| 104 |
+
<div class="stat-card-icon stat-card-icon--machines">
|
| 105 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
|
| 106 |
+
</div>
|
| 107 |
+
<div class="stat-card-content">
|
| 108 |
+
<span class="stat-card-label">Machines Tested</span>
|
| 109 |
+
<span class="stat-card-value" id="stat-machines">0</span>
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
<div class="stat-card">
|
| 113 |
+
<div class="stat-card-icon stat-card-icon--benchmarks">
|
| 114 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
|
| 115 |
+
</div>
|
| 116 |
+
<div class="stat-card-content">
|
| 117 |
+
<span class="stat-card-label">Benchmarks</span>
|
| 118 |
+
<span class="stat-card-value" id="stat-benchmarks">0</span>
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
<div class="stat-card">
|
| 122 |
+
<div class="stat-card-icon stat-card-icon--pass">
|
| 123 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
| 124 |
+
</div>
|
| 125 |
+
<div class="stat-card-content">
|
| 126 |
+
<span class="stat-card-label">Pass Rate</span>
|
| 127 |
+
<span class="stat-card-value" id="stat-pass-rate">0%</span>
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
</section>
|
| 133 |
|
| 134 |
+
<!-- Results -->
|
| 135 |
+
<section id="results-section" class="dash-section">
|
| 136 |
+
<div class="container">
|
| 137 |
+
<div class="section-header">
|
| 138 |
+
<h2>Results</h2>
|
| 139 |
+
<span class="results-count" id="results-count"></span>
|
| 140 |
+
</div>
|
| 141 |
+
<div class="table-card">
|
| 142 |
+
<div class="results-wrapper" id="results-table"></div>
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
+
</section>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
+
<!-- Performance Charts -->
|
| 148 |
+
<section id="performance-section" class="dash-section">
|
| 149 |
+
<div class="container">
|
| 150 |
+
<div class="section-header">
|
| 151 |
+
<h2>Performance</h2>
|
| 152 |
+
</div>
|
| 153 |
+
<div class="charts-grid">
|
| 154 |
+
<div class="chart-box"><canvas id="chart-decode"></canvas></div>
|
| 155 |
+
<div class="chart-box"><canvas id="chart-prefill"></canvas></div>
|
| 156 |
+
<div class="chart-box"><canvas id="chart-size"></canvas></div>
|
| 157 |
+
<div class="chart-box" id="machine-chart-section"><canvas id="chart-machine"></canvas></div>
|
| 158 |
+
</div>
|
|
|
|
|
|
|
| 159 |
|
| 160 |
+
<div class="section-header" style="margin-top: 32px;">
|
| 161 |
+
<h3 class="subsection-title">CPU vs WebGPU</h3>
|
| 162 |
+
<div class="metric-selector">
|
| 163 |
+
<span class="metric-selector-label">Metric</span>
|
| 164 |
+
<select id="cpu-gpu-metric" class="filter-select">
|
| 165 |
+
<option value="decode_tok_s">Decode tok/s</option>
|
| 166 |
+
<option value="prefill_tok_s">Prefill tok/s</option>
|
| 167 |
+
</select>
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
<div class="charts-grid">
|
| 171 |
+
<div class="chart-box"><canvas id="chart-cpu-gpu"></canvas></div>
|
| 172 |
+
<div class="chart-box"><canvas id="chart-speedup"></canvas></div>
|
| 173 |
+
</div>
|
| 174 |
+
<div id="cpu-gpu-table" style="margin-top: 16px;"></div>
|
| 175 |
+
</div>
|
| 176 |
+
</section>
|
| 177 |
+
|
| 178 |
+
<!-- Error Analysis -->
|
| 179 |
+
<section id="errors-section" class="dash-section">
|
| 180 |
+
<div class="container">
|
| 181 |
+
<div class="section-header">
|
| 182 |
+
<h2>Error Analysis</h2>
|
| 183 |
+
</div>
|
| 184 |
+
<div id="error-table"></div>
|
| 185 |
+
</div>
|
| 186 |
+
</section>
|
| 187 |
+
|
| 188 |
+
<!-- Machines -->
|
| 189 |
+
<section id="machines-section" class="dash-section">
|
| 190 |
+
<div class="container">
|
| 191 |
+
<div class="section-header">
|
| 192 |
+
<h2>Machines</h2>
|
| 193 |
+
</div>
|
| 194 |
+
<div id="machine-info"></div>
|
| 195 |
+
</div>
|
| 196 |
+
</section>
|
| 197 |
+
</div>
|
| 198 |
+
</main>
|
| 199 |
|
| 200 |
+
<script type="module" src="js/app.js"></script>
|
| 201 |
</body>
|
| 202 |
</html>
|
js/app.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { loadData, filterResults } from './data.js';
|
| 2 |
+
import { initFilters, populateQuantOptions, getFilters, resetFilters } from './filters.js';
|
| 3 |
+
import { renderDecodeChart, renderPrefillChart, renderSizeChart, renderMachineChart, renderCpuGpuChart, renderSpeedupChart } from './charts.js';
|
| 4 |
+
import { renderResultsTable, renderErrorTable, renderMachineInfo, renderCpuGpuTable } from './tables.js';
|
| 5 |
+
|
| 6 |
+
let appData = null;
|
| 7 |
+
|
| 8 |
+
async function init() {
|
| 9 |
+
try {
|
| 10 |
+
appData = await loadData();
|
| 11 |
+
} catch (e) {
|
| 12 |
+
document.getElementById('loading').innerHTML = `
|
| 13 |
+
<div class="loading-content">
|
| 14 |
+
<p class="loading-error">Failed to load data</p>
|
| 15 |
+
<p class="loading-hint">Run: <code>node scripts/build-site.js</code></p>
|
| 16 |
+
</div>
|
| 17 |
+
`;
|
| 18 |
+
return;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
// Hide loading, show dashboard with entrance animation
|
| 22 |
+
const loading = document.getElementById('loading');
|
| 23 |
+
const dashboard = document.getElementById('dashboard');
|
| 24 |
+
loading.style.display = 'none';
|
| 25 |
+
dashboard.style.display = '';
|
| 26 |
+
requestAnimationFrame(() => dashboard.classList.add('animate-in'));
|
| 27 |
+
|
| 28 |
+
// Populate quant options from actual data
|
| 29 |
+
populateQuantOptions(appData.results);
|
| 30 |
+
|
| 31 |
+
// Init filter dropdowns
|
| 32 |
+
initFilters(appData.meta, () => render());
|
| 33 |
+
|
| 34 |
+
// Wire theme toggle
|
| 35 |
+
document.getElementById('theme-toggle')?.addEventListener('click', () => {
|
| 36 |
+
const next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
| 37 |
+
document.documentElement.setAttribute('data-theme', next);
|
| 38 |
+
localStorage.setItem('theme', next);
|
| 39 |
+
if (appData) render();
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
// Wire reset button
|
| 43 |
+
const resetBtn = document.getElementById('filter-reset');
|
| 44 |
+
if (resetBtn) {
|
| 45 |
+
resetBtn.addEventListener('click', () => {
|
| 46 |
+
resetFilters();
|
| 47 |
+
render();
|
| 48 |
+
});
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// Wire metric selector for CPU vs GPU section
|
| 52 |
+
const metricSelect = document.getElementById('cpu-gpu-metric');
|
| 53 |
+
if (metricSelect) {
|
| 54 |
+
metricSelect.addEventListener('change', () => render());
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// Init section navigation
|
| 58 |
+
initSectionNav();
|
| 59 |
+
|
| 60 |
+
// Initial render
|
| 61 |
+
render();
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
function render() {
|
| 65 |
+
// Sync Chart.js defaults with current theme
|
| 66 |
+
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
| 67 |
+
Chart.defaults.color = isDark ? '#a1a1aa' : '#71717a';
|
| 68 |
+
Chart.defaults.plugins.tooltip.backgroundColor = isDark ? 'rgba(15,15,18,0.95)' : 'rgba(255,255,255,0.95)';
|
| 69 |
+
Chart.defaults.plugins.tooltip.borderColor = isDark ? '#27272a' : '#e4e4e7';
|
| 70 |
+
Chart.defaults.plugins.tooltip.titleColor = isDark ? '#e4e4e7' : '#09090b';
|
| 71 |
+
Chart.defaults.plugins.tooltip.bodyColor = isDark ? '#a1a1aa' : '#71717a';
|
| 72 |
+
|
| 73 |
+
const filters = getFilters();
|
| 74 |
+
const filtered = filterResults(appData.results, filters);
|
| 75 |
+
|
| 76 |
+
// Summary cards
|
| 77 |
+
const passed = filtered.filter(r => r.status === 'done');
|
| 78 |
+
document.getElementById('stat-machines').textContent = appData.meta.machines.length;
|
| 79 |
+
document.getElementById('stat-benchmarks').textContent = filtered.length;
|
| 80 |
+
const passRate = filtered.length > 0 ? ((passed.length / filtered.length) * 100).toFixed(0) : '0';
|
| 81 |
+
document.getElementById('stat-pass-rate').textContent = `${passRate}%`;
|
| 82 |
+
|
| 83 |
+
// Results count
|
| 84 |
+
const countEl = document.getElementById('results-count');
|
| 85 |
+
if (countEl) {
|
| 86 |
+
const total = appData.results.length;
|
| 87 |
+
countEl.textContent = filtered.length === total
|
| 88 |
+
? `${total} total`
|
| 89 |
+
: `${filtered.length} of ${total}`;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// Reset button visibility
|
| 93 |
+
const resetBtn = document.getElementById('filter-reset');
|
| 94 |
+
if (resetBtn) {
|
| 95 |
+
const active = filters.machine !== 'all' || filters.browser !== 'all' ||
|
| 96 |
+
filters.model !== 'all' || filters.backend !== 'all' ||
|
| 97 |
+
filters.status !== 'all' || filters.quants.size > 0;
|
| 98 |
+
resetBtn.style.display = active ? '' : 'none';
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// Tables
|
| 102 |
+
renderResultsTable(filtered);
|
| 103 |
+
renderErrorTable(filtered);
|
| 104 |
+
renderMachineInfo(appData.meta.machines);
|
| 105 |
+
|
| 106 |
+
// Charts
|
| 107 |
+
renderDecodeChart(filtered);
|
| 108 |
+
renderPrefillChart(filtered);
|
| 109 |
+
renderSizeChart(filtered);
|
| 110 |
+
renderMachineChart(filtered, appData.meta.machines);
|
| 111 |
+
|
| 112 |
+
// CPU vs GPU comparison
|
| 113 |
+
const metric = document.getElementById('cpu-gpu-metric')?.value || 'decode_tok_s';
|
| 114 |
+
renderCpuGpuChart(filtered, metric);
|
| 115 |
+
renderSpeedupChart(filtered, metric);
|
| 116 |
+
renderCpuGpuTable(filtered);
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
function initSectionNav() {
|
| 120 |
+
const nav = document.getElementById('section-nav');
|
| 121 |
+
if (!nav) return;
|
| 122 |
+
|
| 123 |
+
const buttons = nav.querySelectorAll('.section-nav-item');
|
| 124 |
+
const sections = [];
|
| 125 |
+
|
| 126 |
+
buttons.forEach(btn => {
|
| 127 |
+
const sectionId = btn.dataset.section;
|
| 128 |
+
const section = document.getElementById(sectionId);
|
| 129 |
+
if (section) sections.push({ btn, section });
|
| 130 |
+
|
| 131 |
+
btn.addEventListener('click', (e) => {
|
| 132 |
+
e.preventDefault();
|
| 133 |
+
if (section) {
|
| 134 |
+
const navHeight = nav.offsetHeight;
|
| 135 |
+
const top = section.getBoundingClientRect().top + window.scrollY - navHeight;
|
| 136 |
+
window.scrollTo({ top, behavior: 'smooth' });
|
| 137 |
+
}
|
| 138 |
+
});
|
| 139 |
+
});
|
| 140 |
+
|
| 141 |
+
// Scroll spy with IntersectionObserver
|
| 142 |
+
if (sections.length === 0) return;
|
| 143 |
+
|
| 144 |
+
const observer = new IntersectionObserver(
|
| 145 |
+
(entries) => {
|
| 146 |
+
for (const entry of entries) {
|
| 147 |
+
if (entry.isIntersecting) {
|
| 148 |
+
const id = entry.target.id;
|
| 149 |
+
buttons.forEach(b => b.classList.toggle('active', b.dataset.section === id));
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
},
|
| 153 |
+
{ rootMargin: '-20% 0px -60% 0px' }
|
| 154 |
+
);
|
| 155 |
+
|
| 156 |
+
sections.forEach(({ section }) => observer.observe(section));
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
init();
|
js/charts.js
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { BROWSER_COLORS, quantSortKey, groupBy, formatTokS } from './utils.js';
|
| 2 |
+
|
| 3 |
+
// Global Chart.js theme
|
| 4 |
+
Chart.defaults.font.family = "'Manrope', system-ui, -apple-system, sans-serif";
|
| 5 |
+
Chart.defaults.color = '#a1a1aa';
|
| 6 |
+
Chart.defaults.plugins.tooltip.backgroundColor = 'rgba(15, 15, 18, 0.95)';
|
| 7 |
+
Chart.defaults.plugins.tooltip.borderColor = '#27272a';
|
| 8 |
+
Chart.defaults.plugins.tooltip.borderWidth = 1;
|
| 9 |
+
Chart.defaults.plugins.tooltip.cornerRadius = 8;
|
| 10 |
+
Chart.defaults.plugins.tooltip.padding = { top: 8, bottom: 8, left: 12, right: 12 };
|
| 11 |
+
Chart.defaults.plugins.tooltip.titleFont = { weight: '600', size: 13 };
|
| 12 |
+
Chart.defaults.plugins.tooltip.bodyFont = { family: "'JetBrains Mono', monospace", size: 12 };
|
| 13 |
+
Chart.defaults.plugins.legend.labels.boxWidth = 12;
|
| 14 |
+
Chart.defaults.plugins.legend.labels.boxHeight = 12;
|
| 15 |
+
Chart.defaults.plugins.legend.labels.padding = 16;
|
| 16 |
+
|
| 17 |
+
const chartInstances = new Map();
|
| 18 |
+
|
| 19 |
+
function destroyChart(id) {
|
| 20 |
+
if (chartInstances.has(id)) {
|
| 21 |
+
chartInstances.get(id).destroy();
|
| 22 |
+
chartInstances.delete(id);
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
function themeColors() {
|
| 27 |
+
const dark = document.documentElement.getAttribute('data-theme') === 'dark';
|
| 28 |
+
return {
|
| 29 |
+
grid: dark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)',
|
| 30 |
+
text: dark ? '#a1a1aa' : '#71717a',
|
| 31 |
+
title: dark ? '#e4e4e7' : '#09090b',
|
| 32 |
+
};
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
function darkScales(xTitle, yTitle) {
|
| 36 |
+
const c = themeColors();
|
| 37 |
+
return {
|
| 38 |
+
x: {
|
| 39 |
+
ticks: { color: c.text },
|
| 40 |
+
grid: { color: c.grid },
|
| 41 |
+
title: xTitle ? { display: true, text: xTitle, color: c.text } : undefined,
|
| 42 |
+
},
|
| 43 |
+
y: {
|
| 44 |
+
ticks: { color: c.text },
|
| 45 |
+
grid: { color: c.grid },
|
| 46 |
+
title: yTitle ? { display: true, text: yTitle, color: c.text } : undefined,
|
| 47 |
+
beginAtZero: true,
|
| 48 |
+
},
|
| 49 |
+
};
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
function darkLegend() {
|
| 53 |
+
const c = themeColors();
|
| 54 |
+
return { labels: { color: c.text, usePointStyle: true, pointStyle: 'circle' } };
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
function titleConfig(text) {
|
| 58 |
+
const c = themeColors();
|
| 59 |
+
return { display: true, text, color: c.title, font: { size: 14, weight: '600' } };
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
export function renderDecodeChart(results) {
|
| 63 |
+
const canvasId = 'chart-decode';
|
| 64 |
+
destroyChart(canvasId);
|
| 65 |
+
const canvas = document.getElementById(canvasId);
|
| 66 |
+
if (!canvas) return;
|
| 67 |
+
|
| 68 |
+
const passed = results.filter(r => r.status === 'done' && r.decode_tok_s != null);
|
| 69 |
+
if (passed.length === 0) {
|
| 70 |
+
canvas.parentElement.querySelector('.chart-empty')?.remove();
|
| 71 |
+
const msg = document.createElement('div');
|
| 72 |
+
msg.className = 'chart-empty';
|
| 73 |
+
msg.textContent = 'No data';
|
| 74 |
+
canvas.parentElement.appendChild(msg);
|
| 75 |
+
return;
|
| 76 |
+
}
|
| 77 |
+
canvas.parentElement.querySelector('.chart-empty')?.remove();
|
| 78 |
+
|
| 79 |
+
const byBrowser = groupBy(passed, 'browser');
|
| 80 |
+
const allQuants = [...new Set(passed.map(r => r.variant))].sort((a, b) => quantSortKey(a) - quantSortKey(b));
|
| 81 |
+
|
| 82 |
+
const datasets = Object.entries(byBrowser).map(([browser, items]) => {
|
| 83 |
+
const byQuant = groupBy(items, 'variant');
|
| 84 |
+
return {
|
| 85 |
+
label: browser,
|
| 86 |
+
backgroundColor: BROWSER_COLORS[browser] || '#888',
|
| 87 |
+
data: allQuants.map(q => {
|
| 88 |
+
const group = byQuant[q];
|
| 89 |
+
if (!group) return null;
|
| 90 |
+
const vals = group.map(r => r.decode_tok_s).filter(v => v != null);
|
| 91 |
+
return vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : null;
|
| 92 |
+
}),
|
| 93 |
+
};
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
chartInstances.set(canvasId, new Chart(canvas, {
|
| 97 |
+
type: 'bar',
|
| 98 |
+
data: { labels: allQuants, datasets },
|
| 99 |
+
options: {
|
| 100 |
+
responsive: true,
|
| 101 |
+
maintainAspectRatio: false,
|
| 102 |
+
plugins: {
|
| 103 |
+
title: titleConfig('Decode Throughput by Quantization'),
|
| 104 |
+
legend: darkLegend(),
|
| 105 |
+
tooltip: {
|
| 106 |
+
callbacks: { label: ctx => `${ctx.dataset.label}: ${formatTokS(ctx.raw)} tok/s` },
|
| 107 |
+
},
|
| 108 |
+
},
|
| 109 |
+
scales: darkScales('Quantization', 'Decode tok/s'),
|
| 110 |
+
},
|
| 111 |
+
}));
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
export function renderPrefillChart(results) {
|
| 115 |
+
const canvasId = 'chart-prefill';
|
| 116 |
+
destroyChart(canvasId);
|
| 117 |
+
const canvas = document.getElementById(canvasId);
|
| 118 |
+
if (!canvas) return;
|
| 119 |
+
|
| 120 |
+
const passed = results.filter(r => r.status === 'done' && r.prefill_tok_s != null);
|
| 121 |
+
if (passed.length === 0) {
|
| 122 |
+
canvas.parentElement.querySelector('.chart-empty')?.remove();
|
| 123 |
+
const msg = document.createElement('div');
|
| 124 |
+
msg.className = 'chart-empty';
|
| 125 |
+
msg.textContent = 'No data';
|
| 126 |
+
canvas.parentElement.appendChild(msg);
|
| 127 |
+
return;
|
| 128 |
+
}
|
| 129 |
+
canvas.parentElement.querySelector('.chart-empty')?.remove();
|
| 130 |
+
|
| 131 |
+
const byBrowser = groupBy(passed, 'browser');
|
| 132 |
+
const allQuants = [...new Set(passed.map(r => r.variant))].sort((a, b) => quantSortKey(a) - quantSortKey(b));
|
| 133 |
+
|
| 134 |
+
const datasets = Object.entries(byBrowser).map(([browser, items]) => {
|
| 135 |
+
const byQuant = groupBy(items, 'variant');
|
| 136 |
+
return {
|
| 137 |
+
label: browser,
|
| 138 |
+
backgroundColor: BROWSER_COLORS[browser] || '#888',
|
| 139 |
+
data: allQuants.map(q => {
|
| 140 |
+
const group = byQuant[q];
|
| 141 |
+
if (!group) return null;
|
| 142 |
+
const vals = group.map(r => r.prefill_tok_s).filter(v => v != null);
|
| 143 |
+
return vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : null;
|
| 144 |
+
}),
|
| 145 |
+
};
|
| 146 |
+
});
|
| 147 |
+
|
| 148 |
+
chartInstances.set(canvasId, new Chart(canvas, {
|
| 149 |
+
type: 'bar',
|
| 150 |
+
data: { labels: allQuants, datasets },
|
| 151 |
+
options: {
|
| 152 |
+
responsive: true,
|
| 153 |
+
maintainAspectRatio: false,
|
| 154 |
+
plugins: {
|
| 155 |
+
title: titleConfig('Prefill Throughput by Quantization'),
|
| 156 |
+
legend: darkLegend(),
|
| 157 |
+
tooltip: {
|
| 158 |
+
callbacks: { label: ctx => `${ctx.dataset.label}: ${formatTokS(ctx.raw)} tok/s` },
|
| 159 |
+
},
|
| 160 |
+
},
|
| 161 |
+
scales: darkScales('Quantization', 'Prefill tok/s'),
|
| 162 |
+
},
|
| 163 |
+
}));
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
export function renderSizeChart(results) {
|
| 167 |
+
const canvasId = 'chart-size';
|
| 168 |
+
destroyChart(canvasId);
|
| 169 |
+
const canvas = document.getElementById(canvasId);
|
| 170 |
+
if (!canvas) return;
|
| 171 |
+
|
| 172 |
+
const passed = results.filter(r => r.status === 'done' && r.decode_tok_s != null && r.sizeMB);
|
| 173 |
+
if (passed.length === 0) {
|
| 174 |
+
canvas.parentElement.querySelector('.chart-empty')?.remove();
|
| 175 |
+
const msg = document.createElement('div');
|
| 176 |
+
msg.className = 'chart-empty';
|
| 177 |
+
msg.textContent = 'No data';
|
| 178 |
+
canvas.parentElement.appendChild(msg);
|
| 179 |
+
return;
|
| 180 |
+
}
|
| 181 |
+
canvas.parentElement.querySelector('.chart-empty')?.remove();
|
| 182 |
+
|
| 183 |
+
const byBrowser = groupBy(passed, 'browser');
|
| 184 |
+
|
| 185 |
+
const datasets = Object.entries(byBrowser).map(([browser, items]) => {
|
| 186 |
+
const sorted = [...items].sort((a, b) => a.sizeMB - b.sizeMB);
|
| 187 |
+
return {
|
| 188 |
+
label: browser,
|
| 189 |
+
borderColor: BROWSER_COLORS[browser] || '#888',
|
| 190 |
+
backgroundColor: BROWSER_COLORS[browser] || '#888',
|
| 191 |
+
data: sorted.map(r => ({ x: r.sizeMB, y: r.decode_tok_s })),
|
| 192 |
+
showLine: true,
|
| 193 |
+
pointRadius: 4,
|
| 194 |
+
tension: 0.2,
|
| 195 |
+
};
|
| 196 |
+
});
|
| 197 |
+
|
| 198 |
+
chartInstances.set(canvasId, new Chart(canvas, {
|
| 199 |
+
type: 'scatter',
|
| 200 |
+
data: { datasets },
|
| 201 |
+
options: {
|
| 202 |
+
responsive: true,
|
| 203 |
+
maintainAspectRatio: false,
|
| 204 |
+
plugins: {
|
| 205 |
+
title: titleConfig('Throughput vs Model Size'),
|
| 206 |
+
legend: darkLegend(),
|
| 207 |
+
tooltip: {
|
| 208 |
+
callbacks: {
|
| 209 |
+
label: ctx => `${ctx.dataset.label}: ${ctx.parsed.x}MB \u2192 ${formatTokS(ctx.parsed.y)} tok/s`,
|
| 210 |
+
},
|
| 211 |
+
},
|
| 212 |
+
},
|
| 213 |
+
scales: darkScales('Model Size (MB)', 'Decode tok/s'),
|
| 214 |
+
},
|
| 215 |
+
}));
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
const CPU_COLOR = 'rgba(245, 158, 11, 0.75)';
|
| 219 |
+
|
| 220 |
+
function showEmptyState(canvas, msg) {
|
| 221 |
+
canvas.parentElement.querySelector('.chart-empty')?.remove();
|
| 222 |
+
const el = document.createElement('div');
|
| 223 |
+
el.className = 'chart-empty';
|
| 224 |
+
el.textContent = msg || 'No data';
|
| 225 |
+
canvas.parentElement.appendChild(el);
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
function clearEmptyState(canvas) {
|
| 229 |
+
canvas.parentElement.querySelector('.chart-empty')?.remove();
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
const METRIC_LABELS = {
|
| 233 |
+
decode_tok_s: 'Decode tok/s',
|
| 234 |
+
prefill_tok_s: 'Prefill tok/s',
|
| 235 |
+
};
|
| 236 |
+
|
| 237 |
+
function avgBy(items, field) {
|
| 238 |
+
const vals = items.map(r => r[field]).filter(v => v != null);
|
| 239 |
+
return vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : null;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
export function renderCpuGpuChart(results, metric = 'decode_tok_s') {
|
| 243 |
+
const canvasId = 'chart-cpu-gpu';
|
| 244 |
+
destroyChart(canvasId);
|
| 245 |
+
const canvas = document.getElementById(canvasId);
|
| 246 |
+
if (!canvas) return;
|
| 247 |
+
|
| 248 |
+
const passed = results.filter(r => r.status === 'done' && r[metric] != null);
|
| 249 |
+
const cpuResults = passed.filter(r => r.nGpuLayers === 0);
|
| 250 |
+
const gpuResults = passed.filter(r => r.nGpuLayers !== 0);
|
| 251 |
+
|
| 252 |
+
if (cpuResults.length === 0 || gpuResults.length === 0) {
|
| 253 |
+
showEmptyState(canvas, cpuResults.length === 0 ? 'No CPU baseline data in current filter' : 'No GPU data in current filter');
|
| 254 |
+
return;
|
| 255 |
+
}
|
| 256 |
+
clearEmptyState(canvas);
|
| 257 |
+
|
| 258 |
+
const cpuVariants = new Set(cpuResults.map(r => r.variant));
|
| 259 |
+
const allQuants = [...new Set(gpuResults.map(r => r.variant))]
|
| 260 |
+
.filter(q => cpuVariants.has(q))
|
| 261 |
+
.sort((a, b) => quantSortKey(a) - quantSortKey(b));
|
| 262 |
+
|
| 263 |
+
if (allQuants.length === 0) {
|
| 264 |
+
showEmptyState(canvas, 'No overlapping variants between CPU and GPU');
|
| 265 |
+
return;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
const cpuByVariant = groupBy(cpuResults, 'variant');
|
| 269 |
+
const cpuData = allQuants.map(q => avgBy(cpuByVariant[q] || [], metric));
|
| 270 |
+
|
| 271 |
+
const gpuByBrowser = groupBy(gpuResults, 'browser');
|
| 272 |
+
const gpuDatasets = Object.entries(gpuByBrowser).map(([browser, items]) => {
|
| 273 |
+
const byVariant = groupBy(items, 'variant');
|
| 274 |
+
return {
|
| 275 |
+
label: browser,
|
| 276 |
+
backgroundColor: BROWSER_COLORS[browser] || '#888',
|
| 277 |
+
data: allQuants.map(q => avgBy(byVariant[q] || [], metric)),
|
| 278 |
+
};
|
| 279 |
+
});
|
| 280 |
+
|
| 281 |
+
const metricLabel = METRIC_LABELS[metric] || metric;
|
| 282 |
+
chartInstances.set(canvasId, new Chart(canvas, {
|
| 283 |
+
type: 'bar',
|
| 284 |
+
data: { labels: allQuants, datasets: [{ label: 'CPU', backgroundColor: CPU_COLOR, data: cpuData }, ...gpuDatasets] },
|
| 285 |
+
options: {
|
| 286 |
+
responsive: true,
|
| 287 |
+
maintainAspectRatio: false,
|
| 288 |
+
plugins: {
|
| 289 |
+
title: titleConfig(`CPU vs WebGPU: ${metricLabel}`),
|
| 290 |
+
legend: darkLegend(),
|
| 291 |
+
tooltip: { callbacks: { label: ctx => `${ctx.dataset.label}: ${formatTokS(ctx.raw)} tok/s` } },
|
| 292 |
+
},
|
| 293 |
+
scales: darkScales('Quantization', metricLabel),
|
| 294 |
+
},
|
| 295 |
+
}));
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
export function renderSpeedupChart(results, metric = 'decode_tok_s') {
|
| 299 |
+
const canvasId = 'chart-speedup';
|
| 300 |
+
destroyChart(canvasId);
|
| 301 |
+
const canvas = document.getElementById(canvasId);
|
| 302 |
+
if (!canvas) return;
|
| 303 |
+
|
| 304 |
+
const passed = results.filter(r => r.status === 'done' && r[metric] != null);
|
| 305 |
+
const cpuResults = passed.filter(r => r.nGpuLayers === 0);
|
| 306 |
+
const gpuResults = passed.filter(r => r.nGpuLayers !== 0);
|
| 307 |
+
|
| 308 |
+
if (cpuResults.length === 0 || gpuResults.length === 0) {
|
| 309 |
+
showEmptyState(canvas, cpuResults.length === 0 ? 'No CPU baseline data in current filter' : 'No GPU data in current filter');
|
| 310 |
+
return;
|
| 311 |
+
}
|
| 312 |
+
clearEmptyState(canvas);
|
| 313 |
+
|
| 314 |
+
const cpuAvgByVariant = {};
|
| 315 |
+
for (const [q, items] of Object.entries(groupBy(cpuResults, 'variant'))) {
|
| 316 |
+
cpuAvgByVariant[q] = avgBy(items, metric);
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
const allQuants = [...new Set(gpuResults.map(r => r.variant))]
|
| 320 |
+
.filter(q => cpuAvgByVariant[q] != null)
|
| 321 |
+
.sort((a, b) => quantSortKey(a) - quantSortKey(b));
|
| 322 |
+
|
| 323 |
+
if (allQuants.length === 0) {
|
| 324 |
+
showEmptyState(canvas, 'No overlapping variants between CPU and GPU');
|
| 325 |
+
return;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
const gpuByBrowser = groupBy(gpuResults, 'browser');
|
| 329 |
+
const barDatasets = Object.entries(gpuByBrowser).map(([browser, items]) => {
|
| 330 |
+
const byVariant = groupBy(items, 'variant');
|
| 331 |
+
return {
|
| 332 |
+
label: browser,
|
| 333 |
+
backgroundColor: BROWSER_COLORS[browser] || '#888',
|
| 334 |
+
data: allQuants.map(q => {
|
| 335 |
+
const cpuAvg = cpuAvgByVariant[q];
|
| 336 |
+
const gpuAvg = avgBy(byVariant[q] || [], metric);
|
| 337 |
+
return cpuAvg && gpuAvg ? gpuAvg / cpuAvg : null;
|
| 338 |
+
}),
|
| 339 |
+
};
|
| 340 |
+
});
|
| 341 |
+
|
| 342 |
+
const refLine = {
|
| 343 |
+
label: '1\u00d7',
|
| 344 |
+
type: 'line',
|
| 345 |
+
data: allQuants.map(() => 1),
|
| 346 |
+
borderColor: 'rgba(255,255,255,0.3)',
|
| 347 |
+
borderDash: [4, 4],
|
| 348 |
+
borderWidth: 1.5,
|
| 349 |
+
pointRadius: 0,
|
| 350 |
+
fill: false,
|
| 351 |
+
order: -1,
|
| 352 |
+
};
|
| 353 |
+
|
| 354 |
+
const metricLabel = METRIC_LABELS[metric] || metric;
|
| 355 |
+
chartInstances.set(canvasId, new Chart(canvas, {
|
| 356 |
+
type: 'bar',
|
| 357 |
+
data: { labels: allQuants, datasets: [...barDatasets, refLine] },
|
| 358 |
+
options: {
|
| 359 |
+
responsive: true,
|
| 360 |
+
maintainAspectRatio: false,
|
| 361 |
+
plugins: {
|
| 362 |
+
title: titleConfig(`WebGPU Speedup over CPU (${metricLabel})`),
|
| 363 |
+
legend: {
|
| 364 |
+
...darkLegend(),
|
| 365 |
+
labels: { ...darkLegend().labels, filter: item => item.text !== '1\u00d7' },
|
| 366 |
+
},
|
| 367 |
+
tooltip: {
|
| 368 |
+
filter: item => item.dataset.label !== '1\u00d7',
|
| 369 |
+
callbacks: { label: ctx => `${ctx.dataset.label}: ${ctx.raw != null ? ctx.raw.toFixed(2) + '\u00d7' : '\u2014'}` },
|
| 370 |
+
},
|
| 371 |
+
},
|
| 372 |
+
scales: {
|
| 373 |
+
x: { ticks: { color: themeColors().text }, grid: { color: themeColors().grid }, title: { display: true, text: 'Quantization', color: themeColors().text } },
|
| 374 |
+
y: {
|
| 375 |
+
ticks: { color: themeColors().text, callback: v => `${v.toFixed(1)}\u00d7` },
|
| 376 |
+
grid: { color: themeColors().grid },
|
| 377 |
+
title: { display: true, text: 'Speedup (\u00d7)', color: themeColors().text },
|
| 378 |
+
beginAtZero: true,
|
| 379 |
+
},
|
| 380 |
+
},
|
| 381 |
+
},
|
| 382 |
+
}));
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
export function renderMachineChart(results, machines) {
|
| 386 |
+
const canvasId = 'chart-machine';
|
| 387 |
+
destroyChart(canvasId);
|
| 388 |
+
const canvas = document.getElementById(canvasId);
|
| 389 |
+
const container = document.getElementById('machine-chart-section');
|
| 390 |
+
if (!canvas || !container) return;
|
| 391 |
+
|
| 392 |
+
if (machines.length <= 1) {
|
| 393 |
+
container.style.display = 'none';
|
| 394 |
+
return;
|
| 395 |
+
}
|
| 396 |
+
container.style.display = '';
|
| 397 |
+
|
| 398 |
+
const passed = results.filter(r => r.status === 'done' && r.decode_tok_s != null);
|
| 399 |
+
const quantCounts = {};
|
| 400 |
+
for (const r of passed) quantCounts[r.variant] = (quantCounts[r.variant] || 0) + 1;
|
| 401 |
+
const targetQuant = quantCounts['Q4_K_M'] ? 'Q4_K_M' : Object.keys(quantCounts).sort((a, b) => quantCounts[b] - quantCounts[a])[0];
|
| 402 |
+
|
| 403 |
+
if (!targetQuant) {
|
| 404 |
+
container.style.display = 'none';
|
| 405 |
+
return;
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
const forQuant = passed.filter(r => r.variant === targetQuant);
|
| 409 |
+
const byMachine = groupBy(forQuant, 'machineSlug');
|
| 410 |
+
const machineLabels = Object.keys(byMachine);
|
| 411 |
+
const browsers = [...new Set(forQuant.map(r => r.browser))].sort();
|
| 412 |
+
|
| 413 |
+
const datasets = browsers.map(browser => ({
|
| 414 |
+
label: browser,
|
| 415 |
+
backgroundColor: BROWSER_COLORS[browser] || '#888',
|
| 416 |
+
data: machineLabels.map(slug => {
|
| 417 |
+
const items = byMachine[slug].filter(r => r.browser === browser);
|
| 418 |
+
if (!items.length) return null;
|
| 419 |
+
return items.reduce((s, r) => s + r.decode_tok_s, 0) / items.length;
|
| 420 |
+
}),
|
| 421 |
+
}));
|
| 422 |
+
|
| 423 |
+
chartInstances.set(canvasId, new Chart(canvas, {
|
| 424 |
+
type: 'bar',
|
| 425 |
+
data: { labels: machineLabels, datasets },
|
| 426 |
+
options: {
|
| 427 |
+
indexAxis: 'y',
|
| 428 |
+
responsive: true,
|
| 429 |
+
maintainAspectRatio: false,
|
| 430 |
+
plugins: {
|
| 431 |
+
title: titleConfig(`Machine Comparison (${targetQuant})`),
|
| 432 |
+
legend: darkLegend(),
|
| 433 |
+
tooltip: {
|
| 434 |
+
callbacks: { label: ctx => `${ctx.dataset.label}: ${formatTokS(ctx.raw)} tok/s` },
|
| 435 |
+
},
|
| 436 |
+
},
|
| 437 |
+
scales: darkScales('Decode tok/s', 'Machine'),
|
| 438 |
+
},
|
| 439 |
+
}));
|
| 440 |
+
}
|
js/data.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
let cachedData = null;
|
| 2 |
+
|
| 3 |
+
export async function loadData() {
|
| 4 |
+
if (cachedData) return cachedData;
|
| 5 |
+
const resp = await fetch('data/combined.json');
|
| 6 |
+
cachedData = await resp.json();
|
| 7 |
+
return cachedData;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export function filterResults(results, filters) {
|
| 11 |
+
return results.filter(r => {
|
| 12 |
+
if (filters.machine && filters.machine !== 'all' && r.machineSlug !== filters.machine) return false;
|
| 13 |
+
if (filters.browser && filters.browser !== 'all' && r.browser !== filters.browser) return false;
|
| 14 |
+
if (filters.model && filters.model !== 'all' && r.model !== filters.model) return false;
|
| 15 |
+
if (filters.backend && filters.backend !== 'all') {
|
| 16 |
+
if (filters.backend === 'cpu' && r.nGpuLayers !== 0) return false;
|
| 17 |
+
if (filters.backend === 'webgpu' && r.nGpuLayers === 0) return false;
|
| 18 |
+
}
|
| 19 |
+
if (filters.status && filters.status !== 'all') {
|
| 20 |
+
if (filters.status === 'pass' && r.status !== 'done') return false;
|
| 21 |
+
if (filters.status === 'fail' && r.status === 'done') return false;
|
| 22 |
+
}
|
| 23 |
+
if (filters.quants && filters.quants.size > 0 && !filters.quants.has(r.variant)) return false;
|
| 24 |
+
return true;
|
| 25 |
+
});
|
| 26 |
+
}
|
js/filters.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { QUANT_ORDER, quantSortKey } from './utils.js';
|
| 2 |
+
|
| 3 |
+
let selectedQuants = new Set();
|
| 4 |
+
let onChange = null;
|
| 5 |
+
|
| 6 |
+
export function initFilters(meta, onChangeCallback) {
|
| 7 |
+
onChange = onChangeCallback;
|
| 8 |
+
|
| 9 |
+
// Machine select
|
| 10 |
+
const machineSelect = document.getElementById('filter-machine');
|
| 11 |
+
machineSelect.innerHTML = '<option value="all">All Machines</option>';
|
| 12 |
+
for (const m of meta.machines) {
|
| 13 |
+
const opt = document.createElement('option');
|
| 14 |
+
opt.value = m.slug;
|
| 15 |
+
opt.textContent = `${m.cpus} (${m.totalMemoryGB}GB)`;
|
| 16 |
+
machineSelect.appendChild(opt);
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// Browser select
|
| 20 |
+
const browserSelect = document.getElementById('filter-browser');
|
| 21 |
+
browserSelect.innerHTML = '<option value="all">All Browsers</option>';
|
| 22 |
+
for (const b of meta.browsers) {
|
| 23 |
+
const opt = document.createElement('option');
|
| 24 |
+
opt.value = b;
|
| 25 |
+
opt.textContent = b.charAt(0).toUpperCase() + b.slice(1);
|
| 26 |
+
browserSelect.appendChild(opt);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// Model select
|
| 30 |
+
const modelSelect = document.getElementById('filter-model');
|
| 31 |
+
modelSelect.innerHTML = '<option value="all">All Models</option>';
|
| 32 |
+
for (const m of meta.models) {
|
| 33 |
+
const opt = document.createElement('option');
|
| 34 |
+
opt.value = m;
|
| 35 |
+
opt.textContent = m;
|
| 36 |
+
modelSelect.appendChild(opt);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// Backend select
|
| 40 |
+
const backendSelect = document.getElementById('filter-backend');
|
| 41 |
+
backendSelect.innerHTML = `
|
| 42 |
+
<option value="all">All Backends</option>
|
| 43 |
+
<option value="webgpu">WebGPU</option>
|
| 44 |
+
<option value="cpu">CPU</option>
|
| 45 |
+
`;
|
| 46 |
+
|
| 47 |
+
// Status select
|
| 48 |
+
const statusSelect = document.getElementById('filter-status');
|
| 49 |
+
statusSelect.innerHTML = `
|
| 50 |
+
<option value="all">All Status</option>
|
| 51 |
+
<option value="pass">Pass</option>
|
| 52 |
+
<option value="fail">Fail</option>
|
| 53 |
+
`;
|
| 54 |
+
|
| 55 |
+
// Quant multi-select
|
| 56 |
+
initQuantMultiSelect(meta);
|
| 57 |
+
|
| 58 |
+
// Wire up change events for single selects
|
| 59 |
+
for (const sel of [machineSelect, browserSelect, modelSelect, backendSelect, statusSelect]) {
|
| 60 |
+
sel.addEventListener('change', fireChange);
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
function initQuantMultiSelect(meta) {
|
| 65 |
+
const btn = document.getElementById('quant-dropdown-btn');
|
| 66 |
+
const dropdown = document.getElementById('quant-dropdown');
|
| 67 |
+
|
| 68 |
+
// Toggle dropdown
|
| 69 |
+
btn.addEventListener('click', (e) => {
|
| 70 |
+
e.stopPropagation();
|
| 71 |
+
dropdown.classList.toggle('open');
|
| 72 |
+
});
|
| 73 |
+
|
| 74 |
+
// Close on outside click
|
| 75 |
+
document.addEventListener('click', (e) => {
|
| 76 |
+
if (!dropdown.contains(e.target) && e.target !== btn) {
|
| 77 |
+
dropdown.classList.remove('open');
|
| 78 |
+
}
|
| 79 |
+
});
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
export function populateQuantOptions(results) {
|
| 83 |
+
const quantsInData = new Set(results.map(r => r.variant));
|
| 84 |
+
const sorted = [...quantsInData].sort((a, b) => quantSortKey(a) - quantSortKey(b));
|
| 85 |
+
|
| 86 |
+
const list = document.getElementById('quant-options');
|
| 87 |
+
list.innerHTML = '';
|
| 88 |
+
|
| 89 |
+
// Select All
|
| 90 |
+
const selectAllLabel = document.createElement('label');
|
| 91 |
+
selectAllLabel.className = 'quant-option select-all';
|
| 92 |
+
const selectAllCb = document.createElement('input');
|
| 93 |
+
selectAllCb.type = 'checkbox';
|
| 94 |
+
selectAllCb.checked = true;
|
| 95 |
+
selectAllCb.id = 'quant-select-all';
|
| 96 |
+
selectAllLabel.appendChild(selectAllCb);
|
| 97 |
+
selectAllLabel.appendChild(document.createTextNode(' Select All'));
|
| 98 |
+
list.appendChild(selectAllLabel);
|
| 99 |
+
|
| 100 |
+
const checkboxes = [];
|
| 101 |
+
|
| 102 |
+
for (const q of sorted) {
|
| 103 |
+
const label = document.createElement('label');
|
| 104 |
+
label.className = 'quant-option';
|
| 105 |
+
const cb = document.createElement('input');
|
| 106 |
+
cb.type = 'checkbox';
|
| 107 |
+
cb.checked = true;
|
| 108 |
+
cb.value = q;
|
| 109 |
+
cb.className = 'quant-cb';
|
| 110 |
+
label.appendChild(cb);
|
| 111 |
+
label.appendChild(document.createTextNode(` ${q}`));
|
| 112 |
+
list.appendChild(label);
|
| 113 |
+
checkboxes.push(cb);
|
| 114 |
+
|
| 115 |
+
cb.addEventListener('change', () => {
|
| 116 |
+
updateQuantSelection(checkboxes, selectAllCb);
|
| 117 |
+
fireChange();
|
| 118 |
+
});
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
selectAllCb.addEventListener('change', () => {
|
| 122 |
+
const checked = selectAllCb.checked;
|
| 123 |
+
for (const cb of checkboxes) cb.checked = checked;
|
| 124 |
+
updateQuantSelection(checkboxes, selectAllCb);
|
| 125 |
+
fireChange();
|
| 126 |
+
});
|
| 127 |
+
|
| 128 |
+
// Update button text
|
| 129 |
+
updateQuantButtonText(sorted.length, sorted.length);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
function updateQuantSelection(checkboxes, selectAllCb) {
|
| 133 |
+
selectedQuants = new Set();
|
| 134 |
+
let checkedCount = 0;
|
| 135 |
+
for (const cb of checkboxes) {
|
| 136 |
+
if (cb.checked) {
|
| 137 |
+
checkedCount++;
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
if (checkedCount === checkboxes.length) {
|
| 142 |
+
selectedQuants = new Set();
|
| 143 |
+
selectAllCb.checked = true;
|
| 144 |
+
selectAllCb.indeterminate = false;
|
| 145 |
+
} else if (checkedCount === 0) {
|
| 146 |
+
selectedQuants = new Set(['__none__']);
|
| 147 |
+
selectAllCb.checked = false;
|
| 148 |
+
selectAllCb.indeterminate = false;
|
| 149 |
+
} else {
|
| 150 |
+
selectedQuants = new Set();
|
| 151 |
+
for (const cb of checkboxes) {
|
| 152 |
+
if (cb.checked) selectedQuants.add(cb.value);
|
| 153 |
+
}
|
| 154 |
+
selectAllCb.checked = false;
|
| 155 |
+
selectAllCb.indeterminate = true;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
updateQuantButtonText(checkedCount, checkboxes.length);
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
function updateQuantButtonText(checked, total) {
|
| 162 |
+
const textEl = document.getElementById('quant-dropdown-text');
|
| 163 |
+
if (!textEl) return;
|
| 164 |
+
if (checked === total) {
|
| 165 |
+
textEl.textContent = 'All Quants';
|
| 166 |
+
} else if (checked === 0) {
|
| 167 |
+
textEl.textContent = 'No Quants';
|
| 168 |
+
} else {
|
| 169 |
+
textEl.textContent = `${checked}/${total} Quants`;
|
| 170 |
+
}
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
function fireChange() {
|
| 174 |
+
if (onChange) onChange(getFilters());
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
export function resetFilters() {
|
| 178 |
+
// Reset all select dropdowns
|
| 179 |
+
document.getElementById('filter-machine').value = 'all';
|
| 180 |
+
document.getElementById('filter-browser').value = 'all';
|
| 181 |
+
document.getElementById('filter-model').value = 'all';
|
| 182 |
+
document.getElementById('filter-backend').value = 'all';
|
| 183 |
+
document.getElementById('filter-status').value = 'all';
|
| 184 |
+
|
| 185 |
+
// Reset quant checkboxes
|
| 186 |
+
const selectAllCb = document.getElementById('quant-select-all');
|
| 187 |
+
const checkboxes = [...document.querySelectorAll('#quant-options .quant-cb')];
|
| 188 |
+
|
| 189 |
+
if (selectAllCb) {
|
| 190 |
+
selectAllCb.checked = true;
|
| 191 |
+
selectAllCb.indeterminate = false;
|
| 192 |
+
}
|
| 193 |
+
for (const cb of checkboxes) cb.checked = true;
|
| 194 |
+
|
| 195 |
+
// Reset internal state
|
| 196 |
+
selectedQuants = new Set();
|
| 197 |
+
updateQuantButtonText(checkboxes.length, checkboxes.length);
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
export function getFilters() {
|
| 201 |
+
return {
|
| 202 |
+
machine: document.getElementById('filter-machine').value,
|
| 203 |
+
browser: document.getElementById('filter-browser').value,
|
| 204 |
+
model: document.getElementById('filter-model').value,
|
| 205 |
+
backend: document.getElementById('filter-backend').value,
|
| 206 |
+
status: document.getElementById('filter-status').value,
|
| 207 |
+
quants: selectedQuants,
|
| 208 |
+
};
|
| 209 |
+
}
|
bench-config.js → js/run/config.js
RENAMED
|
File without changes
|
bench-app.js → js/run/controller.js
RENAMED
|
@@ -1,16 +1,15 @@
|
|
| 1 |
-
//
|
| 2 |
-
//
|
| 3 |
-
//
|
| 4 |
-
//
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
import {
|
| 8 |
-
import {
|
| 9 |
-
import { getDeviceBudgetMB, variantFits, describeDevice } from './bench-device.js';
|
| 10 |
import {
|
| 11 |
resumeHFSession, beginHFSignIn, signOutHF, submitResultsToDataset,
|
| 12 |
-
} from './
|
| 13 |
-
import { isHubConfigured, HF_DATASET_REPO } from './
|
| 14 |
|
| 15 |
const OVERHEAD = 1.5;
|
| 16 |
const DEFAULT_PROMPT =
|
|
@@ -25,42 +24,69 @@ const DEFAULT_ITERATIONS = 5;
|
|
| 25 |
const MIN_ITERATIONS_FOR_SUBMIT = 5;
|
| 26 |
|
| 27 |
const state = {
|
| 28 |
-
|
| 29 |
-
source: null,
|
| 30 |
-
models: null,
|
| 31 |
-
budget: null,
|
| 32 |
-
device: null,
|
| 33 |
-
cacheStatus: {},
|
| 34 |
-
variants: [],
|
| 35 |
running: false,
|
| 36 |
aborted: false,
|
| 37 |
-
results: [],
|
| 38 |
-
hfSession: null,
|
| 39 |
iterations: DEFAULT_ITERATIONS,
|
|
|
|
| 40 |
};
|
| 41 |
|
| 42 |
-
// ────────────────
|
| 43 |
|
| 44 |
-
async function
|
| 45 |
const params = new URLSearchParams(location.search);
|
| 46 |
-
if (params.get('mode') === '
|
| 47 |
-
if (params.get('mode') === '
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
}
|
| 54 |
|
|
|
|
|
|
|
| 55 |
async function loadModels() {
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
}
|
| 61 |
|
| 62 |
async function loadCacheStatus() {
|
| 63 |
-
if (state.
|
| 64 |
try {
|
| 65 |
const r = await fetch('/api/cache-status');
|
| 66 |
if (r.ok) return r.json();
|
|
@@ -124,31 +150,54 @@ function $(id) { return document.getElementById(id); }
|
|
| 124 |
function renderHeader() {
|
| 125 |
const d = state.device;
|
| 126 |
const b = state.budget;
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
const uaShort = d.userAgent.match(/(Firefox|Chrome|CriOS|Edg|Safari)\/[\d.]+/)?.[0] || 'browser';
|
| 130 |
const gpuStr = d.gpu
|
| 131 |
? [d.gpu.vendor, d.gpu.architecture, d.gpu.device].filter(Boolean).join(' ').trim()
|
| 132 |
-
: '
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
-
const pieces = [];
|
| 136 |
-
if (b.memGB !== null) pieces.push(`deviceMemory ${b.memGB} GB`);
|
| 137 |
-
if (b.quotaMB !== null) pieces.push(`storage quota ${(b.quotaMB / 1024).toFixed(1)} GB`);
|
| 138 |
const budgetGB = (b.budgetMB / 1024).toFixed(1);
|
| 139 |
-
$('
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
-
$('output-actions-local').hidden = state.mode !== 'local';
|
| 143 |
-
const hubSection = $('hub-section');
|
| 144 |
-
if (hubSection) hubSection.hidden = state.mode !== 'hosted';
|
| 145 |
const purgeBtn = $('btn-purge');
|
| 146 |
-
if (purgeBtn) purgeBtn.hidden = state.
|
|
|
|
| 147 |
renderHfSection();
|
| 148 |
}
|
| 149 |
|
| 150 |
function renderHfSection() {
|
| 151 |
-
if (state.
|
| 152 |
const signinBtn = $('btn-signin');
|
| 153 |
const submitBtn = $('btn-submit');
|
| 154 |
const userEl = $('hf-user');
|
|
@@ -157,7 +206,7 @@ function renderHfSection() {
|
|
| 157 |
if (!isHubConfigured()) {
|
| 158 |
signinBtn.disabled = true;
|
| 159 |
signinBtn.textContent = 'HF hub not configured';
|
| 160 |
-
signinBtn.title = 'Set HF_DATASET_REPO in
|
| 161 |
submitBtn.hidden = true;
|
| 162 |
userEl.textContent = '';
|
| 163 |
return;
|
|
@@ -186,7 +235,7 @@ function renderHfSection() {
|
|
| 186 |
}
|
| 187 |
|
| 188 |
function renderModels() {
|
| 189 |
-
const panel = $('
|
| 190 |
panel.innerHTML = '';
|
| 191 |
|
| 192 |
const groups = groupByFamily(state.variants);
|
|
@@ -195,71 +244,81 @@ function renderModels() {
|
|
| 195 |
const allFit = fitsCount === variants.length;
|
| 196 |
|
| 197 |
const familyEl = document.createElement('details');
|
| 198 |
-
familyEl.className = 'family';
|
| 199 |
familyEl.dataset.family = family;
|
| 200 |
familyEl.open = true;
|
| 201 |
|
| 202 |
const summary = document.createElement('summary');
|
|
|
|
| 203 |
summary.innerHTML = `
|
| 204 |
-
<
|
| 205 |
-
<
|
| 206 |
-
<span class="family-
|
|
|
|
| 207 |
`;
|
| 208 |
if (/^granite-4/i.test(family)) {
|
| 209 |
const w = document.createElement('span');
|
| 210 |
-
w.className = 'family-
|
| 211 |
w.textContent = '⚠ needs SSM_SCAN in llama.cpp';
|
| 212 |
summary.appendChild(w);
|
| 213 |
}
|
| 214 |
familyEl.appendChild(summary);
|
| 215 |
|
|
|
|
|
|
|
|
|
|
| 216 |
for (const v of variants) {
|
| 217 |
const row = document.createElement('label');
|
| 218 |
-
row.className = 'variant-row';
|
| 219 |
-
if (!variantFitsDevice(v)) row.classList.add('non-fit');
|
| 220 |
row.dataset.key = cacheKey(v);
|
| 221 |
|
| 222 |
const cb = document.createElement('input');
|
| 223 |
cb.type = 'checkbox';
|
| 224 |
-
cb.className = 'variant-select';
|
| 225 |
cb.dataset.key = cacheKey(v);
|
| 226 |
cb.checked = variantFitsDevice(v);
|
| 227 |
|
| 228 |
-
const
|
| 229 |
-
|
| 230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
|
| 232 |
const size = document.createElement('span');
|
| 233 |
-
size.className = 'size';
|
| 234 |
size.textContent = v.sizeMB > 0 ? formatSize(v.sizeMB) : '?';
|
| 235 |
|
| 236 |
const badges = document.createElement('span');
|
| 237 |
-
badges.className = 'badges';
|
| 238 |
updateBadgesForVariant(badges, v);
|
| 239 |
|
| 240 |
-
row.append(cb,
|
| 241 |
-
|
| 242 |
}
|
|
|
|
| 243 |
panel.appendChild(familyEl);
|
| 244 |
}
|
| 245 |
}
|
| 246 |
|
| 247 |
function updateBadgesForVariant(badgesEl, v) {
|
| 248 |
badgesEl.innerHTML = '';
|
| 249 |
-
if (isCached(v)) badgesEl.appendChild(makeBadge('cached', '
|
| 250 |
-
for (const w of v.warnings) badgesEl.appendChild(makeBadge(w, '
|
| 251 |
}
|
| 252 |
|
| 253 |
function refreshCacheBadge(v) {
|
| 254 |
-
const row = document.querySelector(`.variant-row[data-key="${cssEscape(cacheKey(v))}"]`);
|
| 255 |
if (!row) return;
|
| 256 |
-
const badges = row.querySelector('.badges');
|
| 257 |
if (badges) updateBadgesForVariant(badges, v);
|
| 258 |
}
|
| 259 |
|
| 260 |
function makeBadge(text, cls) {
|
| 261 |
const el = document.createElement('span');
|
| 262 |
-
el.className = cls;
|
| 263 |
el.textContent = text;
|
| 264 |
return el;
|
| 265 |
}
|
|
@@ -281,25 +340,32 @@ function cssEscape(s) {
|
|
| 281 |
// ──────────────── selection / filters ────────────────
|
| 282 |
|
| 283 |
function wireSelectionHandlers() {
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
|
|
|
|
|
|
| 289 |
);
|
| 290 |
-
rows.forEach(cb => { cb.checked =
|
| 291 |
updateButtons();
|
| 292 |
-
})
|
| 293 |
-
|
|
|
|
| 294 |
});
|
| 295 |
-
|
| 296 |
-
|
|
|
|
|
|
|
|
|
|
| 297 |
});
|
| 298 |
}
|
| 299 |
|
| 300 |
function wireFilters() {
|
| 301 |
['hide-ud', 'hide-iq', 'hide-hifp'].forEach(id => {
|
| 302 |
-
$(id)
|
|
|
|
| 303 |
});
|
| 304 |
}
|
| 305 |
|
|
@@ -321,10 +387,10 @@ function submittableResults() {
|
|
| 321 |
}
|
| 322 |
|
| 323 |
function applyFilters() {
|
| 324 |
-
const hideUd = $('hide-ud').checked;
|
| 325 |
-
const hideIq = $('hide-iq').checked;
|
| 326 |
-
const hideHifp = $('hide-hifp').checked;
|
| 327 |
-
document.querySelectorAll('.variant-row').forEach(row => {
|
| 328 |
const v = state.variants.find(x => cacheKey(x) === row.dataset.key);
|
| 329 |
if (!v) return;
|
| 330 |
const isUd = v.quant.startsWith('UD-');
|
|
@@ -336,7 +402,7 @@ function applyFilters() {
|
|
| 336 |
}
|
| 337 |
|
| 338 |
function getCheckedVariants() {
|
| 339 |
-
return Array.from(document.querySelectorAll('.variant-select:checked'))
|
| 340 |
.map(cb => state.variants.find(v => cacheKey(v) === cb.dataset.key))
|
| 341 |
.filter(Boolean);
|
| 342 |
}
|
|
@@ -344,26 +410,54 @@ function getCheckedVariants() {
|
|
| 344 |
function updateButtons() {
|
| 345 |
const checked = getCheckedVariants();
|
| 346 |
const cachedChecked = checked.filter(isCached);
|
| 347 |
-
$('btn-download').disabled = state.running || checked.length === 0;
|
| 348 |
-
$('btn-run').disabled = state.running || cachedChecked.length === 0;
|
| 349 |
-
$('btn-abort').disabled = !state.running;
|
| 350 |
-
$('queue-status')
|
| 351 |
-
|
| 352 |
-
|
|
|
|
|
|
|
|
|
|
| 353 |
}
|
| 354 |
|
| 355 |
// ──────────────── progress table ────────────────
|
| 356 |
|
| 357 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
|
| 359 |
function progressRowFor(v) {
|
| 360 |
const key = cacheKey(v);
|
| 361 |
-
const
|
|
|
|
| 362 |
let tr = tbody.querySelector(`tr[data-key="${cssEscape(key)}"]`);
|
| 363 |
if (!tr) {
|
| 364 |
tr = document.createElement('tr');
|
| 365 |
tr.dataset.key = key;
|
| 366 |
-
tr.className = 'row-queued';
|
| 367 |
tr.innerHTML = `
|
| 368 |
<td>${escapeText(v.modelName)}</td>
|
| 369 |
<td>${escapeText(v.quant)}</td>
|
|
@@ -377,7 +471,7 @@ function progressRowFor(v) {
|
|
| 377 |
}
|
| 378 |
return {
|
| 379 |
setStatus(status, msg) {
|
| 380 |
-
tr.className = `row-${rowClassFor(status)}`;
|
| 381 |
tr.querySelector('.status').textContent = msg ? `${status} — ${msg}` : status;
|
| 382 |
},
|
| 383 |
setProgress(fraction, downloaded, total) {
|
|
@@ -388,7 +482,7 @@ function progressRowFor(v) {
|
|
| 388 |
tr.querySelector('.status').textContent = detail ? `downloading ${detail}` : 'downloading';
|
| 389 |
},
|
| 390 |
fillFromRecord(record) {
|
| 391 |
-
tr.className = `row-${record.status === 'done' ? 'ok' : 'error'}`;
|
| 392 |
tr.querySelector('.status').textContent = record.status;
|
| 393 |
tr.querySelector('.prefill').textContent = record.metrics?.prefill_tok_s ?? '—';
|
| 394 |
tr.querySelector('.decode').textContent = record.metrics?.decode_tok_s ?? '—';
|
|
@@ -480,7 +574,6 @@ async function onDownloadClick() {
|
|
| 480 |
state.running = true;
|
| 481 |
state.aborted = false;
|
| 482 |
updateButtons();
|
| 483 |
-
showProgressPanel();
|
| 484 |
|
| 485 |
for (const v of variants) {
|
| 486 |
if (state.aborted) break;
|
|
@@ -508,9 +601,9 @@ async function onDownloadClick() {
|
|
| 508 |
}
|
| 509 |
}
|
| 510 |
|
| 511 |
-
// Refresh cache inventory
|
| 512 |
state.cacheStatus = await loadCacheStatus();
|
| 513 |
-
document.querySelectorAll('.variant-row').forEach(row => {
|
| 514 |
const v = state.variants.find(x => cacheKey(x) === row.dataset.key);
|
| 515 |
if (v) refreshCacheBadge(v);
|
| 516 |
});
|
|
@@ -529,7 +622,6 @@ async function onRunClick() {
|
|
| 529 |
state.aborted = false;
|
| 530 |
state.results = [];
|
| 531 |
updateButtons();
|
| 532 |
-
showProgressPanel();
|
| 533 |
|
| 534 |
const machine = await machineInfo();
|
| 535 |
const browser = browserInfo();
|
|
@@ -551,7 +643,7 @@ async function onRunClick() {
|
|
| 551 |
localStorage.setItem('webgpu-bench:lastRun', JSON.stringify(state.results));
|
| 552 |
} catch { /* quota */ }
|
| 553 |
|
| 554 |
-
if (state.
|
| 555 |
fetch('/api/results', {
|
| 556 |
method: 'POST',
|
| 557 |
headers: { 'Content-Type': 'application/json' },
|
|
@@ -627,9 +719,6 @@ async function runVariantWithIterations(v, row) {
|
|
| 627 |
nPredict: DEFAULT_N_PREDICT,
|
| 628 |
nCtx: DEFAULT_N_CTX,
|
| 629 |
nGpuLayers: DEFAULT_N_GPU_LAYERS,
|
| 630 |
-
// Only the first iteration runs the consistency check — the result
|
| 631 |
-
// is deterministic with greedy decoding, so subsequent iterations
|
| 632 |
-
// would just repeat the same check.
|
| 633 |
refTokenIds: i === 0 ? (refTokenIds || null) : null,
|
| 634 |
onStatus: (s, m) => row.setStatus(`gpu${i + 1}/${s}`, m),
|
| 635 |
onProgress: (fr, d, t) => row.setProgress(fr, d, t),
|
|
@@ -699,7 +788,6 @@ function makeRecord(v, vr, machine, browser, wallTimeMs) {
|
|
| 699 |
prefill_samples: vr.gpuSamples.map(s => round2(s.prefill_tok_s)),
|
| 700 |
decode_samples: vr.gpuSamples.map(s => round2(s.decode_tok_s)),
|
| 701 |
iterations: vr.iterations,
|
| 702 |
-
// Retain first-iteration detail for backward-compat with dashboard tables.
|
| 703 |
n_p_eval: first.n_p_eval,
|
| 704 |
n_eval: first.n_eval,
|
| 705 |
t_p_eval_ms: first.t_p_eval_ms,
|
|
@@ -733,7 +821,7 @@ function makeRecord(v, vr, machine, browser, wallTimeMs) {
|
|
| 733 |
cpu_baseline: cpuBaseline,
|
| 734 |
output: vr.gpuCore?.output || '',
|
| 735 |
machine,
|
| 736 |
-
source:
|
| 737 |
};
|
| 738 |
}
|
| 739 |
|
|
@@ -742,8 +830,8 @@ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
| 742 |
// ──────────────── Output ────────────────
|
| 743 |
|
| 744 |
function renderOutput() {
|
| 745 |
-
$('output-textarea')
|
| 746 |
-
|
| 747 |
}
|
| 748 |
|
| 749 |
function generateMarkdown(results) {
|
|
@@ -791,7 +879,7 @@ function generateMarkdown(results) {
|
|
| 791 |
}
|
| 792 |
|
| 793 |
function wireOutputHandlers() {
|
| 794 |
-
$('btn-copy').addEventListener('click', async () => {
|
| 795 |
const text = $('output-textarea').value;
|
| 796 |
try {
|
| 797 |
await navigator.clipboard.writeText(text);
|
|
@@ -802,7 +890,7 @@ function wireOutputHandlers() {
|
|
| 802 |
}
|
| 803 |
});
|
| 804 |
|
| 805 |
-
$('btn-download-json').addEventListener('click', () => {
|
| 806 |
if (state.results.length === 0) return;
|
| 807 |
const blob = new Blob([JSON.stringify(state.results, null, 2)], { type: 'application/json' });
|
| 808 |
const url = URL.createObjectURL(blob);
|
|
@@ -821,12 +909,13 @@ function flashButton(el, msg) {
|
|
| 821 |
setTimeout(() => { el.textContent = original; }, 1200);
|
| 822 |
}
|
| 823 |
|
| 824 |
-
// ──────────────── Abort ────────────────
|
| 825 |
|
| 826 |
function wireAbortHandler() {
|
| 827 |
-
$('btn-abort').addEventListener('click', () => {
|
| 828 |
state.aborted = true;
|
| 829 |
-
$('btn-abort')
|
|
|
|
| 830 |
logLine('Abort requested — will stop between variants.');
|
| 831 |
});
|
| 832 |
}
|
|
@@ -835,12 +924,12 @@ function wirePurgeHandler() {
|
|
| 835 |
const btn = $('btn-purge');
|
| 836 |
if (!btn) return;
|
| 837 |
btn.addEventListener('click', async () => {
|
| 838 |
-
if (state.
|
| 839 |
if (!confirm('Delete all cached GGUF files from OPFS? This frees browser storage but re-downloads will be needed.')) return;
|
| 840 |
try {
|
| 841 |
await purgeOpfs();
|
| 842 |
state.cacheStatus = {};
|
| 843 |
-
document.querySelectorAll('.variant-row').forEach(row => {
|
| 844 |
const v = state.variants.find(x => cacheKey(x) === row.dataset.key);
|
| 845 |
if (v) refreshCacheBadge(v);
|
| 846 |
});
|
|
@@ -902,23 +991,34 @@ function wireHubHandlers() {
|
|
| 902 |
}
|
| 903 |
|
| 904 |
function wireRunHandlers() {
|
| 905 |
-
$('btn-download').addEventListener('click', onDownloadClick);
|
| 906 |
-
$('btn-run').addEventListener('click', onRunClick);
|
| 907 |
}
|
| 908 |
|
| 909 |
-
// ────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
| 910 |
|
| 911 |
-
|
| 912 |
-
state.
|
| 913 |
-
state.source = state.mode === 'local' ? localSource() : hostedSource();
|
| 914 |
state.budget = await getDeviceBudgetMB();
|
| 915 |
state.device = await describeDevice();
|
| 916 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 917 |
state.cacheStatus = await loadCacheStatus();
|
| 918 |
state.variants = flattenVariants(state.models);
|
| 919 |
|
| 920 |
-
|
| 921 |
-
if (state.mode === 'hosted') {
|
| 922 |
try { state.hfSession = await resumeHFSession(); } catch { /* ignore */ }
|
| 923 |
}
|
| 924 |
|
|
@@ -935,8 +1035,8 @@ async function init() {
|
|
| 935 |
updateButtons();
|
| 936 |
}
|
| 937 |
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
}
|
|
|
|
| 1 |
+
// Run-tab controller. Mounts into the existing #run-section subtree and
|
| 2 |
+
// drives the one-click benchmark UI using the dashboard's design-system
|
| 3 |
+
// classes. Detects `surface` (localhost / space / pages) to gate the
|
| 4 |
+
// server save checkbox and the HF hub sign-in/submit row.
|
| 5 |
+
|
| 6 |
+
import { runBenchmarkCore } from './core.js';
|
| 7 |
+
import { localSource, hostedSource, inventoryOpfs, purgeOpfs } from './source.js';
|
| 8 |
+
import { getDeviceBudgetMB, variantFits, describeDevice } from './device.js';
|
|
|
|
| 9 |
import {
|
| 10 |
resumeHFSession, beginHFSignIn, signOutHF, submitResultsToDataset,
|
| 11 |
+
} from './hub.js';
|
| 12 |
+
import { isHubConfigured, HF_DATASET_REPO } from './config.js';
|
| 13 |
|
| 14 |
const OVERHEAD = 1.5;
|
| 15 |
const DEFAULT_PROMPT =
|
|
|
|
| 24 |
const MIN_ITERATIONS_FOR_SUBMIT = 5;
|
| 25 |
|
| 26 |
const state = {
|
| 27 |
+
surface: 'pages', // 'localhost' | 'space' | 'pages' | 'file'
|
| 28 |
+
source: null, // localSource() | hostedSource()
|
| 29 |
+
models: null, // parsed models.json
|
| 30 |
+
budget: null, // { budgetMB, memGB, quotaMB, source }
|
| 31 |
+
device: null, // describeDevice() output
|
| 32 |
+
cacheStatus: {}, // { 'repo/file': { cachedBytes } }
|
| 33 |
+
variants: [], // flat variant rows with metadata
|
| 34 |
running: false,
|
| 35 |
aborted: false,
|
| 36 |
+
results: [], // result records from the current session
|
| 37 |
+
hfSession: null, // { accessToken, expiresAt, userName } when signed in
|
| 38 |
iterations: DEFAULT_ITERATIONS,
|
| 39 |
+
mounted: false,
|
| 40 |
};
|
| 41 |
|
| 42 |
+
// ──────────────── surface detection ────────────────
|
| 43 |
|
| 44 |
+
async function detectSurface() {
|
| 45 |
const params = new URLSearchParams(location.search);
|
| 46 |
+
if (params.get('mode') === 'local') return 'localhost';
|
| 47 |
+
if (params.get('mode') === 'hosted') return 'space';
|
| 48 |
+
if (/\.static\.hf\.space$/.test(location.hostname)) return 'space';
|
| 49 |
+
if (/\.github\.io$/.test(location.hostname)) return 'pages';
|
| 50 |
+
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
|
| 51 |
+
try {
|
| 52 |
+
const r = await fetch('/api/models', { method: 'HEAD' });
|
| 53 |
+
if (r.ok) return 'localhost';
|
| 54 |
+
} catch { /* no backend */ }
|
| 55 |
+
}
|
| 56 |
+
if (location.protocol === 'file:') return 'file';
|
| 57 |
+
return 'pages';
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
function canSubmit() {
|
| 61 |
+
return state.surface === 'localhost'
|
| 62 |
+
|| (state.surface === 'space' && isHubConfigured());
|
| 63 |
}
|
| 64 |
|
| 65 |
+
// ──────────────── data loading ────────────────
|
| 66 |
+
|
| 67 |
async function loadModels() {
|
| 68 |
+
// Candidates in order — the Run page lives at /site/run/ locally but may be
|
| 69 |
+
// flattened to Space root. Relative `../models.json` catches the local case;
|
| 70 |
+
// `./models.json` catches the flattened case; absolute `/models.json` catches
|
| 71 |
+
// Space root too; `/api/models` is the Express backend.
|
| 72 |
+
const candidates = state.surface === 'localhost'
|
| 73 |
+
? ['/api/models', '../models.json', './models.json', '/models.json']
|
| 74 |
+
: ['./models.json', '../models.json', '/models.json', './data/models.json'];
|
| 75 |
+
let lastErr = null;
|
| 76 |
+
for (const url of candidates) {
|
| 77 |
+
try {
|
| 78 |
+
const r = await fetch(url);
|
| 79 |
+
if (r.ok) return await r.json();
|
| 80 |
+
lastErr = new Error(`${url} → ${r.status}`);
|
| 81 |
+
} catch (err) {
|
| 82 |
+
lastErr = err;
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
throw lastErr || new Error('Could not load models.json');
|
| 86 |
}
|
| 87 |
|
| 88 |
async function loadCacheStatus() {
|
| 89 |
+
if (state.surface === 'localhost') {
|
| 90 |
try {
|
| 91 |
const r = await fetch('/api/cache-status');
|
| 92 |
if (r.ok) return r.json();
|
|
|
|
| 150 |
function renderHeader() {
|
| 151 |
const d = state.device;
|
| 152 |
const b = state.budget;
|
| 153 |
+
|
| 154 |
+
const badge = $('run-mode-badge');
|
| 155 |
+
if (badge) {
|
| 156 |
+
badge.textContent = state.surface;
|
| 157 |
+
badge.className = `badge run-mode-badge run-mode-${state.surface}`;
|
| 158 |
+
}
|
| 159 |
|
| 160 |
const uaShort = d.userAgent.match(/(Firefox|Chrome|CriOS|Edg|Safari)\/[\d.]+/)?.[0] || 'browser';
|
| 161 |
const gpuStr = d.gpu
|
| 162 |
? [d.gpu.vendor, d.gpu.architecture, d.gpu.device].filter(Boolean).join(' ').trim()
|
| 163 |
+
: '';
|
| 164 |
+
|
| 165 |
+
$('device-browser').textContent = uaShort;
|
| 166 |
+
$('device-platform').textContent = d.platform || 'unknown';
|
| 167 |
+
$('device-gpu').textContent = gpuStr || (d.webgpu ? 'WebGPU (no info)' : 'no WebGPU');
|
| 168 |
+
|
| 169 |
+
const memStr = b.memGB !== null ? `${b.memGB} GB` : '—';
|
| 170 |
+
$('device-memory').textContent = memStr;
|
| 171 |
|
|
|
|
|
|
|
|
|
|
| 172 |
const budgetGB = (b.budgetMB / 1024).toFixed(1);
|
| 173 |
+
$('device-budget').textContent = `${budgetGB} GB`;
|
| 174 |
+
$('device-budget-source').textContent = `source: ${b.source}`;
|
| 175 |
+
|
| 176 |
+
const webgpuCell = $('device-webgpu');
|
| 177 |
+
if (webgpuCell) {
|
| 178 |
+
webgpuCell.textContent = d.webgpu ? 'yes' : 'no';
|
| 179 |
+
webgpuCell.classList.toggle('text-success', d.webgpu);
|
| 180 |
+
webgpuCell.classList.toggle('text-error', !d.webgpu);
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
// Surface-dependent UI gating.
|
| 184 |
+
const hubRow = $('hub-row');
|
| 185 |
+
if (hubRow) hubRow.hidden = state.surface !== 'space';
|
| 186 |
+
|
| 187 |
+
const saveLocalRow = $('save-local-row');
|
| 188 |
+
if (saveLocalRow) saveLocalRow.hidden = state.surface !== 'localhost';
|
| 189 |
+
|
| 190 |
+
const pagesBanner = $('run-pages-banner');
|
| 191 |
+
if (pagesBanner) pagesBanner.hidden = state.surface !== 'pages';
|
| 192 |
|
|
|
|
|
|
|
|
|
|
| 193 |
const purgeBtn = $('btn-purge');
|
| 194 |
+
if (purgeBtn) purgeBtn.hidden = state.surface === 'localhost';
|
| 195 |
+
|
| 196 |
renderHfSection();
|
| 197 |
}
|
| 198 |
|
| 199 |
function renderHfSection() {
|
| 200 |
+
if (state.surface !== 'space') return;
|
| 201 |
const signinBtn = $('btn-signin');
|
| 202 |
const submitBtn = $('btn-submit');
|
| 203 |
const userEl = $('hf-user');
|
|
|
|
| 206 |
if (!isHubConfigured()) {
|
| 207 |
signinBtn.disabled = true;
|
| 208 |
signinBtn.textContent = 'HF hub not configured';
|
| 209 |
+
signinBtn.title = 'Set HF_DATASET_REPO in site/js/run/config.js';
|
| 210 |
submitBtn.hidden = true;
|
| 211 |
userEl.textContent = '';
|
| 212 |
return;
|
|
|
|
| 235 |
}
|
| 236 |
|
| 237 |
function renderModels() {
|
| 238 |
+
const panel = $('run-models');
|
| 239 |
panel.innerHTML = '';
|
| 240 |
|
| 241 |
const groups = groupByFamily(state.variants);
|
|
|
|
| 244 |
const allFit = fitsCount === variants.length;
|
| 245 |
|
| 246 |
const familyEl = document.createElement('details');
|
| 247 |
+
familyEl.className = 'run-family card';
|
| 248 |
familyEl.dataset.family = family;
|
| 249 |
familyEl.open = true;
|
| 250 |
|
| 251 |
const summary = document.createElement('summary');
|
| 252 |
+
summary.className = 'run-family-summary';
|
| 253 |
summary.innerHTML = `
|
| 254 |
+
<span class="run-family-chevron" aria-hidden="true"></span>
|
| 255 |
+
<input type="checkbox" class="run-family-select-all" data-family="${escapeAttr(family)}"${allFit ? ' checked' : ''}>
|
| 256 |
+
<span class="run-family-name">${escapeText(family)}</span>
|
| 257 |
+
<span class="run-family-stats">${variants.length} variants · ${fitsCount} fit</span>
|
| 258 |
`;
|
| 259 |
if (/^granite-4/i.test(family)) {
|
| 260 |
const w = document.createElement('span');
|
| 261 |
+
w.className = 'run-family-warning';
|
| 262 |
w.textContent = '⚠ needs SSM_SCAN in llama.cpp';
|
| 263 |
summary.appendChild(w);
|
| 264 |
}
|
| 265 |
familyEl.appendChild(summary);
|
| 266 |
|
| 267 |
+
const list = document.createElement('div');
|
| 268 |
+
list.className = 'run-variant-list';
|
| 269 |
+
|
| 270 |
for (const v of variants) {
|
| 271 |
const row = document.createElement('label');
|
| 272 |
+
row.className = 'run-variant-row';
|
| 273 |
+
if (!variantFitsDevice(v)) row.classList.add('is-non-fit');
|
| 274 |
row.dataset.key = cacheKey(v);
|
| 275 |
|
| 276 |
const cb = document.createElement('input');
|
| 277 |
cb.type = 'checkbox';
|
| 278 |
+
cb.className = 'run-variant-select';
|
| 279 |
cb.dataset.key = cacheKey(v);
|
| 280 |
cb.checked = variantFitsDevice(v);
|
| 281 |
|
| 282 |
+
const quant = document.createElement('span');
|
| 283 |
+
quant.className = 'run-variant-quant';
|
| 284 |
+
quant.textContent = v.quant;
|
| 285 |
+
|
| 286 |
+
const filename = document.createElement('code');
|
| 287 |
+
filename.className = 'run-variant-file';
|
| 288 |
+
filename.textContent = v.filename;
|
| 289 |
|
| 290 |
const size = document.createElement('span');
|
| 291 |
+
size.className = 'run-variant-size';
|
| 292 |
size.textContent = v.sizeMB > 0 ? formatSize(v.sizeMB) : '?';
|
| 293 |
|
| 294 |
const badges = document.createElement('span');
|
| 295 |
+
badges.className = 'run-variant-badges';
|
| 296 |
updateBadgesForVariant(badges, v);
|
| 297 |
|
| 298 |
+
row.append(cb, quant, filename, size, badges);
|
| 299 |
+
list.appendChild(row);
|
| 300 |
}
|
| 301 |
+
familyEl.appendChild(list);
|
| 302 |
panel.appendChild(familyEl);
|
| 303 |
}
|
| 304 |
}
|
| 305 |
|
| 306 |
function updateBadgesForVariant(badgesEl, v) {
|
| 307 |
badgesEl.innerHTML = '';
|
| 308 |
+
if (isCached(v)) badgesEl.appendChild(makeBadge('cached', 'badge--cached'));
|
| 309 |
+
for (const w of v.warnings) badgesEl.appendChild(makeBadge(w, 'badge--warn'));
|
| 310 |
}
|
| 311 |
|
| 312 |
function refreshCacheBadge(v) {
|
| 313 |
+
const row = document.querySelector(`.run-variant-row[data-key="${cssEscape(cacheKey(v))}"]`);
|
| 314 |
if (!row) return;
|
| 315 |
+
const badges = row.querySelector('.run-variant-badges');
|
| 316 |
if (badges) updateBadgesForVariant(badges, v);
|
| 317 |
}
|
| 318 |
|
| 319 |
function makeBadge(text, cls) {
|
| 320 |
const el = document.createElement('span');
|
| 321 |
+
el.className = `badge ${cls}`;
|
| 322 |
el.textContent = text;
|
| 323 |
return el;
|
| 324 |
}
|
|
|
|
| 340 |
// ──────────────── selection / filters ────────────────
|
| 341 |
|
| 342 |
function wireSelectionHandlers() {
|
| 343 |
+
const panel = $('run-models');
|
| 344 |
+
panel.addEventListener('change', (e) => {
|
| 345 |
+
const t = e.target;
|
| 346 |
+
if (t.classList?.contains('run-family-select-all')) {
|
| 347 |
+
const family = t.dataset.family;
|
| 348 |
+
const rows = panel.querySelectorAll(
|
| 349 |
+
`details.run-family[data-family="${cssEscape(family)}"] .run-variant-select`,
|
| 350 |
);
|
| 351 |
+
rows.forEach(cb => { cb.checked = t.checked; });
|
| 352 |
updateButtons();
|
| 353 |
+
} else if (t.classList?.contains('run-variant-select')) {
|
| 354 |
+
updateButtons();
|
| 355 |
+
}
|
| 356 |
});
|
| 357 |
+
// Prevent the select-all toggling expand/collapse.
|
| 358 |
+
panel.addEventListener('click', (e) => {
|
| 359 |
+
if (e.target.classList?.contains('run-family-select-all')) {
|
| 360 |
+
e.stopPropagation();
|
| 361 |
+
}
|
| 362 |
});
|
| 363 |
}
|
| 364 |
|
| 365 |
function wireFilters() {
|
| 366 |
['hide-ud', 'hide-iq', 'hide-hifp'].forEach(id => {
|
| 367 |
+
const el = $(id);
|
| 368 |
+
if (el) el.addEventListener('change', applyFilters);
|
| 369 |
});
|
| 370 |
}
|
| 371 |
|
|
|
|
| 387 |
}
|
| 388 |
|
| 389 |
function applyFilters() {
|
| 390 |
+
const hideUd = $('hide-ud')?.checked;
|
| 391 |
+
const hideIq = $('hide-iq')?.checked;
|
| 392 |
+
const hideHifp = $('hide-hifp')?.checked;
|
| 393 |
+
document.querySelectorAll('.run-variant-row').forEach(row => {
|
| 394 |
const v = state.variants.find(x => cacheKey(x) === row.dataset.key);
|
| 395 |
if (!v) return;
|
| 396 |
const isUd = v.quant.startsWith('UD-');
|
|
|
|
| 402 |
}
|
| 403 |
|
| 404 |
function getCheckedVariants() {
|
| 405 |
+
return Array.from(document.querySelectorAll('.run-variant-select:checked'))
|
| 406 |
.map(cb => state.variants.find(v => cacheKey(v) === cb.dataset.key))
|
| 407 |
.filter(Boolean);
|
| 408 |
}
|
|
|
|
| 410 |
function updateButtons() {
|
| 411 |
const checked = getCheckedVariants();
|
| 412 |
const cachedChecked = checked.filter(isCached);
|
| 413 |
+
const dl = $('btn-download'); if (dl) dl.disabled = state.running || checked.length === 0;
|
| 414 |
+
const rn = $('btn-run'); if (rn) rn.disabled = state.running || cachedChecked.length === 0;
|
| 415 |
+
const ab = $('btn-abort'); if (ab) { ab.disabled = !state.running; ab.hidden = !state.running; }
|
| 416 |
+
const status = $('queue-status');
|
| 417 |
+
if (status) {
|
| 418 |
+
status.textContent = checked.length
|
| 419 |
+
? `${checked.length} selected · ${cachedChecked.length} cached`
|
| 420 |
+
: '';
|
| 421 |
+
}
|
| 422 |
}
|
| 423 |
|
| 424 |
// ──────────────── progress table ────────────────
|
| 425 |
|
| 426 |
+
function ensureProgressTable() {
|
| 427 |
+
const wrap = $('run-progress-wrapper');
|
| 428 |
+
if (!wrap) return null;
|
| 429 |
+
let table = wrap.querySelector('table');
|
| 430 |
+
if (!table) {
|
| 431 |
+
table = document.createElement('table');
|
| 432 |
+
table.className = 'results-table run-progress-table';
|
| 433 |
+
table.innerHTML = `
|
| 434 |
+
<thead>
|
| 435 |
+
<tr>
|
| 436 |
+
<th>Model</th>
|
| 437 |
+
<th>Variant</th>
|
| 438 |
+
<th>Status</th>
|
| 439 |
+
<th class="num">Prefill tok/s</th>
|
| 440 |
+
<th class="num">Decode tok/s</th>
|
| 441 |
+
<th class="num">Wall s</th>
|
| 442 |
+
<th>Error</th>
|
| 443 |
+
</tr>
|
| 444 |
+
</thead>
|
| 445 |
+
<tbody></tbody>
|
| 446 |
+
`;
|
| 447 |
+
wrap.appendChild(table);
|
| 448 |
+
}
|
| 449 |
+
return table;
|
| 450 |
+
}
|
| 451 |
|
| 452 |
function progressRowFor(v) {
|
| 453 |
const key = cacheKey(v);
|
| 454 |
+
const table = ensureProgressTable();
|
| 455 |
+
const tbody = table.querySelector('tbody');
|
| 456 |
let tr = tbody.querySelector(`tr[data-key="${cssEscape(key)}"]`);
|
| 457 |
if (!tr) {
|
| 458 |
tr = document.createElement('tr');
|
| 459 |
tr.dataset.key = key;
|
| 460 |
+
tr.className = 'run-row-queued';
|
| 461 |
tr.innerHTML = `
|
| 462 |
<td>${escapeText(v.modelName)}</td>
|
| 463 |
<td>${escapeText(v.quant)}</td>
|
|
|
|
| 471 |
}
|
| 472 |
return {
|
| 473 |
setStatus(status, msg) {
|
| 474 |
+
tr.className = `run-row-${rowClassFor(status)}`;
|
| 475 |
tr.querySelector('.status').textContent = msg ? `${status} — ${msg}` : status;
|
| 476 |
},
|
| 477 |
setProgress(fraction, downloaded, total) {
|
|
|
|
| 482 |
tr.querySelector('.status').textContent = detail ? `downloading ${detail}` : 'downloading';
|
| 483 |
},
|
| 484 |
fillFromRecord(record) {
|
| 485 |
+
tr.className = `run-row-${record.status === 'done' ? 'ok' : 'error'}`;
|
| 486 |
tr.querySelector('.status').textContent = record.status;
|
| 487 |
tr.querySelector('.prefill').textContent = record.metrics?.prefill_tok_s ?? '—';
|
| 488 |
tr.querySelector('.decode').textContent = record.metrics?.decode_tok_s ?? '—';
|
|
|
|
| 574 |
state.running = true;
|
| 575 |
state.aborted = false;
|
| 576 |
updateButtons();
|
|
|
|
| 577 |
|
| 578 |
for (const v of variants) {
|
| 579 |
if (state.aborted) break;
|
|
|
|
| 601 |
}
|
| 602 |
}
|
| 603 |
|
| 604 |
+
// Refresh cache inventory to reconcile any partial downloads.
|
| 605 |
state.cacheStatus = await loadCacheStatus();
|
| 606 |
+
document.querySelectorAll('.run-variant-row').forEach(row => {
|
| 607 |
const v = state.variants.find(x => cacheKey(x) === row.dataset.key);
|
| 608 |
if (v) refreshCacheBadge(v);
|
| 609 |
});
|
|
|
|
| 622 |
state.aborted = false;
|
| 623 |
state.results = [];
|
| 624 |
updateButtons();
|
|
|
|
| 625 |
|
| 626 |
const machine = await machineInfo();
|
| 627 |
const browser = browserInfo();
|
|
|
|
| 643 |
localStorage.setItem('webgpu-bench:lastRun', JSON.stringify(state.results));
|
| 644 |
} catch { /* quota */ }
|
| 645 |
|
| 646 |
+
if (state.surface === 'localhost' && $('save-local')?.checked) {
|
| 647 |
fetch('/api/results', {
|
| 648 |
method: 'POST',
|
| 649 |
headers: { 'Content-Type': 'application/json' },
|
|
|
|
| 719 |
nPredict: DEFAULT_N_PREDICT,
|
| 720 |
nCtx: DEFAULT_N_CTX,
|
| 721 |
nGpuLayers: DEFAULT_N_GPU_LAYERS,
|
|
|
|
|
|
|
|
|
|
| 722 |
refTokenIds: i === 0 ? (refTokenIds || null) : null,
|
| 723 |
onStatus: (s, m) => row.setStatus(`gpu${i + 1}/${s}`, m),
|
| 724 |
onProgress: (fr, d, t) => row.setProgress(fr, d, t),
|
|
|
|
| 788 |
prefill_samples: vr.gpuSamples.map(s => round2(s.prefill_tok_s)),
|
| 789 |
decode_samples: vr.gpuSamples.map(s => round2(s.decode_tok_s)),
|
| 790 |
iterations: vr.iterations,
|
|
|
|
| 791 |
n_p_eval: first.n_p_eval,
|
| 792 |
n_eval: first.n_eval,
|
| 793 |
t_p_eval_ms: first.t_p_eval_ms,
|
|
|
|
| 821 |
cpu_baseline: cpuBaseline,
|
| 822 |
output: vr.gpuCore?.output || '',
|
| 823 |
machine,
|
| 824 |
+
source: `webgpu-bench/site (${state.surface})`,
|
| 825 |
};
|
| 826 |
}
|
| 827 |
|
|
|
|
| 830 |
// ──────────────── Output ────────────────
|
| 831 |
|
| 832 |
function renderOutput() {
|
| 833 |
+
const ta = $('output-textarea');
|
| 834 |
+
if (ta) ta.value = generateMarkdown(state.results);
|
| 835 |
}
|
| 836 |
|
| 837 |
function generateMarkdown(results) {
|
|
|
|
| 879 |
}
|
| 880 |
|
| 881 |
function wireOutputHandlers() {
|
| 882 |
+
$('btn-copy')?.addEventListener('click', async () => {
|
| 883 |
const text = $('output-textarea').value;
|
| 884 |
try {
|
| 885 |
await navigator.clipboard.writeText(text);
|
|
|
|
| 890 |
}
|
| 891 |
});
|
| 892 |
|
| 893 |
+
$('btn-download-json')?.addEventListener('click', () => {
|
| 894 |
if (state.results.length === 0) return;
|
| 895 |
const blob = new Blob([JSON.stringify(state.results, null, 2)], { type: 'application/json' });
|
| 896 |
const url = URL.createObjectURL(blob);
|
|
|
|
| 909 |
setTimeout(() => { el.textContent = original; }, 1200);
|
| 910 |
}
|
| 911 |
|
| 912 |
+
// ──────────────── Abort / Purge / Hub ────────────────
|
| 913 |
|
| 914 |
function wireAbortHandler() {
|
| 915 |
+
$('btn-abort')?.addEventListener('click', () => {
|
| 916 |
state.aborted = true;
|
| 917 |
+
const ab = $('btn-abort');
|
| 918 |
+
if (ab) ab.disabled = true;
|
| 919 |
logLine('Abort requested — will stop between variants.');
|
| 920 |
});
|
| 921 |
}
|
|
|
|
| 924 |
const btn = $('btn-purge');
|
| 925 |
if (!btn) return;
|
| 926 |
btn.addEventListener('click', async () => {
|
| 927 |
+
if (state.surface === 'localhost') return;
|
| 928 |
if (!confirm('Delete all cached GGUF files from OPFS? This frees browser storage but re-downloads will be needed.')) return;
|
| 929 |
try {
|
| 930 |
await purgeOpfs();
|
| 931 |
state.cacheStatus = {};
|
| 932 |
+
document.querySelectorAll('.run-variant-row').forEach(row => {
|
| 933 |
const v = state.variants.find(x => cacheKey(x) === row.dataset.key);
|
| 934 |
if (v) refreshCacheBadge(v);
|
| 935 |
});
|
|
|
|
| 991 |
}
|
| 992 |
|
| 993 |
function wireRunHandlers() {
|
| 994 |
+
$('btn-download')?.addEventListener('click', onDownloadClick);
|
| 995 |
+
$('btn-run')?.addEventListener('click', onRunClick);
|
| 996 |
}
|
| 997 |
|
| 998 |
+
// ──────────────── Public API ────────────────
|
| 999 |
+
|
| 1000 |
+
export async function mountRunSection() {
|
| 1001 |
+
if (state.mounted) return;
|
| 1002 |
+
state.mounted = true;
|
| 1003 |
|
| 1004 |
+
state.surface = await detectSurface();
|
| 1005 |
+
state.source = state.surface === 'localhost' ? localSource() : hostedSource();
|
|
|
|
| 1006 |
state.budget = await getDeviceBudgetMB();
|
| 1007 |
state.device = await describeDevice();
|
| 1008 |
+
|
| 1009 |
+
try {
|
| 1010 |
+
state.models = await loadModels();
|
| 1011 |
+
} catch (err) {
|
| 1012 |
+
const panel = $('run-models');
|
| 1013 |
+
if (panel) panel.innerHTML = `<div class="empty-state">Could not load models.json — ${escapeText(err.message)}</div>`;
|
| 1014 |
+
console.error(err);
|
| 1015 |
+
return;
|
| 1016 |
+
}
|
| 1017 |
+
|
| 1018 |
state.cacheStatus = await loadCacheStatus();
|
| 1019 |
state.variants = flattenVariants(state.models);
|
| 1020 |
|
| 1021 |
+
if (state.surface === 'space') {
|
|
|
|
| 1022 |
try { state.hfSession = await resumeHFSession(); } catch { /* ignore */ }
|
| 1023 |
}
|
| 1024 |
|
|
|
|
| 1035 |
updateButtons();
|
| 1036 |
}
|
| 1037 |
|
| 1038 |
+
export function teardownRunSection() {
|
| 1039 |
+
// Placeholder — no explicit teardown today. Future: abort in-flight runs,
|
| 1040 |
+
// detach listeners. For now the Run tab just sits idle.
|
| 1041 |
+
state.aborted = true;
|
| 1042 |
+
}
|
bench-core.js → js/run/core.js
RENAMED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
// Benchmark core: load GGUF via a source adapter, init llama.cpp WASM,
|
| 2 |
// run inference, collect metrics. Used by harness.js (URL-param driven, for
|
| 3 |
-
// runner.js) and by
|
| 4 |
|
| 5 |
const DEFAULT_PROMPT = 'Hello, how are you?';
|
| 6 |
|
|
@@ -31,7 +31,7 @@ export async function runBenchmarkCore({
|
|
| 31 |
onProgress = () => {},
|
| 32 |
onLog = () => {},
|
| 33 |
}) {
|
| 34 |
-
if (!source) throw new Error('No source provided (see
|
| 35 |
if (!modelFile) throw new Error('No model file specified.');
|
| 36 |
if (!hfRepo) throw new Error('No hfRepo specified.');
|
| 37 |
|
|
|
|
| 1 |
// Benchmark core: load GGUF via a source adapter, init llama.cpp WASM,
|
| 2 |
// run inference, collect metrics. Used by harness.js (URL-param driven, for
|
| 3 |
+
// runner.js) and by the Run-tab controller (UI driven).
|
| 4 |
|
| 5 |
const DEFAULT_PROMPT = 'Hello, how are you?';
|
| 6 |
|
|
|
|
| 31 |
onProgress = () => {},
|
| 32 |
onLog = () => {},
|
| 33 |
}) {
|
| 34 |
+
if (!source) throw new Error('No source provided (see run/source.js).');
|
| 35 |
if (!modelFile) throw new Error('No model file specified.');
|
| 36 |
if (!hfRepo) throw new Error('No hfRepo specified.');
|
| 37 |
|
bench-device.js → js/run/device.js
RENAMED
|
File without changes
|
bench-hub.js → js/run/hub.js
RENAMED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
// Hugging Face OAuth + dataset push for the bench page (hosted mode).
|
| 2 |
//
|
| 3 |
// Flow:
|
| 4 |
-
// 1. On page load,
|
| 5 |
-
// OAuth redirect (if the URL has the expected query params) and to
|
| 6 |
// reuse any previously-stored access token.
|
| 7 |
// 2. Clicking [Sign in] calls `beginHFSignIn()` which redirects the
|
| 8 |
// browser to HF; after consent, HF redirects back to the page with
|
|
@@ -24,7 +24,7 @@ import {
|
|
| 24 |
HF_OAUTH_SCOPES,
|
| 25 |
HF_DATASET_REPO,
|
| 26 |
isHubConfigured,
|
| 27 |
-
} from './
|
| 28 |
|
| 29 |
const TOKEN_STORAGE_KEY = 'webgpu-bench:hfOauth';
|
| 30 |
|
|
@@ -73,7 +73,7 @@ function readStoredSession() {
|
|
| 73 |
|
| 74 |
export async function beginHFSignIn() {
|
| 75 |
if (!isHubConfigured()) {
|
| 76 |
-
throw new Error('HF hub is not configured. Set HF_DATASET_REPO in
|
| 77 |
}
|
| 78 |
// When served from an HF Space with `hf_oauth: true`, HF injects
|
| 79 |
// `window.huggingface.variables` with OAUTH_CLIENT_ID + OAUTH_SCOPES.
|
|
|
|
| 1 |
// Hugging Face OAuth + dataset push for the bench page (hosted mode).
|
| 2 |
//
|
| 3 |
// Flow:
|
| 4 |
+
// 1. On page load, the run controller calls `resumeHFSession()` to handle
|
| 5 |
+
// the OAuth redirect (if the URL has the expected query params) and to
|
| 6 |
// reuse any previously-stored access token.
|
| 7 |
// 2. Clicking [Sign in] calls `beginHFSignIn()` which redirects the
|
| 8 |
// browser to HF; after consent, HF redirects back to the page with
|
|
|
|
| 24 |
HF_OAUTH_SCOPES,
|
| 25 |
HF_DATASET_REPO,
|
| 26 |
isHubConfigured,
|
| 27 |
+
} from './config.js';
|
| 28 |
|
| 29 |
const TOKEN_STORAGE_KEY = 'webgpu-bench:hfOauth';
|
| 30 |
|
|
|
|
| 73 |
|
| 74 |
export async function beginHFSignIn() {
|
| 75 |
if (!isHubConfigured()) {
|
| 76 |
+
throw new Error('HF hub is not configured. Set HF_DATASET_REPO in run/config.js.');
|
| 77 |
}
|
| 78 |
// When served from an HF Space with `hf_oauth: true`, HF injects
|
| 79 |
// `window.huggingface.variables` with OAUTH_CLIENT_ID + OAUTH_SCOPES.
|
bench-source.js → js/run/source.js
RENAMED
|
File without changes
|
js/tables.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { formatTokS, formatMs, categorizeError, groupBy, quantSortKey } from './utils.js';
|
| 2 |
+
|
| 3 |
+
let lastResults = [];
|
| 4 |
+
let sortState = { key: null, dir: 'asc' };
|
| 5 |
+
|
| 6 |
+
const NUM_KEYS = new Set([
|
| 7 |
+
'sizeMB', 'decode_tok_s', 'prefill_tok_s', 'n_eval', 't_eval_ms',
|
| 8 |
+
'n_p_eval', 't_p_eval_ms', 'wallTimeMs', 'consistency_rate', 'nGpuLayers',
|
| 9 |
+
]);
|
| 10 |
+
|
| 11 |
+
function sortResults(results, key, dir) {
|
| 12 |
+
const isNum = NUM_KEYS.has(key);
|
| 13 |
+
return [...results].sort((a, b) => {
|
| 14 |
+
let va = a[key], vb = b[key];
|
| 15 |
+
if (va == null && vb == null) return 0;
|
| 16 |
+
if (va == null) return 1;
|
| 17 |
+
if (vb == null) return -1;
|
| 18 |
+
|
| 19 |
+
let cmp;
|
| 20 |
+
if (isNum) {
|
| 21 |
+
cmp = Number(va) - Number(vb);
|
| 22 |
+
} else if (key === 'webgpuAvailable') {
|
| 23 |
+
cmp = (va === vb) ? 0 : va ? -1 : 1;
|
| 24 |
+
} else {
|
| 25 |
+
cmp = String(va).localeCompare(String(vb));
|
| 26 |
+
}
|
| 27 |
+
return dir === 'desc' ? -cmp : cmp;
|
| 28 |
+
});
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
function handleSort(key) {
|
| 32 |
+
if (sortState.key === key) {
|
| 33 |
+
sortState.dir = sortState.dir === 'asc' ? 'desc' : 'asc';
|
| 34 |
+
} else {
|
| 35 |
+
sortState.key = key;
|
| 36 |
+
// Default to descending for performance metrics
|
| 37 |
+
sortState.dir = NUM_KEYS.has(key) ? 'desc' : 'asc';
|
| 38 |
+
}
|
| 39 |
+
renderResultsTable(lastResults);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
export function renderResultsTable(results) {
|
| 43 |
+
lastResults = results;
|
| 44 |
+
const container = document.getElementById('results-table');
|
| 45 |
+
if (!container) return;
|
| 46 |
+
|
| 47 |
+
if (results.length === 0) {
|
| 48 |
+
container.innerHTML = '<div class="empty-state"><p>No results match the current filters.</p></div>';
|
| 49 |
+
return;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
const sorted = sortState.key ? sortResults(results, sortState.key, sortState.dir) : results;
|
| 53 |
+
|
| 54 |
+
const cols = [
|
| 55 |
+
{ key: 'machineSlug', label: 'Machine' },
|
| 56 |
+
{ key: 'model', label: 'Model' },
|
| 57 |
+
{ key: 'variant', label: 'Quant' },
|
| 58 |
+
{ key: 'sizeMB', label: 'Size (MB)' },
|
| 59 |
+
{ key: 'browser', label: 'Browser' },
|
| 60 |
+
{ key: 'nGpuLayers', label: 'Backend' },
|
| 61 |
+
{ key: 'status', label: 'Status' },
|
| 62 |
+
{ key: 'buildType', label: 'Build' },
|
| 63 |
+
{ key: 'webgpuAvailable', label: 'WebGPU' },
|
| 64 |
+
{ key: 'decode_tok_s', label: 'Decode tok/s' },
|
| 65 |
+
{ key: 'prefill_tok_s', label: 'Prefill tok/s' },
|
| 66 |
+
{ key: 'n_eval', label: 'n_eval' },
|
| 67 |
+
{ key: 't_eval_ms', label: 't_eval (ms)' },
|
| 68 |
+
{ key: 'n_p_eval', label: 'n_p_eval' },
|
| 69 |
+
{ key: 't_p_eval_ms', label: 't_p_eval (ms)' },
|
| 70 |
+
{ key: 'wallTimeMs', label: 'Wall (s)' },
|
| 71 |
+
{ key: 'consistency_rate', label: 'CPU Match' },
|
| 72 |
+
{ key: 'llamaCppCommit', label: 'llama.cpp' },
|
| 73 |
+
{ key: 'error', label: 'Error' },
|
| 74 |
+
];
|
| 75 |
+
|
| 76 |
+
let html = '<table class="results-table"><thead><tr>';
|
| 77 |
+
for (const col of cols) {
|
| 78 |
+
const isActive = sortState.key === col.key;
|
| 79 |
+
const arrow = isActive ? (sortState.dir === 'asc' ? ' \u2191' : ' \u2193') : '';
|
| 80 |
+
const cls = isActive ? ' class="sorted"' : '';
|
| 81 |
+
html += `<th data-key="${col.key}"${cls}>${col.label}${arrow}</th>`;
|
| 82 |
+
}
|
| 83 |
+
html += '</tr></thead><tbody>';
|
| 84 |
+
|
| 85 |
+
for (const r of sorted) {
|
| 86 |
+
const rowClass = r.status === 'done' ? 'row-pass' : 'row-fail';
|
| 87 |
+
html += `<tr class="${rowClass}">`;
|
| 88 |
+
for (const col of cols) {
|
| 89 |
+
html += '<td>';
|
| 90 |
+
switch (col.key) {
|
| 91 |
+
case 'status':
|
| 92 |
+
html += r.status === 'done'
|
| 93 |
+
? '<span class="badge badge--pass">PASS</span>'
|
| 94 |
+
: '<span class="badge badge--fail">FAIL</span>';
|
| 95 |
+
break;
|
| 96 |
+
case 'nGpuLayers':
|
| 97 |
+
if (r.nGpuLayers != null) {
|
| 98 |
+
const isCpu = r.nGpuLayers === 0;
|
| 99 |
+
html += `<span class="badge ${isCpu ? 'badge--cpu' : 'badge--webgpu'}">${isCpu ? 'CPU' : 'WebGPU'}</span>`;
|
| 100 |
+
} else {
|
| 101 |
+
html += '<span class="text-muted">\u2014</span>';
|
| 102 |
+
}
|
| 103 |
+
break;
|
| 104 |
+
case 'webgpuAvailable':
|
| 105 |
+
html += r.webgpuAvailable
|
| 106 |
+
? '<span class="badge badge--yes">Yes</span>'
|
| 107 |
+
: '<span class="badge badge--no">No</span>';
|
| 108 |
+
break;
|
| 109 |
+
case 'decode_tok_s':
|
| 110 |
+
case 'prefill_tok_s':
|
| 111 |
+
html += `<span class="mono">${formatTokS(r[col.key])}</span>`;
|
| 112 |
+
break;
|
| 113 |
+
case 't_eval_ms':
|
| 114 |
+
case 't_p_eval_ms':
|
| 115 |
+
html += `<span class="mono">${formatMs(r[col.key])}</span>`;
|
| 116 |
+
break;
|
| 117 |
+
case 'wallTimeMs':
|
| 118 |
+
html += `<span class="mono">${r.wallTimeMs != null ? (r.wallTimeMs / 1000).toFixed(1) : '\u2014'}</span>`;
|
| 119 |
+
break;
|
| 120 |
+
case 'consistency_rate':
|
| 121 |
+
if (r.consistency_rate != null) {
|
| 122 |
+
const pct = (r.consistency_rate * 100).toFixed(1);
|
| 123 |
+
const cls = r.consistency_rate >= 0.95 ? 'text-success' : r.consistency_rate >= 0.90 ? '' : 'text-error';
|
| 124 |
+
const diverge = r.consistency_first_disagree >= 0 ? ` (diverge@${r.consistency_first_disagree})` : '';
|
| 125 |
+
html += `<span class="mono ${cls}">${pct}%${diverge}</span>`;
|
| 126 |
+
} else {
|
| 127 |
+
html += '<span class="text-muted">\u2014</span>';
|
| 128 |
+
}
|
| 129 |
+
break;
|
| 130 |
+
case 'llamaCppCommit':
|
| 131 |
+
if (r.llamaCppCommit) {
|
| 132 |
+
const short = r.llamaCppCommit.slice(0, 10);
|
| 133 |
+
html += `<a class="mono" href="https://github.com/ggml-org/llama.cpp/commit/${r.llamaCppCommit}" target="_blank" rel="noopener">${short}</a>`;
|
| 134 |
+
} else {
|
| 135 |
+
html += '<span class="text-muted">\u2014</span>';
|
| 136 |
+
}
|
| 137 |
+
break;
|
| 138 |
+
case 'error':
|
| 139 |
+
if (r.error) {
|
| 140 |
+
const cat = categorizeError(r.error);
|
| 141 |
+
const short = r.error.length > 60 ? r.error.slice(0, 60) + '\u2026' : r.error;
|
| 142 |
+
html += `<span class="error-cell" title="${escapeHtml(r.error)}"><span class="error-cat">${cat}</span>${escapeHtml(short)}</span>`;
|
| 143 |
+
} else {
|
| 144 |
+
html += '<span class="text-muted">\u2014</span>';
|
| 145 |
+
}
|
| 146 |
+
break;
|
| 147 |
+
case 'sizeMB':
|
| 148 |
+
case 'n_eval':
|
| 149 |
+
case 'n_p_eval':
|
| 150 |
+
html += `<span class="mono">${r[col.key] != null ? r[col.key] : '\u2014'}</span>`;
|
| 151 |
+
break;
|
| 152 |
+
default:
|
| 153 |
+
html += escapeHtml(String(r[col.key] ?? '\u2014'));
|
| 154 |
+
}
|
| 155 |
+
html += '</td>';
|
| 156 |
+
}
|
| 157 |
+
html += '</tr>';
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
html += '</tbody></table>';
|
| 161 |
+
container.innerHTML = html;
|
| 162 |
+
|
| 163 |
+
// Wire sort click handlers
|
| 164 |
+
container.querySelectorAll('th[data-key]').forEach(th => {
|
| 165 |
+
th.addEventListener('click', () => handleSort(th.dataset.key));
|
| 166 |
+
});
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
export function renderErrorTable(results) {
|
| 170 |
+
const container = document.getElementById('error-table');
|
| 171 |
+
if (!container) return;
|
| 172 |
+
|
| 173 |
+
const errors = results.filter(r => r.status !== 'done' && r.error);
|
| 174 |
+
if (errors.length === 0) {
|
| 175 |
+
container.innerHTML = '<div class="empty-state"><p>No errors found.</p></div>';
|
| 176 |
+
return;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
const grouped = groupBy(errors, r => categorizeError(r.error));
|
| 180 |
+
|
| 181 |
+
let html = '<div class="table-card"><table class="data-table"><thead><tr><th>Category</th><th>Count</th><th>Variants</th><th>Browsers</th></tr></thead><tbody>';
|
| 182 |
+
for (const [cat, items] of Object.entries(grouped).sort((a, b) => b[1].length - a[1].length)) {
|
| 183 |
+
const variants = [...new Set(items.map(i => i.variant))].join(', ');
|
| 184 |
+
const browsers = [...new Set(items.map(i => i.browser))].join(', ');
|
| 185 |
+
html += `<tr><td><span class="error-cat">${cat}</span></td><td><span class="mono">${items.length}</span></td><td>${variants}</td><td>${browsers}</td></tr>`;
|
| 186 |
+
}
|
| 187 |
+
html += '</tbody></table></div>';
|
| 188 |
+
container.innerHTML = html;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
export function renderMachineInfo(machines) {
|
| 192 |
+
const container = document.getElementById('machine-info');
|
| 193 |
+
if (!container) return;
|
| 194 |
+
|
| 195 |
+
if (machines.length === 0) {
|
| 196 |
+
container.innerHTML = '<div class="empty-state"><p>No machine data.</p></div>';
|
| 197 |
+
return;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
let html = '<div class="machine-grid">';
|
| 201 |
+
for (const m of machines) {
|
| 202 |
+
const failCount = m.resultCount - m.passCount;
|
| 203 |
+
html += `
|
| 204 |
+
<div class="machine-card">
|
| 205 |
+
<div class="machine-card-header">
|
| 206 |
+
<svg class="machine-card-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
|
| 207 |
+
<h3>${escapeHtml(m.cpus)}</h3>
|
| 208 |
+
</div>
|
| 209 |
+
<div class="machine-card-specs">
|
| 210 |
+
<div class="spec-row"><span class="spec-label">Platform</span><span class="spec-value">${m.platform}</span></div>
|
| 211 |
+
<div class="spec-row"><span class="spec-label">Arch</span><span class="spec-value">${m.arch}</span></div>
|
| 212 |
+
<div class="spec-row"><span class="spec-label">RAM</span><span class="spec-value">${m.totalMemoryGB} GB</span></div>
|
| 213 |
+
<div class="spec-row"><span class="spec-label">Results</span><span class="spec-value">${m.resultCount}</span></div>
|
| 214 |
+
<div class="spec-row"><span class="spec-label">Passed</span><span class="spec-value text-success">${m.passCount}</span></div>
|
| 215 |
+
<div class="spec-row"><span class="spec-label">Failed</span><span class="spec-value text-error">${failCount}</span></div>
|
| 216 |
+
${m.llamaCppCommit ? `<div class="spec-row"><span class="spec-label">llama.cpp</span><span class="spec-value"><a href="https://github.com/ggml-org/llama.cpp/commit/${m.llamaCppCommit}" target="_blank" rel="noopener">${m.llamaCppCommit.slice(0, 10)}</a></span></div>` : ''}
|
| 217 |
+
</div>
|
| 218 |
+
</div>`;
|
| 219 |
+
}
|
| 220 |
+
html += '</div>';
|
| 221 |
+
container.innerHTML = html;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
function escapeHtml(str) {
|
| 225 |
+
const div = document.createElement('div');
|
| 226 |
+
div.textContent = str;
|
| 227 |
+
return div.innerHTML;
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
export function renderCpuGpuTable(results) {
|
| 231 |
+
const container = document.getElementById('cpu-gpu-table');
|
| 232 |
+
if (!container) return;
|
| 233 |
+
|
| 234 |
+
const METRICS = [
|
| 235 |
+
{ field: 'decode_tok_s', label: 'Decode tok/s' },
|
| 236 |
+
{ field: 'prefill_tok_s', label: 'Prefill tok/s' },
|
| 237 |
+
];
|
| 238 |
+
|
| 239 |
+
const passed = results.filter(r => r.status === 'done');
|
| 240 |
+
const cpuResults = passed.filter(r => r.nGpuLayers === 0);
|
| 241 |
+
const gpuResults = passed.filter(r => r.nGpuLayers !== 0);
|
| 242 |
+
|
| 243 |
+
if (cpuResults.length === 0 || gpuResults.length === 0) {
|
| 244 |
+
container.innerHTML = '<div class="empty-state"><p>Select "All Backends" to see CPU vs GPU comparison.</p></div>';
|
| 245 |
+
return;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
function avg(items, field) {
|
| 249 |
+
const vals = items.map(r => r[field]).filter(v => v != null);
|
| 250 |
+
return vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : null;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
const gpuBrowsers = [...new Set(gpuResults.map(r => r.browser))].sort();
|
| 254 |
+
|
| 255 |
+
const cpuByModelVariant = groupBy(cpuResults, r => `${r.model}::${r.variant}`);
|
| 256 |
+
const gpuByModelVariant = groupBy(gpuResults, r => `${r.model}::${r.variant}`);
|
| 257 |
+
|
| 258 |
+
const keys = [...new Set([...Object.keys(cpuByModelVariant), ...Object.keys(gpuByModelVariant)])]
|
| 259 |
+
.filter(k => cpuByModelVariant[k] && gpuByModelVariant[k]);
|
| 260 |
+
|
| 261 |
+
if (keys.length === 0) {
|
| 262 |
+
container.innerHTML = '<div class="empty-state"><p>No matching model+variant pairs between CPU and GPU results.</p></div>';
|
| 263 |
+
return;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
keys.sort((a, b) => {
|
| 267 |
+
const [aModel, aVar] = a.split('::');
|
| 268 |
+
const [bModel, bVar] = b.split('::');
|
| 269 |
+
if (aModel !== bModel) return aModel.localeCompare(bModel);
|
| 270 |
+
return quantSortKey(aVar) - quantSortKey(bVar);
|
| 271 |
+
});
|
| 272 |
+
|
| 273 |
+
// Two-row grouped header: row1 = group labels (CPU, Chromium, …), row2 = metric sub-labels
|
| 274 |
+
// CPU gets colspan = METRICS.length, each GPU browser gets colspan = METRICS.length * 2 (value + speedup per metric)
|
| 275 |
+
const gpuColspan = METRICS.length * 2;
|
| 276 |
+
let html = '<div class="table-card"><div class="results-wrapper"><table class="results-table"><thead>';
|
| 277 |
+
|
| 278 |
+
// Row 1: group headers
|
| 279 |
+
html += '<tr>';
|
| 280 |
+
html += '<th rowspan="2" class="th-group-border">Model</th><th rowspan="2" class="th-group-border">Quant</th>';
|
| 281 |
+
html += `<th colspan="${METRICS.length}" class="th-group th-group-border">CPU</th>`;
|
| 282 |
+
for (const b of gpuBrowsers) {
|
| 283 |
+
html += `<th colspan="${gpuColspan}" class="th-group th-group-border">${escapeHtml(b.charAt(0).toUpperCase() + b.slice(1))}</th>`;
|
| 284 |
+
}
|
| 285 |
+
html += '</tr>';
|
| 286 |
+
|
| 287 |
+
// Row 2: metric sub-headers
|
| 288 |
+
html += '<tr>';
|
| 289 |
+
for (const m of METRICS) {
|
| 290 |
+
html += `<th class="th-sub">${m.label}</th>`;
|
| 291 |
+
}
|
| 292 |
+
for (const b of gpuBrowsers) {
|
| 293 |
+
for (const m of METRICS) {
|
| 294 |
+
html += `<th class="th-sub">${m.label}</th><th class="th-sub">Speedup</th>`;
|
| 295 |
+
}
|
| 296 |
+
}
|
| 297 |
+
html += '</tr></thead><tbody>';
|
| 298 |
+
|
| 299 |
+
for (const key of keys) {
|
| 300 |
+
const [model, variant] = key.split('::');
|
| 301 |
+
const cpuItems = cpuByModelVariant[key] || [];
|
| 302 |
+
const gpuByBrowser = groupBy(gpuByModelVariant[key] || [], 'browser');
|
| 303 |
+
|
| 304 |
+
html += '<tr>';
|
| 305 |
+
html += `<td>${escapeHtml(model)}</td>`;
|
| 306 |
+
html += `<td><span class="mono">${escapeHtml(variant)}</span></td>`;
|
| 307 |
+
|
| 308 |
+
// CPU columns
|
| 309 |
+
for (const m of METRICS) {
|
| 310 |
+
const val = avg(cpuItems, m.field);
|
| 311 |
+
html += `<td><span class="mono">${formatTokS(val)}</span></td>`;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
// GPU columns per browser
|
| 315 |
+
for (const b of gpuBrowsers) {
|
| 316 |
+
const gpuItems = gpuByBrowser[b] || [];
|
| 317 |
+
for (const m of METRICS) {
|
| 318 |
+
const cpuVal = avg(cpuItems, m.field);
|
| 319 |
+
const gpuVal = avg(gpuItems, m.field);
|
| 320 |
+
const speedup = cpuVal && gpuVal ? gpuVal / cpuVal : null;
|
| 321 |
+
const cls = speedup == null ? '' : speedup >= 3 ? 'text-success' : speedup >= 1.5 ? '' : speedup >= 1 ? 'text-muted' : 'text-error';
|
| 322 |
+
html += `<td><span class="mono">${formatTokS(gpuVal)}</span></td>`;
|
| 323 |
+
html += `<td><span class="mono ${cls}">${speedup != null ? speedup.toFixed(2) + '\u00d7' : '\u2014'}</span></td>`;
|
| 324 |
+
}
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
html += '</tr>';
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
html += '</tbody></table></div></div>';
|
| 331 |
+
container.innerHTML = html;
|
| 332 |
+
}
|
js/utils.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const BROWSER_COLORS = {
|
| 2 |
+
chromium: '#60a5fa',
|
| 3 |
+
firefox: '#fb923c',
|
| 4 |
+
webkit: '#a78bfa',
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
// Quantization types sorted by approximate bit-width (low -> high)
|
| 8 |
+
export const QUANT_ORDER = [
|
| 9 |
+
'IQ1_S', 'IQ1_M',
|
| 10 |
+
'IQ2_XXS', 'IQ2_XS', 'IQ2_S', 'IQ2_M', 'Q2_K', 'Q2_K_S',
|
| 11 |
+
'IQ3_XXS', 'IQ3_XS', 'IQ3_S', 'Q3_K_S', 'Q3_K_M', 'Q3_K_L',
|
| 12 |
+
'IQ4_NL', 'IQ4_XS', 'Q4_0', 'Q4_K_S', 'Q4_K_M',
|
| 13 |
+
'Q5_0', 'Q5_K_S', 'Q5_K_M',
|
| 14 |
+
'Q6_K',
|
| 15 |
+
'Q8_0',
|
| 16 |
+
'F16',
|
| 17 |
+
'F32',
|
| 18 |
+
'BF16',
|
| 19 |
+
];
|
| 20 |
+
|
| 21 |
+
export function quantSortKey(q) {
|
| 22 |
+
const idx = QUANT_ORDER.indexOf(q);
|
| 23 |
+
return idx >= 0 ? idx : QUANT_ORDER.length;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export function formatTokS(v) {
|
| 27 |
+
if (v == null || isNaN(v)) return '\u2014';
|
| 28 |
+
return v.toFixed(1);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export function formatMs(v) {
|
| 32 |
+
if (v == null || isNaN(v)) return '\u2014';
|
| 33 |
+
return v.toFixed(1);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
export function categorizeError(err) {
|
| 37 |
+
if (!err) return null;
|
| 38 |
+
const e = err.toLowerCase();
|
| 39 |
+
if (e.includes('out of memory') || e.includes('oom') || e.includes('memory allocation')) return 'OOM';
|
| 40 |
+
if (e.includes('wasm') || e.includes('abort') || e.includes('unreachable')) return 'WASM Abort';
|
| 41 |
+
if (e.includes('timeout') || e.includes('timed out')) return 'Timeout';
|
| 42 |
+
if (e.includes('download') || e.includes('fetch') || e.includes('404') || e.includes('network')) return 'Download Failed';
|
| 43 |
+
return 'Other';
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
export function groupBy(arr, keyFn) {
|
| 47 |
+
const map = {};
|
| 48 |
+
for (const item of arr) {
|
| 49 |
+
const key = typeof keyFn === 'function' ? keyFn(item) : item[keyFn];
|
| 50 |
+
if (!map[key]) map[key] = [];
|
| 51 |
+
map[key].push(item);
|
| 52 |
+
}
|
| 53 |
+
return map;
|
| 54 |
+
}
|
methodology.html
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<meta name="color-scheme" content="dark">
|
| 7 |
+
<title>Methodology — WebGPU Bench</title>
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
| 11 |
+
<link rel="stylesheet" href="css/style.css">
|
| 12 |
+
</head>
|
| 13 |
+
<body>
|
| 14 |
+
<header class="header">
|
| 15 |
+
<div class="header-inner">
|
| 16 |
+
<a href="index.html" class="header-brand">
|
| 17 |
+
<svg class="header-logo" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/></svg>
|
| 18 |
+
<span class="header-title">WebGPU Bench</span>
|
| 19 |
+
</a>
|
| 20 |
+
<nav class="header-nav">
|
| 21 |
+
<a href="index.html" class="header-link">Dashboard</a>
|
| 22 |
+
<a href="run/" class="header-link">Run</a>
|
| 23 |
+
</nav>
|
| 24 |
+
</div>
|
| 25 |
+
</header>
|
| 26 |
+
|
| 27 |
+
<div class="methodology-content">
|
| 28 |
+
<a href="index.html" class="back-link">
|
| 29 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
|
| 30 |
+
Back to Dashboard
|
| 31 |
+
</a>
|
| 32 |
+
|
| 33 |
+
<h2>How Benchmarks Work</h2>
|
| 34 |
+
<ol>
|
| 35 |
+
<li><code>build.sh</code> compiles llama.cpp to WebAssembly with WebGPU support via Emscripten + emdawnwebgpu, producing two WASM variants: JSPI (Chrome) and Asyncify (Firefox, Safari).</li>
|
| 36 |
+
<li><code>runner.js</code> launches Playwright browsers and navigates to <code>harness.html</code>.</li>
|
| 37 |
+
<li><code>harness.js</code> detects JSPI support and loads the correct WASM variant.</li>
|
| 38 |
+
<li>The GGUF model is downloaded from HuggingFace directly in the browser.</li>
|
| 39 |
+
<li>Inference runs via WebGPU (or CPU fallback) using llama.cpp's C API with greedy sampling for deterministic output.</li>
|
| 40 |
+
<li>Performance metrics are collected via <code>llama_perf_context()</code> and returned to Playwright.</li>
|
| 41 |
+
<li>A fresh browser instance is launched for each variant to prevent WASM memory accumulation (OOM fix).</li>
|
| 42 |
+
</ol>
|
| 43 |
+
|
| 44 |
+
<h2>Dashboard Columns</h2>
|
| 45 |
+
<table>
|
| 46 |
+
<thead>
|
| 47 |
+
<tr><th>Column</th><th>Description</th></tr>
|
| 48 |
+
</thead>
|
| 49 |
+
<tbody>
|
| 50 |
+
<tr><td>Machine</td><td>Machine slug identifying the hardware (e.g. <code>apple-m3-16gb-darwin</code>)</td></tr>
|
| 51 |
+
<tr><td>Model</td><td>Model name (e.g. Llama-3.2-1B-Instruct)</td></tr>
|
| 52 |
+
<tr><td>Quant</td><td>Quantization variant (e.g. Q4_K_M, Q8_0)</td></tr>
|
| 53 |
+
<tr><td>Size (MB)</td><td>Model file size in megabytes</td></tr>
|
| 54 |
+
<tr><td>Browser</td><td>Browser used for the benchmark (chromium, firefox, webkit)</td></tr>
|
| 55 |
+
<tr><td>Status</td><td>PASS if inference completed successfully, FAIL otherwise</td></tr>
|
| 56 |
+
<tr><td>Build</td><td><code>jspi</code> or <code>asyncify</code> — which WASM variant was used. Chrome supports JSPI; Firefox and Safari use Asyncify.</td></tr>
|
| 57 |
+
<tr><td>WebGPU</td><td>Whether WebGPU was available in the browser. If not, inference falls back to CPU.</td></tr>
|
| 58 |
+
<tr><td>Decode tok/s</td><td>Token generation speed (tokens/sec) — main performance metric</td></tr>
|
| 59 |
+
<tr><td>Prefill tok/s</td><td>Prompt processing speed (tokens/sec)</td></tr>
|
| 60 |
+
<tr><td>n_eval</td><td>Number of tokens generated during decode</td></tr>
|
| 61 |
+
<tr><td>t_eval (ms)</td><td>Total decode time in milliseconds</td></tr>
|
| 62 |
+
<tr><td>n_p_eval</td><td>Number of prompt tokens processed during prefill</td></tr>
|
| 63 |
+
<tr><td>t_p_eval (ms)</td><td>Total prefill time in milliseconds</td></tr>
|
| 64 |
+
<tr><td>Wall (s)</td><td>Total wall-clock time for the benchmark run in seconds (includes model download, load, and inference)</td></tr>
|
| 65 |
+
<tr><td>CPU Match</td><td>Consistency with CPU baseline — percentage of token positions where WebGPU and CPU agree on the top-1 token. Only present when benchmarks are run with <code>--consistency</code>. See Consistency Measurement below.</td></tr>
|
| 66 |
+
<tr><td>Error</td><td>Error message and category (OOM, WASM Abort, Timeout, etc.) when status is FAIL</td></tr>
|
| 67 |
+
</tbody>
|
| 68 |
+
</table>
|
| 69 |
+
|
| 70 |
+
<h2>Error Categories</h2>
|
| 71 |
+
<table>
|
| 72 |
+
<thead>
|
| 73 |
+
<tr><th>Category</th><th>Pattern</th><th>Typical Cause</th></tr>
|
| 74 |
+
</thead>
|
| 75 |
+
<tbody>
|
| 76 |
+
<tr><td>OOM</td><td>out of memory, memory allocation</td><td>Model too large for available WASM memory</td></tr>
|
| 77 |
+
<tr><td>WASM Abort</td><td>wasm, abort, unreachable</td><td>WASM execution error, often from unsupported operations</td></tr>
|
| 78 |
+
<tr><td>Timeout</td><td>timeout, timed out</td><td>Benchmark exceeded time limit (model download or inference)</td></tr>
|
| 79 |
+
<tr><td>Download Failed</td><td>download, fetch, 404, network</td><td>Model file not found or network error</td></tr>
|
| 80 |
+
<tr><td>Other</td><td>everything else</td><td>Uncategorized errors</td></tr>
|
| 81 |
+
</tbody>
|
| 82 |
+
</table>
|
| 83 |
+
|
| 84 |
+
<h2>Consistency Measurement</h2>
|
| 85 |
+
<p>The <code>--consistency</code> flag measures how faithfully the WebGPU backend reproduces the CPU computation for each quantization type.</p>
|
| 86 |
+
|
| 87 |
+
<h3>How it works</h3>
|
| 88 |
+
<p>For each variant, two runs are performed:</p>
|
| 89 |
+
<ol>
|
| 90 |
+
<li><strong>CPU baseline</strong> (<code>n_gpu_layers=0</code>): greedy-decodes 128 tokens and records the token ID sequence. Cached to <code>results/cpu_baselines.json</code>. When testing multiple browsers, the baseline is collected once on the first browser and shared across all browsers (CPU output is identical regardless of JSPI vs Asyncify). When testing a single browser, the baseline runs in that same browser.</li>
|
| 91 |
+
<li><strong>WebGPU run</strong> (<code>n_gpu_layers=999</code>): performs a forced-decoding pass — feeds the CPU's token sequence one token at a time and checks whether the WebGPU backend independently predicts the same top-1 token at each position.</li>
|
| 92 |
+
</ol>
|
| 93 |
+
|
| 94 |
+
<h3>Why forced decoding</h3>
|
| 95 |
+
<p>Naively comparing generated text suffers from cascading divergence: a single token difference changes the KV cache context for all subsequent tokens. Forced decoding evaluates each position independently, giving a clean per-token accuracy signal.</p>
|
| 96 |
+
|
| 97 |
+
<h3>Interpreting CPU Match</h3>
|
| 98 |
+
<table>
|
| 99 |
+
<thead>
|
| 100 |
+
<tr><th>CPU Match</th><th>Interpretation</th></tr>
|
| 101 |
+
</thead>
|
| 102 |
+
<tbody>
|
| 103 |
+
<tr><td><code>100.0%</code></td><td>Numerically identical to CPU — no precision issues</td></tr>
|
| 104 |
+
<tr><td><code>95–99%</code></td><td>A few tokens differ due to near-equal logits — expected for lower-precision quants</td></tr>
|
| 105 |
+
<tr><td><code>< 90%</code></td><td>Systematic precision issues — GPU kernel may need investigation</td></tr>
|
| 106 |
+
<tr><td><code>0.0%</code></td><td>First token wrong — quantization kernel likely broken</td></tr>
|
| 107 |
+
<tr><td><code>—</code></td><td>No consistency data — benchmarks were run without <code>--consistency</code></td></tr>
|
| 108 |
+
</tbody>
|
| 109 |
+
</table>
|
| 110 |
+
</div>
|
| 111 |
+
</body>
|
| 112 |
+
</html>
|
run/index.html
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" data-theme="light">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<meta name="color-scheme" content="light dark">
|
| 7 |
+
<script>(function(){var t=localStorage.getItem('theme')||'light';document.documentElement.setAttribute('data-theme',t);})();</script>
|
| 8 |
+
<title>Run — WebGPU Bench</title>
|
| 9 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 11 |
+
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
| 12 |
+
<link rel="stylesheet" href="../css/style.css">
|
| 13 |
+
<!-- Import map so `@huggingface/hub` resolves in the browser via esm.sh.
|
| 14 |
+
Must appear before any <script type="module">. -->
|
| 15 |
+
<script type="importmap">
|
| 16 |
+
{
|
| 17 |
+
"imports": {
|
| 18 |
+
"@huggingface/hub": "https://esm.sh/@huggingface/hub"
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
</script>
|
| 22 |
+
</head>
|
| 23 |
+
<body>
|
| 24 |
+
<header class="header">
|
| 25 |
+
<div class="header-inner">
|
| 26 |
+
<a href="../index.html" class="header-brand">
|
| 27 |
+
<svg class="header-logo" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/></svg>
|
| 28 |
+
<span class="header-title">WebGPU Bench</span>
|
| 29 |
+
</a>
|
| 30 |
+
<nav class="header-nav">
|
| 31 |
+
<a href="../index.html" class="header-link">Dashboard</a>
|
| 32 |
+
<a href="../methodology.html" class="header-link">Methodology</a>
|
| 33 |
+
<button id="theme-toggle" class="header-link theme-toggle-btn" type="button" title="Toggle theme">
|
| 34 |
+
<svg class="icon-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
| 35 |
+
<svg class="icon-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
| 36 |
+
</button>
|
| 37 |
+
<a href="https://github.com/abhijitramesh/webgpu-bench" target="_blank" rel="noopener" class="header-link">
|
| 38 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.44 9.8 8.2 11.39.6.11.82-.26.82-.58v-2.03c-3.34.73-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.09-.74.08-.73.08-.73 1.2.09 1.84 1.24 1.84 1.24 1.07 1.83 2.81 1.3 3.5 1 .11-.78.42-1.3.76-1.6-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.13-.3-.54-1.52.12-3.18 0 0 1.01-.32 3.3 1.23a11.5 11.5 0 0 1 6.02 0c2.28-1.55 3.29-1.23 3.29-1.23.66 1.66.25 2.88.12 3.18.77.84 1.24 1.91 1.24 3.22 0 4.61-2.81 5.63-5.48 5.92.43.37.81 1.1.81 2.22v3.29c0 .32.22.7.82.58C20.57 21.8 24 17.3 24 12c0-6.63-5.37-12-12-12z"/></svg>
|
| 39 |
+
GitHub
|
| 40 |
+
</a>
|
| 41 |
+
</nav>
|
| 42 |
+
</div>
|
| 43 |
+
</header>
|
| 44 |
+
|
| 45 |
+
<main>
|
| 46 |
+
<section id="run-section" class="dash-section">
|
| 47 |
+
<div class="container">
|
| 48 |
+
<div class="section-header">
|
| 49 |
+
<h2>Run a benchmark</h2>
|
| 50 |
+
<span id="run-mode-badge" class="badge run-mode-badge">…</span>
|
| 51 |
+
</div>
|
| 52 |
+
|
| 53 |
+
<!-- Read-only banner (shown on GH Pages — no backend, no OAuth) -->
|
| 54 |
+
<div id="run-pages-banner" class="run-pages-banner" hidden>
|
| 55 |
+
<span>Read-only mode — run benchmarks locally but submit via the <a href="https://abhijitramesh-webgpu-bench.static.hf.space/run/" target="_blank" rel="noopener">HF Space</a>.</span>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
<!-- HF sign-in + submit (space surface only) -->
|
| 59 |
+
<div id="hub-row" class="card hub-row" hidden>
|
| 60 |
+
<div class="hub-row-inner">
|
| 61 |
+
<div class="hub-row-info">
|
| 62 |
+
<span id="hf-user"></span>
|
| 63 |
+
</div>
|
| 64 |
+
<div class="hub-row-actions">
|
| 65 |
+
<button id="btn-signin" class="btn btn-secondary" type="button">Sign in with Hugging Face</button>
|
| 66 |
+
<button id="btn-submit" class="btn btn-primary" type="button" disabled hidden>Submit to leaderboard</button>
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
|
| 71 |
+
<!-- Device & budget -->
|
| 72 |
+
<div class="summary-grid run-device-grid">
|
| 73 |
+
<div class="stat-card run-device-card">
|
| 74 |
+
<span class="stat-card-label">Device</span>
|
| 75 |
+
<div class="run-device-rows">
|
| 76 |
+
<div class="run-device-row"><span class="run-device-row-label">Browser</span><span class="run-device-row-value" id="device-browser">—</span></div>
|
| 77 |
+
<div class="run-device-row"><span class="run-device-row-label">Platform</span><span class="run-device-row-value" id="device-platform">—</span></div>
|
| 78 |
+
<div class="run-device-row"><span class="run-device-row-label">GPU</span><span class="run-device-row-value" id="device-gpu">—</span></div>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
<div class="stat-card run-device-card">
|
| 82 |
+
<span class="stat-card-label">Capability</span>
|
| 83 |
+
<div class="run-device-rows">
|
| 84 |
+
<div class="run-device-row"><span class="run-device-row-label">deviceMemory</span><span class="run-device-row-value" id="device-memory">—</span></div>
|
| 85 |
+
<div class="run-device-row"><span class="run-device-row-label">WebGPU</span><span class="run-device-row-value" id="device-webgpu">—</span></div>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
<div class="stat-card run-device-card">
|
| 89 |
+
<span class="stat-card-label">Model budget</span>
|
| 90 |
+
<div class="run-device-rows">
|
| 91 |
+
<div class="run-device-row"><span class="run-device-row-label">Max size</span><span class="run-device-row-value" id="device-budget">—</span></div>
|
| 92 |
+
<div class="run-device-note" id="device-budget-source"></div>
|
| 93 |
+
</div>
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
|
| 97 |
+
<!-- Hide filters, iterations, actions -->
|
| 98 |
+
<div class="filter-bar">
|
| 99 |
+
<div class="filter-bar-inner run-filters">
|
| 100 |
+
<div class="filter-group">
|
| 101 |
+
<span class="filter-label">Hide</span>
|
| 102 |
+
<div class="run-filters-checks">
|
| 103 |
+
<label class="run-hide-label"><input type="checkbox" id="hide-ud"> UD</label>
|
| 104 |
+
<label class="run-hide-label"><input type="checkbox" id="hide-iq"> IQ</label>
|
| 105 |
+
<label class="run-hide-label"><input type="checkbox" id="hide-hifp"> BF16/F16</label>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
<div class="filter-group">
|
| 109 |
+
<label class="filter-label" for="iterations-input">Iterations</label>
|
| 110 |
+
<input type="number" id="iterations-input" class="filter-select run-iter-input" value="5" min="1" max="50" step="1">
|
| 111 |
+
</div>
|
| 112 |
+
<div class="run-actions">
|
| 113 |
+
<span id="queue-status"></span>
|
| 114 |
+
<button class="btn btn-secondary" id="btn-download" type="button" disabled>Download selected</button>
|
| 115 |
+
<button class="btn btn-primary" id="btn-run" type="button" disabled>Run benchmarks</button>
|
| 116 |
+
<button class="btn btn-danger" id="btn-abort" type="button" hidden>Abort</button>
|
| 117 |
+
<button class="btn btn-secondary" id="btn-purge" type="button" hidden>Purge OPFS cache</button>
|
| 118 |
+
</div>
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
|
| 122 |
+
<!-- Family / variant list -->
|
| 123 |
+
<div id="run-models" class="run-models-stack">
|
| 124 |
+
<div class="empty-state">Loading models…</div>
|
| 125 |
+
</div>
|
| 126 |
+
|
| 127 |
+
<!-- Progress -->
|
| 128 |
+
<div class="section-header">
|
| 129 |
+
<h3 class="subsection-title">Progress</h3>
|
| 130 |
+
</div>
|
| 131 |
+
<div class="table-card">
|
| 132 |
+
<div id="run-progress-wrapper" class="results-wrapper"></div>
|
| 133 |
+
</div>
|
| 134 |
+
|
| 135 |
+
<!-- Output -->
|
| 136 |
+
<div class="section-header" style="margin-top: 32px;">
|
| 137 |
+
<h3 class="subsection-title">Output</h3>
|
| 138 |
+
</div>
|
| 139 |
+
<div class="card run-output">
|
| 140 |
+
<label id="save-local-row" class="run-output-toggle" hidden>
|
| 141 |
+
<input type="checkbox" id="save-local" checked>
|
| 142 |
+
Save to <code>results/results.json</code> on this server
|
| 143 |
+
</label>
|
| 144 |
+
<textarea id="output-textarea" class="run-output-textarea" readonly spellcheck="false"></textarea>
|
| 145 |
+
<div class="run-output-buttons">
|
| 146 |
+
<button class="btn btn-secondary" id="btn-copy" type="button">Copy</button>
|
| 147 |
+
<button class="btn btn-secondary" id="btn-download-json" type="button">Download JSON</button>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
|
| 151 |
+
<!-- Log -->
|
| 152 |
+
<details id="run-log" class="card run-log" style="margin-top: 16px;">
|
| 153 |
+
<summary>Run log</summary>
|
| 154 |
+
<pre id="log-output" class="run-log-pre"></pre>
|
| 155 |
+
</details>
|
| 156 |
+
</div>
|
| 157 |
+
</section>
|
| 158 |
+
</main>
|
| 159 |
+
|
| 160 |
+
<script type="module">
|
| 161 |
+
import { mountRunSection } from '../js/run/controller.js';
|
| 162 |
+
// Theme toggle wiring (kept here since app.js no longer runs on this page).
|
| 163 |
+
document.getElementById('theme-toggle')?.addEventListener('click', () => {
|
| 164 |
+
const next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
| 165 |
+
document.documentElement.setAttribute('data-theme', next);
|
| 166 |
+
localStorage.setItem('theme', next);
|
| 167 |
+
});
|
| 168 |
+
mountRunSection();
|
| 169 |
+
</script>
|
| 170 |
+
</body>
|
| 171 |
+
</html>
|