GitHub Actions commited on
Commit
da0c2f2
·
1 Parent(s): b0f367a

sync from abhijitramesh/webgpu-bench@3fae9c4594

Browse files
Files changed (2) hide show
  1. js/run/controller.js +69 -16
  2. js/run/source.js +8 -5
js/run/controller.js CHANGED
@@ -55,6 +55,11 @@ const state = {
55
  sessionDownloads: new Set(),
56
  // Handle to the currently-running worker, so Abort can terminate it.
57
  currentWorker: null,
 
 
 
 
 
58
  // Build metadata fetched from `build/<variant>/build-info.json`. Stamped
59
  // onto every result record so we can compare performance across llama.cpp
60
  // versions. JSPI and Asyncify variants are built from the same source
@@ -62,6 +67,15 @@ const state = {
62
  buildInfo: null,
63
  };
64
 
 
 
 
 
 
 
 
 
 
65
  async function loadBuildInfo() {
66
  // Try jspi first (Chrome path), fall back to asyncify (Safari/Firefox path).
67
  // Either contains the same llama.cpp commit/describe.
@@ -987,25 +1001,31 @@ async function onDownloadClick() {
987
  if (state.aborted) break;
988
  const row = progressRowFor(v);
989
  row.setStatus('downloading', '');
 
 
990
  try {
991
- const { stream, contentLength } = await state.source.fetchModel(v.repo, v.filename);
992
  const reader = stream.getReader();
993
  let read = 0;
994
  while (true) {
995
- if (state.aborted) { try { reader.cancel(); } catch {} break; }
996
  const { done, value } = await reader.read();
997
  if (done) break;
998
  read += value.length;
999
  row.setProgress(contentLength ? read / contentLength : 0, read, contentLength);
1000
  }
1001
- if (!state.aborted) {
1002
  state.cacheStatus[cacheKey(v)] = { cachedBytes: read };
1003
  refreshCacheBadge(v);
1004
  row.setStatus('cached', formatSize(read / (1024 * 1024)));
 
 
1005
  }
1006
  } catch (err) {
1007
- row.setStatus('error', err.message);
1008
- logLine(`Download failed: ${v.filename}: ${err.message}`);
 
 
1009
  }
1010
  }
1011
 
@@ -1094,12 +1114,14 @@ async function onRunClick() {
1094
  if (skipPrefetch) return;
1095
  const row = progressRowFor(v);
1096
  row.setStatus('prefetching', '');
 
 
1097
  try {
1098
- const { stream, contentLength } = await state.source.fetchModel(v.repo, v.filename);
1099
  const reader = stream.getReader();
1100
  let read = 0;
1101
  while (true) {
1102
- if (state.aborted) { try { reader.cancel(); } catch {} return; }
1103
  const { done, value } = await reader.read();
1104
  if (done) break;
1105
  read += value.length;
@@ -1110,8 +1132,14 @@ async function onRunClick() {
1110
  refreshCacheBadge(v);
1111
  row.setStatus('cached', formatSize(read / (1024 * 1024)));
1112
  } catch (err) {
 
 
 
 
1113
  row.setStatus('error', `prefetch: ${err.message}`);
1114
  logLine(`Prefetch failed: ${v.filename}: ${err.message}`);
 
 
1115
  }
1116
  };
1117
 
@@ -1220,13 +1248,16 @@ function runInWorker({
1220
 
1221
  state.currentWorker = worker;
1222
  let settled = false;
 
1223
  const finish = (record) => {
1224
  if (settled) return;
1225
  settled = true;
1226
  try { worker.terminate(); } catch { /* noop */ }
1227
  if (state.currentWorker === worker) state.currentWorker = null;
 
1228
  resolve(record);
1229
  };
 
1230
 
1231
  worker.onmessage = (e) => {
1232
  const msg = e.data || {};
@@ -1376,11 +1407,14 @@ async function runBenchmarkInWorker(v, params, callbacks) {
1376
 
1377
  if (useOpfs) {
1378
  let contentLength;
 
 
1379
  try {
1380
  callbacks.onStatus?.('downloading', 'Downloading model to OPFS...');
1381
  const r = await state.source.opfsHandleForModel(
1382
  v.repo, v.filename,
1383
  callbacks.onProgress,
 
1384
  );
1385
  contentLength = r.size;
1386
  // When the prefetch is skipped (mobile path), the inline download
@@ -1393,7 +1427,15 @@ async function runBenchmarkInWorker(v, params, callbacks) {
1393
  refreshCacheBadge(v);
1394
  }
1395
  } catch (err) {
 
 
 
1396
  return { status: 'error', error: `opfsHandleForModel failed: ${err.message}` };
 
 
 
 
 
1397
  }
1398
  // Pass the OPFS path components, not the FileHandle. iOS Safari
1399
  // (and some older Chromium/Firefox versions) can't structured-clone
@@ -1410,11 +1452,19 @@ async function runBenchmarkInWorker(v, params, callbacks) {
1410
  }
1411
 
1412
  let fetched;
 
 
1413
  try {
1414
- fetched = await state.source.fetchModel(v.repo, v.filename);
1415
  } catch (err) {
 
 
1416
  return { status: 'error', error: `fetchModel failed: ${err.message}` };
1417
  }
 
 
 
 
1418
 
1419
  return runInWorker({
1420
  params: { ...baseParams, contentLength: fetched.contentLength },
@@ -1783,15 +1833,18 @@ function wireAbortHandler() {
1783
  state.aborted = true;
1784
  const ab = $('btn-abort');
1785
  if (ab) ab.disabled = true;
1786
- // Terminating the worker immediately stops the in-flight iteration —
1787
- // JSPI/Asyncify loops don't respond to cooperative signals.
1788
- if (state.currentWorker) {
1789
- try { state.currentWorker.terminate(); } catch {}
1790
- state.currentWorker = null;
1791
- logLine('Abort requested terminated in-flight worker.');
1792
- } else {
1793
- logLine('Abort requested — will stop between variants.');
1794
  }
 
 
 
 
1795
  });
1796
  }
1797
 
 
55
  sessionDownloads: new Set(),
56
  // Handle to the currently-running worker, so Abort can terminate it.
57
  currentWorker: null,
58
+ // Set of fns that abort an in-flight async op (worker terminate, fetch
59
+ // signal abort). Multiple concurrent ops register here — Run study has a
60
+ // worker running variant i AND a prefetch downloading variant i+1, both
61
+ // of which need to be cancellable. Abort handler iterates the whole set.
62
+ abortHandlers: new Set(),
63
  // Build metadata fetched from `build/<variant>/build-info.json`. Stamped
64
  // onto every result record so we can compare performance across llama.cpp
65
  // versions. JSPI and Asyncify variants are built from the same source
 
67
  buildInfo: null,
68
  };
69
 
70
+ // Register an abort callback for an in-flight async op (worker terminate,
71
+ // fetch signal abort, etc.). Returns an unregister fn the caller MUST
72
+ // invoke when the op settles, so we don't accumulate stale handlers across
73
+ // runs. Abort handler iterates state.abortHandlers and calls every fn.
74
+ function registerAbort(fn) {
75
+ state.abortHandlers.add(fn);
76
+ return () => state.abortHandlers.delete(fn);
77
+ }
78
+
79
  async function loadBuildInfo() {
80
  // Try jspi first (Chrome path), fall back to asyncify (Safari/Firefox path).
81
  // Either contains the same llama.cpp commit/describe.
 
1001
  if (state.aborted) break;
1002
  const row = progressRowFor(v);
1003
  row.setStatus('downloading', '');
1004
+ const ac = new AbortController();
1005
+ const unregister = registerAbort(() => ac.abort());
1006
  try {
1007
+ const { stream, contentLength } = await state.source.fetchModel(v.repo, v.filename, ac.signal);
1008
  const reader = stream.getReader();
1009
  let read = 0;
1010
  while (true) {
1011
+ if (ac.signal.aborted) { try { reader.cancel(); } catch {} break; }
1012
  const { done, value } = await reader.read();
1013
  if (done) break;
1014
  read += value.length;
1015
  row.setProgress(contentLength ? read / contentLength : 0, read, contentLength);
1016
  }
1017
+ if (!ac.signal.aborted) {
1018
  state.cacheStatus[cacheKey(v)] = { cachedBytes: read };
1019
  refreshCacheBadge(v);
1020
  row.setStatus('cached', formatSize(read / (1024 * 1024)));
1021
+ } else {
1022
+ row.setStatus('aborted', '');
1023
  }
1024
  } catch (err) {
1025
+ if (ac.signal.aborted) { row.setStatus('aborted', ''); }
1026
+ else { row.setStatus('error', err.message); logLine(`Download failed: ${v.filename}: ${err.message}`); }
1027
+ } finally {
1028
+ unregister();
1029
  }
1030
  }
1031
 
 
1114
  if (skipPrefetch) return;
1115
  const row = progressRowFor(v);
1116
  row.setStatus('prefetching', '');
1117
+ const ac = new AbortController();
1118
+ const unregister = registerAbort(() => ac.abort());
1119
  try {
1120
+ const { stream, contentLength } = await state.source.fetchModel(v.repo, v.filename, ac.signal);
1121
  const reader = stream.getReader();
1122
  let read = 0;
1123
  while (true) {
1124
+ if (ac.signal.aborted) { try { reader.cancel(); } catch {} return; }
1125
  const { done, value } = await reader.read();
1126
  if (done) break;
1127
  read += value.length;
 
1132
  refreshCacheBadge(v);
1133
  row.setStatus('cached', formatSize(read / (1024 * 1024)));
1134
  } catch (err) {
1135
+ if (ac.signal.aborted) {
1136
+ row.setStatus('aborted', '');
1137
+ return;
1138
+ }
1139
  row.setStatus('error', `prefetch: ${err.message}`);
1140
  logLine(`Prefetch failed: ${v.filename}: ${err.message}`);
1141
+ } finally {
1142
+ unregister();
1143
  }
1144
  };
1145
 
 
1248
 
1249
  state.currentWorker = worker;
1250
  let settled = false;
1251
+ let unregister = () => {};
1252
  const finish = (record) => {
1253
  if (settled) return;
1254
  settled = true;
1255
  try { worker.terminate(); } catch { /* noop */ }
1256
  if (state.currentWorker === worker) state.currentWorker = null;
1257
+ unregister();
1258
  resolve(record);
1259
  };
1260
+ unregister = registerAbort(() => finish({ status: 'aborted', error: 'aborted by user' }));
1261
 
1262
  worker.onmessage = (e) => {
1263
  const msg = e.data || {};
 
1407
 
1408
  if (useOpfs) {
1409
  let contentLength;
1410
+ const ac = new AbortController();
1411
+ const unregister = registerAbort(() => ac.abort());
1412
  try {
1413
  callbacks.onStatus?.('downloading', 'Downloading model to OPFS...');
1414
  const r = await state.source.opfsHandleForModel(
1415
  v.repo, v.filename,
1416
  callbacks.onProgress,
1417
+ ac.signal,
1418
  );
1419
  contentLength = r.size;
1420
  // When the prefetch is skipped (mobile path), the inline download
 
1427
  refreshCacheBadge(v);
1428
  }
1429
  } catch (err) {
1430
+ if (ac.signal.aborted) {
1431
+ return { status: 'aborted', error: 'aborted by user' };
1432
+ }
1433
  return { status: 'error', error: `opfsHandleForModel failed: ${err.message}` };
1434
+ } finally {
1435
+ unregister();
1436
+ }
1437
+ if (state.aborted) {
1438
+ return { status: 'aborted', error: 'aborted by user' };
1439
  }
1440
  // Pass the OPFS path components, not the FileHandle. iOS Safari
1441
  // (and some older Chromium/Firefox versions) can't structured-clone
 
1452
  }
1453
 
1454
  let fetched;
1455
+ const ac = new AbortController();
1456
+ const unregister = registerAbort(() => ac.abort());
1457
  try {
1458
+ fetched = await state.source.fetchModel(v.repo, v.filename, ac.signal);
1459
  } catch (err) {
1460
+ unregister();
1461
+ if (ac.signal.aborted) return { status: 'aborted', error: 'aborted by user' };
1462
  return { status: 'error', error: `fetchModel failed: ${err.message}` };
1463
  }
1464
+ unregister();
1465
+ if (state.aborted) {
1466
+ return { status: 'aborted', error: 'aborted by user' };
1467
+ }
1468
 
1469
  return runInWorker({
1470
  params: { ...baseParams, contentLength: fetched.contentLength },
 
1833
  state.aborted = true;
1834
  const ab = $('btn-abort');
1835
  if (ab) ab.disabled = true;
1836
+ // Iterate every registered op (worker terminate, fetch AbortController):
1837
+ // worker.terminate() alone leaves the Promise pending forever, and
1838
+ // fetch without a signal can hang on slow connections. Each fn is
1839
+ // expected to also resolve / reject its own awaiting promise.
1840
+ const n = state.abortHandlers.size;
1841
+ for (const fn of state.abortHandlers) {
1842
+ try { fn(); } catch { /* keep iterating */ }
 
1843
  }
1844
+ state.abortHandlers.clear();
1845
+ logLine(n > 0
1846
+ ? `Abort requested — cancelled ${n} in-flight op${n === 1 ? '' : 's'}.`
1847
+ : 'Abort requested — will stop between variants.');
1848
  });
1849
  }
1850
 
js/run/source.js CHANGED
@@ -108,7 +108,7 @@ export function hostedSource() {
108
  // (fraction, downloaded, total). The returned `wasDownloaded` flag
109
  // distinguishes a fresh download from a cache hit so the caller can
110
  // decide whether to mark the variant for post-run eviction.
111
- async opfsHandleForModel(repo, file, onProgress) {
112
  const cached = await getOpfsFileHandle(repo, file, { create: false }).catch(() => null);
113
  if (cached) {
114
  const f = await cached.getFile();
@@ -119,8 +119,10 @@ export function hostedSource() {
119
  }
120
 
121
  // Cache miss — download from HF straight into a writable OPFS stream.
 
 
122
  const url = `https://huggingface.co/${repo}/resolve/main/${file}`;
123
- const resp = await fetch(url);
124
  if (!resp.ok) {
125
  throw new Error(`Download failed: ${resp.status} ${resp.statusText}`);
126
  }
@@ -150,7 +152,7 @@ export function hostedSource() {
150
  }
151
  },
152
 
153
- async fetchModel(repo, file) {
154
  // Cache hit → stream the OPFS file straight out.
155
  try {
156
  const handle = await getOpfsFileHandle(repo, file, { create: false });
@@ -164,9 +166,10 @@ export function hostedSource() {
164
  }
165
  } catch { /* miss — fall through */ }
166
 
167
- // Miss: fetch from HF, tee to OPFS + caller.
 
168
  const url = `https://huggingface.co/${repo}/resolve/main/${file}`;
169
- const resp = await fetch(url);
170
  if (!resp.ok) {
171
  throw new Error(`Download failed: ${resp.status} ${resp.statusText}`);
172
  }
 
108
  // (fraction, downloaded, total). The returned `wasDownloaded` flag
109
  // distinguishes a fresh download from a cache hit so the caller can
110
  // decide whether to mark the variant for post-run eviction.
111
+ async opfsHandleForModel(repo, file, onProgress, signal) {
112
  const cached = await getOpfsFileHandle(repo, file, { create: false }).catch(() => null);
113
  if (cached) {
114
  const f = await cached.getFile();
 
119
  }
120
 
121
  // Cache miss — download from HF straight into a writable OPFS stream.
122
+ // signal lets the caller cancel: fetch + reader.read both reject with
123
+ // AbortError when it fires, and the catch below propagates that up.
124
  const url = `https://huggingface.co/${repo}/resolve/main/${file}`;
125
+ const resp = await fetch(url, { signal });
126
  if (!resp.ok) {
127
  throw new Error(`Download failed: ${resp.status} ${resp.statusText}`);
128
  }
 
152
  }
153
  },
154
 
155
+ async fetchModel(repo, file, signal) {
156
  // Cache hit → stream the OPFS file straight out.
157
  try {
158
  const handle = await getOpfsFileHandle(repo, file, { create: false });
 
166
  }
167
  } catch { /* miss — fall through */ }
168
 
169
+ // Miss: fetch from HF, tee to OPFS + caller. signal lets the caller
170
+ // abort the network request; the tee'd reader inherits the abort.
171
  const url = `https://huggingface.co/${repo}/resolve/main/${file}`;
172
+ const resp = await fetch(url, { signal });
173
  if (!resp.ok) {
174
  throw new Error(`Download failed: ${resp.status} ${resp.statusText}`);
175
  }