diff --git a/demo/index.html b/demo/index.html index 01a3dd9..055d436 100644 --- a/demo/index.html +++ b/demo/index.html @@ -10,6 +10,8 @@ button { font: inherit; padding: 0.5rem 1rem; cursor: pointer; } pre { background: #111; color: #b9f; padding: 1rem; border-radius: 6px; overflow: auto; } .ok { color: #5c5; } .bad { color: #f55; } .dim { color: #888; } + .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; } + @keyframes spin { to { transform: rotate(360deg); } } @@ -35,6 +37,7 @@

BrowserVec — M1 (flat GPU brute-force)

+

ready.
@@ -44,6 +47,8 @@

BrowserVec — M1 (flat GPU brute-force)

const out = document.getElementById('out'); const log = (msg, cls = '') => (out.innerHTML += `\n${cls ? `${msg}` : msg}`); + const showLoader = () => document.getElementById('loader').style.display = 'inline-block'; + const hideLoader = () => document.getElementById('loader').style.display = 'none'; function randVec(dim) { const v = new Float32Array(dim); @@ -75,9 +80,10 @@

BrowserVec — M1 (flat GPU brute-force)

document.getElementById('run').onclick = async () => { out.textContent = ''; + showLoader(); const support = BrowserVec.isSupported(); log(`support: ${JSON.stringify(support)}`); - if (!support.webgpu) return log('WebGPU not available in this browser.', 'bad'); + if (!support.webgpu) { hideLoader(); return log('WebGPU not available in this browser.', 'bad'); } const n = +document.getElementById('n').value; const dim = +document.getElementById('dim').value; @@ -112,12 +118,14 @@

BrowserVec — M1 (flat GPU brute-force)

log(`top hit: ${gpuIds[0]} (gpu score ${hits[0].score.toFixed(4)}, cpu ${ref[0].score.toFixed(4)})`); db.destroy(); + hideLoader(); }; // M3: compare int8 TurboQuant (with GPU kernel) against fp32 flat ground truth. document.getElementById('quant').onclick = async () => { out.textContent = ''; - if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad'); + showLoader(); + if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); } const n = +document.getElementById('n').value; const dim = +document.getElementById('dim').value; @@ -166,6 +174,7 @@

BrowserVec — M1 (flat GPU brute-force)

qd.destroy(); } log(`(raw recall drops as bits shrink — int4, then 1-bit; exact re-rank recovers it — that's the design)`, 'dim'); + hideLoader(); }; function nextPow2(n) { let p = 4; while (p < n) p <<= 1; return p; } @@ -191,6 +200,7 @@

BrowserVec — M1 (flat GPU brute-force)

// adapter limits, OPFS/WASM-SIMD/Worker support, and the CPU-fallback path. document.getElementById('m6report').onclick = async () => { out.textContent = ''; + showLoader(); log('Running M6 device report — ~10–40s depending on device. Keep this tab focused.\n', 'dim'); // Deterministic PRNG (mulberry32) → identical corpus on every device. @@ -309,12 +319,14 @@

BrowserVec — M1 (flat GPU brute-force)

log(JSON.stringify(report)); log('=== END BROWSERVEC M6 REPORT ===', 'ok'); log('\nSelect everything between the BEGIN/END lines and paste it back (one per device).', 'dim'); + hideLoader(); }; // M4: IVF approximate index vs fp32 flat ground truth, sweeping nprobe. document.getElementById('ivf').onclick = async () => { out.textContent = ''; - if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad'); + showLoader(); + if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); } const n = +document.getElementById('n').value; const dim = +document.getElementById('dim').value; @@ -365,13 +377,15 @@

BrowserVec — M1 (flat GPU brute-force)

); } db.destroy(); + hideLoader(); }; // M4 combo: IVF × int8 — clusters + quantizes so 1M fits AND only nprobe lists // are scanned. Compares against fp32 flat truth and reports memory + recall. document.getElementById('combo').onclick = async () => { out.textContent = ''; - if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad'); + showLoader(); + if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); } const n = +document.getElementById('n').value; const dim = +document.getElementById('dim').value; @@ -441,12 +455,14 @@

BrowserVec — M1 (flat GPU brute-force)

log(` nprobe=${nlist} (full scan, diagnostic): recall@${k}=${(dh / (queries * k)).toFixed(3)}`, 'dim'); db.destroy(); } + hideLoader(); }; // M5: end-to-end text retrieval, fully offline (zero-dep hashing embedder). document.getElementById('text').onclick = async () => { out.textContent = ''; - if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad'); + showLoader(); + if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); } const embedder = hashingEmbedder({ dimension: 384 }); const db = await BrowserVec.create({ dimension: 384, metric: 'cosine', embedder }); @@ -470,6 +486,7 @@

BrowserVec — M1 (flat GPU brute-force)

log(`\nnote: hashingEmbedder is lexical (keyword), not semantic.`, 'dim'); log(`swap transformersEmbedder() for meaning — same store/query code.`, 'dim'); db.destroy(); + hideLoader(); }; // NFR-8: the CPU-heavy rotate+quantize of a quantized ingest runs in a Web @@ -479,7 +496,8 @@

BrowserVec — M1 (flat GPU brute-force)

// the UI would stay interactive during ingest. document.getElementById('worker').onclick = async () => { out.textContent = ''; - if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad'); + showLoader(); + if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); } const n = +document.getElementById('n').value; const dim = +document.getElementById('dim').value; @@ -573,6 +591,7 @@

BrowserVec — M1 (flat GPU brute-force)

log(`(centroid mean-update ran ${tmode === 'worker' ? 'in the Worker' : 'in-thread'}; ` + `results are byte-identical either way)`, 'dim'); idb.destroy(); + hideLoader(); }; // NFR-10: a single GPU storage buffer is capped (128 MiB on many devices), @@ -582,7 +601,8 @@

BrowserVec — M1 (flat GPU brute-force)

// store (flat is exact, so results must be identical, not just close). document.getElementById('chunk').onclick = async () => { out.textContent = ''; - if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad'); + showLoader(); + if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); } const n = +document.getElementById('n').value; const dim = +document.getElementById('dim').value; @@ -641,6 +661,7 @@

BrowserVec — M1 (flat GPU brute-force)

db.destroy(); } log(`\n→ chunked scan is exact — a corpus can now exceed one GPU buffer.`, 'ok'); + hideLoader(); }; // GPU top-k: past a few thousand rows the flat index reduces on the GPU @@ -650,7 +671,8 @@

BrowserVec — M1 (flat GPU brute-force)

// switch-over threshold to show the readback that was eliminated. document.getElementById('gputopk').onclick = async () => { out.textContent = ''; - if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad'); + showLoader(); + if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); } const dim = +document.getElementById('dim').value; const k = 10; @@ -774,6 +796,7 @@

BrowserVec — M1 (flat GPU brute-force)

log(`\n→ GPU top-k returns the same neighbors while reading back only` + ` ceil(N/256)·k pairs instead of all N scores — now on the quantized and IVF paths too.`, 'ok'); + hideLoader(); }; // M6 / NFR-7: on browsers without WebGPU the library falls back to an exact @@ -782,7 +805,8 @@

BrowserVec — M1 (flat GPU brute-force)

// returns the same top-k as the real GPU path on the same data. document.getElementById('cpufallback').onclick = async () => { out.textContent = ''; - if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad'); + showLoader(); + if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); } const dim = +document.getElementById('dim').value; const n = Math.min(20000, +document.getElementById('n').value); @@ -826,14 +850,16 @@

BrowserVec — M1 (flat GPU brute-force)

} finally { delete globalThis.__BROWSERVEC_FORCE_CPU__; } + hideLoader(); }; // M2: save to OPFS/IndexedDB, drop the instance, reload, confirm it survives. document.getElementById('persist').onclick = async () => { out.textContent = ''; + showLoader(); const support = BrowserVec.isSupported(); log(`support: ${JSON.stringify(support)}`); - if (!support.webgpu) return log('WebGPU not available.', 'bad'); + if (!support.webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); } const dim = 64; const name = 'demo-session'; @@ -873,6 +899,7 @@

BrowserVec — M1 (flat GPU brute-force)

db2.destroy(); db3.destroy(); + hideLoader(); }; // M6: persisted snapshots + exported blobs encrypted at rest with a passphrase @@ -880,8 +907,9 @@

BrowserVec — M1 (flat GPU brute-force)

// fails loudly. Data never leaves the device — and now it's safe at rest too. document.getElementById('encrypt').onclick = async () => { out.textContent = ''; - if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad'); - if (!globalThis.crypto?.subtle) return log('WebCrypto (crypto.subtle) unavailable.', 'bad'); + showLoader(); + if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); } + if (!globalThis.crypto?.subtle) { hideLoader(); return log('WebCrypto (crypto.subtle) unavailable.', 'bad'); } const dim = 64; const name = 'enc-demo'; @@ -936,6 +964,7 @@

BrowserVec — M1 (flat GPU brute-force)

log(`\nencrypted export→import round-trip: [${imported}]`, imported === before ? 'ok' : 'bad'); db2.destroy(); db3.destroy(); + hideLoader(); }; // Deletion: rows are tombstoned (filtered from results immediately, cheap) and @@ -944,7 +973,8 @@

BrowserVec — M1 (flat GPU brute-force)

// that a reload drops them for good. Runs on fp32 and int8×IVF. document.getElementById('delete').onclick = async () => { out.textContent = ''; - if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad'); + showLoader(); + if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); } const dim = 128; const n = 5000; @@ -985,6 +1015,7 @@

BrowserVec — M1 (flat GPU brute-force)

db.destroy(); reloaded.destroy(); } + hideLoader(); }; diff --git a/examples/benchmark-dashboard.html b/examples/benchmark-dashboard.html index 85e5a94..5c17259 100644 --- a/examples/benchmark-dashboard.html +++ b/examples/benchmark-dashboard.html @@ -32,6 +32,8 @@ .log-area { margin-top: 1rem; } .log-area summary { cursor: pointer; font-weight: 600; color: #555; font-size: .85rem; } ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; } + .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; } + @keyframes spin { to { transform: rotate(360deg); } } @@ -112,6 +114,7 @@

Benchmark Dashboard

+
@@ -151,6 +154,8 @@

Benchmark Dashboard

const $ = (id) => document.getElementById(id); const log = (msg, cls = '') => { $('log').innerHTML += `\n${cls ? `${msg}` : msg}`; }; +const showLoader = () => $('loader').style.display = 'inline-block'; +const hideLoader = () => $('loader').style.display = 'none'; // ---- Synthetic clustered corpus ---- // Uniform-random vectors are a worst case for approximate search (no structure @@ -205,6 +210,7 @@

Benchmark Dashboard

const efSearch = Math.max(parseInt($('hnswEfSearch').value), k); $('runBtn').disabled = true; + showLoader(); $('log').textContent = ''; $('resultsBody').innerHTML = `Running…`; @@ -242,6 +248,7 @@

Benchmark Dashboard

`${n.toLocaleString()} rows × ${dim} dim, ${queryCount} queries, k=${k}. ` + `Ground truth: naive JS brute force (exact). HNSW: efConstruction=${efConstruction}, M=${m}, efSearch=${efSearch}.`; $('runBtn').disabled = false; + hideLoader(); } function updateConfigNote() { diff --git a/examples/benchmark-real-embeddings.html b/examples/benchmark-real-embeddings.html index 1c9fbb0..0472b90 100644 --- a/examples/benchmark-real-embeddings.html +++ b/examples/benchmark-real-embeddings.html @@ -51,6 +51,8 @@ .qcard .hit .score { color: #888; margin-right: .5rem; } .qcard .hit .topic { color: #aaa; font-size: .72rem; } ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; } + .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; } + @keyframes spin { to { transform: rotate(360deg); } } @@ -101,6 +103,7 @@

1. Embedding model

+
Model weights download once (tens of MiB) and are cached by the browser — later runs with the same model @@ -153,6 +156,8 @@

3. Qualitative semantic search

const $ = (id) => document.getElementById(id); const log = (msg, cls = '') => { $('log').innerHTML += `\n${cls ? `${msg}` : msg}`; }; +const showLoader = () => $('loader').style.display = 'inline-block'; +const hideLoader = () => $('loader').style.display = 'none'; // Retrieval-tuned models expect asymmetric prefixes on queries vs. documents — // this is each model's own documented convention (E5: "query:"/"passage:", @@ -241,6 +246,7 @@

3. Qualitative semantic search

const k = parseInt($('kVal').value); $('runBtn').disabled = true; + showLoader(); $('log').textContent = ''; $('resultsBody').innerHTML = `Embedding corpus…`; $('qualitative').innerHTML = ''; @@ -254,6 +260,7 @@

3. Qualitative semantic search

console.error(e); $('resultsBody').innerHTML = `Embedding failed — see log below.`; $('runBtn').disabled = false; + hideLoader(); return; } const { modelCfg, docs, docVectors, queryVectors } = embedded; @@ -298,6 +305,7 @@

3. Qualitative semantic search

await runQualitative(modelCfg, docs, docVectors, queryVectors); $('runBtn').disabled = false; + hideLoader(); } // Builds a fresh BrowserVec (WebGPU flat) instance over the already-embedded diff --git a/examples/perf-benchmark.html b/examples/perf-benchmark.html index eba314b..1e834a5 100644 --- a/examples/perf-benchmark.html +++ b/examples/perf-benchmark.html @@ -39,6 +39,8 @@ .bad { color: #d32f2f; } ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; } .config-note { font-size: .8rem; color: #888; margin-top: .5rem; } + .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; } + @keyframes spin { to { transform: rotate(360deg); } } @@ -77,6 +79,7 @@

Performance Benchmark

+
@@ -108,6 +111,8 @@

Performance Benchmark

const $ = (id) => document.getElementById(id); const log = (msg, cls = '') => $('log').innerHTML += `\n${cls ? `${msg}` : msg}`; +const showLoader = () => $('loader').style.display = 'inline-block'; +const hideLoader = () => $('loader').style.display = 'none'; // ---- Generate synthetic text documents at scale ---- // We create realistic-looking "documents" from a template system so every @@ -411,6 +416,7 @@

Performance Benchmark

log(`Starting benchmark: ${indexType}...`); $('runBtn').disabled = true; $('compareBtn').disabled = true; + showLoader(); try { const results = await benchmark(corpus, dim, indexType); results.label += ` (${n.toLocaleString()} docs)`; @@ -420,6 +426,7 @@

Performance Benchmark

} finally { $('runBtn').disabled = false; $('compareBtn').disabled = false; + hideLoader(); } } @@ -430,6 +437,7 @@

Performance Benchmark

$('runBtn').disabled = true; $('compareBtn').disabled = true; + showLoader(); log(''); log(`========== COMPREHENSIVE BENCHMARK ==========`); log(`Corpus: ${n.toLocaleString()} synthetic documents, ${dim}-dim embeddings`); @@ -460,6 +468,7 @@

Performance Benchmark

$('runBtn').disabled = false; $('compareBtn').disabled = false; + hideLoader(); } // ---- Event handlers ----