Skip to content

Commit 1dcb341

Browse files
Merge pull request #3 from sharma-open-source/doc-and-example-improvments
Add loading indicators to benchmark and demo pages
2 parents 7546b60 + c3ffc9f commit 1dcb341

4 files changed

Lines changed: 68 additions & 13 deletions

File tree

‎demo/index.html‎

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
button { font: inherit; padding: 0.5rem 1rem; cursor: pointer; }
1111
pre { background: #111; color: #b9f; padding: 1rem; border-radius: 6px; overflow: auto; }
1212
.ok { color: #5c5; } .bad { color: #f55; } .dim { color: #888; }
13+
.loader { display: none; width: 16px; height: 16px; border: 2px solid #ddd; border-top-color: #5c5; border-radius: 50%; animation: spin .6s linear infinite; margin-left: .5rem; vertical-align: middle; }
14+
@keyframes spin { to { transform: rotate(360deg); } }
1315
</style>
1416
</head>
1517
<body>
@@ -35,6 +37,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
3537
<button id="persist">Test persistence (M2)</button>
3638
<button id="encrypt">Encrypted persist (M6)</button>
3739
<button id="delete">Delete + compaction</button>
40+
<span class="loader" id="loader"></span>
3841
</p>
3942
<pre id="out">ready.</pre>
4043

@@ -44,6 +47,8 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
4447
const out = document.getElementById('out');
4548
const log = (msg, cls = '') =>
4649
(out.innerHTML += `\n${cls ? `<span class="${cls}">${msg}</span>` : msg}`);
50+
const showLoader = () => document.getElementById('loader').style.display = 'inline-block';
51+
const hideLoader = () => document.getElementById('loader').style.display = 'none';
4752

4853
function randVec(dim) {
4954
const v = new Float32Array(dim);
@@ -75,9 +80,10 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
7580

7681
document.getElementById('run').onclick = async () => {
7782
out.textContent = '';
83+
showLoader();
7884
const support = BrowserVec.isSupported();
7985
log(`support: ${JSON.stringify(support)}`);
80-
if (!support.webgpu) return log('WebGPU not available in this browser.', 'bad');
86+
if (!support.webgpu) { hideLoader(); return log('WebGPU not available in this browser.', 'bad'); }
8187

8288
const n = +document.getElementById('n').value;
8389
const dim = +document.getElementById('dim').value;
@@ -112,12 +118,14 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
112118
log(`top hit: ${gpuIds[0]} (gpu score ${hits[0].score.toFixed(4)}, cpu ${ref[0].score.toFixed(4)})`);
113119

114120
db.destroy();
121+
hideLoader();
115122
};
116123

117124
// M3: compare int8 TurboQuant (with GPU kernel) against fp32 flat ground truth.
118125
document.getElementById('quant').onclick = async () => {
119126
out.textContent = '';
120-
if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad');
127+
showLoader();
128+
if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }
121129

122130
const n = +document.getElementById('n').value;
123131
const dim = +document.getElementById('dim').value;
@@ -166,6 +174,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
166174
qd.destroy();
167175
}
168176
log(`(raw recall drops as bits shrink — int4, then 1-bit; exact re-rank recovers it — that's the design)`, 'dim');
177+
hideLoader();
169178
};
170179
function nextPow2(n) { let p = 4; while (p < n) p <<= 1; return p; }
171180

@@ -191,6 +200,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
191200
// adapter limits, OPFS/WASM-SIMD/Worker support, and the CPU-fallback path.
192201
document.getElementById('m6report').onclick = async () => {
193202
out.textContent = '';
203+
showLoader();
194204
log('Running M6 device report — ~10–40s depending on device. Keep this tab focused.\n', 'dim');
195205

196206
// Deterministic PRNG (mulberry32) → identical corpus on every device.
@@ -309,12 +319,14 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
309319
log(JSON.stringify(report));
310320
log('=== END BROWSERVEC M6 REPORT ===', 'ok');
311321
log('\nSelect everything between the BEGIN/END lines and paste it back (one per device).', 'dim');
322+
hideLoader();
312323
};
313324

314325
// M4: IVF approximate index vs fp32 flat ground truth, sweeping nprobe.
315326
document.getElementById('ivf').onclick = async () => {
316327
out.textContent = '';
317-
if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad');
328+
showLoader();
329+
if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }
318330

319331
const n = +document.getElementById('n').value;
320332
const dim = +document.getElementById('dim').value;
@@ -365,13 +377,15 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
365377
);
366378
}
367379
db.destroy();
380+
hideLoader();
368381
};
369382

370383
// M4 combo: IVF × int8 — clusters + quantizes so 1M fits AND only nprobe lists
371384
// are scanned. Compares against fp32 flat truth and reports memory + recall.
372385
document.getElementById('combo').onclick = async () => {
373386
out.textContent = '';
374-
if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad');
387+
showLoader();
388+
if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }
375389

376390
const n = +document.getElementById('n').value;
377391
const dim = +document.getElementById('dim').value;
@@ -441,12 +455,14 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
441455
log(` nprobe=${nlist} (full scan, diagnostic): recall@${k}=${(dh / (queries * k)).toFixed(3)}`, 'dim');
442456
db.destroy();
443457
}
458+
hideLoader();
444459
};
445460

446461
// M5: end-to-end text retrieval, fully offline (zero-dep hashing embedder).
447462
document.getElementById('text').onclick = async () => {
448463
out.textContent = '';
449-
if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad');
464+
showLoader();
465+
if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }
450466

451467
const embedder = hashingEmbedder({ dimension: 384 });
452468
const db = await BrowserVec.create({ dimension: 384, metric: 'cosine', embedder });
@@ -470,6 +486,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
470486
log(`\nnote: hashingEmbedder is lexical (keyword), not semantic.`, 'dim');
471487
log(`swap transformersEmbedder() for meaning — same store/query code.`, 'dim');
472488
db.destroy();
489+
hideLoader();
473490
};
474491

475492
// NFR-8: the CPU-heavy rotate+quantize of a quantized ingest runs in a Web
@@ -479,7 +496,8 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
479496
// the UI would stay interactive during ingest.
480497
document.getElementById('worker').onclick = async () => {
481498
out.textContent = '';
482-
if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad');
499+
showLoader();
500+
if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }
483501

484502
const n = +document.getElementById('n').value;
485503
const dim = +document.getElementById('dim').value;
@@ -573,6 +591,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
573591
log(`(centroid mean-update ran ${tmode === 'worker' ? 'in the Worker' : 'in-thread'}; ` +
574592
`results are byte-identical either way)`, 'dim');
575593
idb.destroy();
594+
hideLoader();
576595
};
577596

578597
// NFR-10: a single GPU storage buffer is capped (128 MiB on many devices),
@@ -582,7 +601,8 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
582601
// store (flat is exact, so results must be identical, not just close).
583602
document.getElementById('chunk').onclick = async () => {
584603
out.textContent = '';
585-
if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad');
604+
showLoader();
605+
if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }
586606

587607
const n = +document.getElementById('n').value;
588608
const dim = +document.getElementById('dim').value;
@@ -641,6 +661,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
641661
db.destroy();
642662
}
643663
log(`\n→ chunked scan is exact — a corpus can now exceed one GPU buffer.`, 'ok');
664+
hideLoader();
644665
};
645666

646667
// GPU top-k: past a few thousand rows the flat index reduces on the GPU
@@ -650,7 +671,8 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
650671
// switch-over threshold to show the readback that was eliminated.
651672
document.getElementById('gputopk').onclick = async () => {
652673
out.textContent = '';
653-
if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad');
674+
showLoader();
675+
if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }
654676

655677
const dim = +document.getElementById('dim').value;
656678
const k = 10;
@@ -774,6 +796,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
774796

775797
log(`\n→ GPU top-k returns the same neighbors while reading back only` +
776798
` ceil(N/256)·k pairs instead of all N scores — now on the quantized and IVF paths too.`, 'ok');
799+
hideLoader();
777800
};
778801

779802
// M6 / NFR-7: on browsers without WebGPU the library falls back to an exact
@@ -782,7 +805,8 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
782805
// returns the same top-k as the real GPU path on the same data.
783806
document.getElementById('cpufallback').onclick = async () => {
784807
out.textContent = '';
785-
if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad');
808+
showLoader();
809+
if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }
786810

787811
const dim = +document.getElementById('dim').value;
788812
const n = Math.min(20000, +document.getElementById('n').value);
@@ -826,14 +850,16 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
826850
} finally {
827851
delete globalThis.__BROWSERVEC_FORCE_CPU__;
828852
}
853+
hideLoader();
829854
};
830855

831856
// M2: save to OPFS/IndexedDB, drop the instance, reload, confirm it survives.
832857
document.getElementById('persist').onclick = async () => {
833858
out.textContent = '';
859+
showLoader();
834860
const support = BrowserVec.isSupported();
835861
log(`support: ${JSON.stringify(support)}`);
836-
if (!support.webgpu) return log('WebGPU not available.', 'bad');
862+
if (!support.webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }
837863

838864
const dim = 64;
839865
const name = 'demo-session';
@@ -873,15 +899,17 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
873899

874900
db2.destroy();
875901
db3.destroy();
902+
hideLoader();
876903
};
877904

878905
// M6: persisted snapshots + exported blobs encrypted at rest with a passphrase
879906
// (AES-256-GCM, PBKDF2 key). Auto-load decrypts; a wrong passphrase or tamper
880907
// fails loudly. Data never leaves the device — and now it's safe at rest too.
881908
document.getElementById('encrypt').onclick = async () => {
882909
out.textContent = '';
883-
if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad');
884-
if (!globalThis.crypto?.subtle) return log('WebCrypto (crypto.subtle) unavailable.', 'bad');
910+
showLoader();
911+
if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }
912+
if (!globalThis.crypto?.subtle) { hideLoader(); return log('WebCrypto (crypto.subtle) unavailable.', 'bad'); }
885913

886914
const dim = 64;
887915
const name = 'enc-demo';
@@ -936,6 +964,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
936964
log(`\nencrypted export→import round-trip: [${imported}]`, imported === before ? 'ok' : 'bad');
937965
db2.destroy();
938966
db3.destroy();
967+
hideLoader();
939968
};
940969

941970
// Deletion: rows are tombstoned (filtered from results immediately, cheap) and
@@ -944,7 +973,8 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
944973
// that a reload drops them for good. Runs on fp32 and int8×IVF.
945974
document.getElementById('delete').onclick = async () => {
946975
out.textContent = '';
947-
if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad');
976+
showLoader();
977+
if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }
948978

949979
const dim = 128;
950980
const n = 5000;
@@ -985,6 +1015,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
9851015
db.destroy();
9861016
reloaded.destroy();
9871017
}
1018+
hideLoader();
9881019
};
9891020
</script>
9901021
</body>

‎examples/benchmark-dashboard.html‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
.log-area { margin-top: 1rem; }
3333
.log-area summary { cursor: pointer; font-weight: 600; color: #555; font-size: .85rem; }
3434
::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; }
35+
.loader { display: none; width: 18px; height: 18px; border: 2px solid #ddd; border-top-color: #1a73e8; border-radius: 50%; animation: spin .6s linear infinite; margin-left: .5rem; vertical-align: middle; }
36+
@keyframes spin { to { transform: rotate(360deg); } }
3537
</style>
3638
</head>
3739
<body>
@@ -112,6 +114,7 @@ <h1>Benchmark Dashboard</h1>
112114
</select>
113115
</label>
114116
<button id="runBtn">Run Benchmark</button>
117+
<span class="loader" id="loader"></span>
115118
</div>
116119
<div class="note" id="configNote"></div>
117120

@@ -151,6 +154,8 @@ <h1>Benchmark Dashboard</h1>
151154

152155
const $ = (id) => document.getElementById(id);
153156
const log = (msg, cls = '') => { $('log').innerHTML += `\n${cls ? `<span class="${cls}">${msg}</span>` : msg}`; };
157+
const showLoader = () => $('loader').style.display = 'inline-block';
158+
const hideLoader = () => $('loader').style.display = 'none';
154159

155160
// ---- Synthetic clustered corpus ----
156161
// Uniform-random vectors are a worst case for approximate search (no structure
@@ -205,6 +210,7 @@ <h1>Benchmark Dashboard</h1>
205210
const efSearch = Math.max(parseInt($('hnswEfSearch').value), k);
206211

207212
$('runBtn').disabled = true;
213+
showLoader();
208214
$('log').textContent = '';
209215
$('resultsBody').innerHTML = `<tr><td colspan="9" style="text-align:center;color:#999;">Running…</td></tr>`;
210216

@@ -242,6 +248,7 @@ <h1>Benchmark Dashboard</h1>
242248
`${n.toLocaleString()} rows × ${dim} dim, ${queryCount} queries, k=${k}. ` +
243249
`Ground truth: naive JS brute force (exact). HNSW: efConstruction=${efConstruction}, M=${m}, efSearch=${efSearch}.`;
244250
$('runBtn').disabled = false;
251+
hideLoader();
245252
}
246253

247254
function updateConfigNote() {

‎examples/benchmark-real-embeddings.html‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
.qcard .hit .score { color: #888; margin-right: .5rem; }
5252
.qcard .hit .topic { color: #aaa; font-size: .72rem; }
5353
::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; }
54+
.loader { display: none; width: 18px; height: 18px; border: 2px solid #ddd; border-top-color: #1a73e8; border-radius: 50%; animation: spin .6s linear infinite; margin-left: .5rem; vertical-align: middle; }
55+
@keyframes spin { to { transform: rotate(360deg); } }
5456
</style>
5557
</head>
5658
<body>
@@ -101,6 +103,7 @@ <h2>1. Embedding model</h2>
101103
</select>
102104
</label>
103105
<button id="runBtn">Embed corpus &amp; run benchmark</button>
106+
<span class="loader" id="loader"></span>
104107
</div>
105108
<div class="note" id="configNote">
106109
Model weights download once (tens of MiB) and are cached by the browser — later runs with the same model
@@ -153,6 +156,8 @@ <h2>3. Qualitative semantic search</h2>
153156

154157
const $ = (id) => document.getElementById(id);
155158
const log = (msg, cls = '') => { $('log').innerHTML += `\n${cls ? `<span class="${cls}">${msg}</span>` : msg}`; };
159+
const showLoader = () => $('loader').style.display = 'inline-block';
160+
const hideLoader = () => $('loader').style.display = 'none';
156161

157162
// Retrieval-tuned models expect asymmetric prefixes on queries vs. documents —
158163
// this is each model's own documented convention (E5: "query:"/"passage:",
@@ -241,6 +246,7 @@ <h2>3. Qualitative semantic search</h2>
241246
const k = parseInt($('kVal').value);
242247

243248
$('runBtn').disabled = true;
249+
showLoader();
244250
$('log').textContent = '';
245251
$('resultsBody').innerHTML = `<tr><td colspan="9" style="text-align:center;color:#999;">Embedding corpus…</td></tr>`;
246252
$('qualitative').innerHTML = '';
@@ -254,6 +260,7 @@ <h2>3. Qualitative semantic search</h2>
254260
console.error(e);
255261
$('resultsBody').innerHTML = `<tr><td colspan="9" style="text-align:center;color:#d32f2f;">Embedding failed — see log below.</td></tr>`;
256262
$('runBtn').disabled = false;
263+
hideLoader();
257264
return;
258265
}
259266
const { modelCfg, docs, docVectors, queryVectors } = embedded;
@@ -298,6 +305,7 @@ <h2>3. Qualitative semantic search</h2>
298305
await runQualitative(modelCfg, docs, docVectors, queryVectors);
299306

300307
$('runBtn').disabled = false;
308+
hideLoader();
301309
}
302310

303311
// Builds a fresh BrowserVec (WebGPU flat) instance over the already-embedded

0 commit comments

Comments
 (0)