Storage Architecture¶
This page documents how benchy/ persists data across the four browser storage mechanisms, why each mechanism was chosen, and the known gaps. It supersedes the prototype's single-key localStorage approach (mlbench_runs JSON array, documented in prototype/index.md).
Overview¶
| Data | Mechanism | Implementation | Rationale |
|---|---|---|---|
| Model bytes | Cache API (mlbench-models-v1) |
src/lib/modelCache.ts, src/lib/downloadManager.ts |
Immutable HTTP resources; Request/Response pairs are the Cache API's native format |
| Benchmark runs | IndexedDB (mlbench-db / mlbench-store) |
src/state/runsStore.ts via idb-keyval |
Structured records, no localStorage size ceiling, survives reloads |
| Capability probe results | IndexedDB (same store) | src/state/envStore.ts |
Sweeps reuse cached probe results with a staleness timestamp |
| Calibration baseline | localStorage | src/core/bench/calibration.ts |
Tiny key/value pair; synchronous read at gate check |
| Theme preference | localStorage (mlbench_theme) |
src/state/themeStore.ts |
Cosmetic; synchronous read before first paint avoids a flash |
| Playground HF models | localStorage (mlbench_hf_models) |
src/pages/PlaygroundPage.tsx |
Tiny JSON list of user-added Hugging Face picks (A19); synchronous read at panel render |
| Consent decision | sessionStorage | src/state/consentStore.ts |
Deliberately per-session; user re-confirms each visit |
All IndexedDB access goes through a dedicated object store (createStore('mlbench-db', 'mlbench-store') in src/state/idb.ts) so app data cannot collide with other origins' keys, and every persistence path degrades gracefully (in-memory fallback) when storage is unavailable (private browsing, quota pressure).
Model Byte Cache¶
Design¶
The Cache API layer guarantees that a second load against the same model URL costs zero network time, so download time never contaminates measured inference:
- Preflight (
prepareModels()indownloadManager.ts) downloads every selected model once with byte-level progress (ReadableStream+Content-Length) and writes the bytes to the Cache API. "Start Benchmark" stays disabled until every selected model is cached. - Adapters create sessions from cached
ArrayBuffers — not URLs: - ONNX:
ort.InferenceSession.create(modelBytes, ...)(src/core/runtimes/onnx.ts) - LiteRT:
loadAndCompile(modelBytes, { accelerator })(src/core/runtimes/litert.ts) - Worker path is cache-first too —
src/workers/inference.worker.tsroutes throughdownloadModelWithProgress(), so offloaded inference never re-downloads (Cache API is available in dedicated workers viaWorkerGlobalScope.caches).
Why Cache API and not IndexedDB for model bytes?¶
Both share the origin storage quota and eviction policy (see MDN: Storage quotas). The split follows each API's intended use:
- Cache API stores HTTP
Request/Responsepairs — model files are exactly that, and the API makes URL-keyed lookup a one-liner (cache.match(url)). - IndexedDB stores structured, queryable records — benchmark runs are exactly that.
Self-caching runtimes (no explicit cache needed)¶
- Transformers.js manages its own browser cache via the Cache API under the
transformers-cachekey (env.useBrowserCachedefaults to on when the Cache API is available; confirmed against the transformers.js source). - MediaPipe Tasks fetches
modelAssetPaththrough its own loading path with standard HTTP caching.
Known Gap: TF.js and ML5 Bypass the Model Cache¶
Documented limitation — decision CLOSED (impl-plan-benchy item #7)
TFJSAdapter (tf.loadGraphModel(url)) and ML5Adapter (ml5.imageClassifier(url)) fetch their model JSON + weight shards directly by URL, bypassing the Cache API model cache. TF.js performs no browser-storage caching of its own, and ML5's high-level API does not expose cache control.
Impact: For these two adapters, a cold browser HTTP cache means the model download happens inside the measured load() phase. This does not corrupt inference metrics — load time is reported separately and excluded from inference statistics — but loadTimeMs for TF.js/ML5 is not comparable with the cache-first ONNX/LiteRT path when the HTTP cache is cold.
Mitigations in place:
- All model URLs are same-origin (
/models/tfjs/...), so standard HTTP caching applies and warm loads are fast. - The download preflight warms the HTTP cache for these URLs even though it cannot hand bytes to the runtime.
Why not fixed: A proper fix requires a custom TF.js IOHandler that reassembles the model JSON + shard set from cached bytes (the graph-model format is a JSON manifest plus binary shard files, not a single blob). The effort is significant and the benchmark impact is nil, since load time is already excluded from inference stats.
Future Hardening (documented, not implemented)¶
Cache Invalidation¶
The Cache API store (mlbench-models-v1) is keyed by URL only — there is no versioning or busting logic. If vendored model bytes ever change (e.g., a new pinned version in scripts/vendor-models.mjs with a different sha), a browser that still holds the old bytes under the same URL would keep serving them from the cache.
Current risk: low. Model URLs are version-pinned paths under /models/... maintained by the vendor script, and the sha manifest (public/models/*) is committed — but the Cache API layer does not consult that manifest. If model artifacts are ever updated in place, either:
- bump the cache name (
mlbench-models-v1→v2) to force a cold cache, or - key cache entries by content sha instead of URL.
Storage Persistence¶
Both the Cache API and IndexedDB live under the origin storage quota and are evictable under disk pressure ("best-effort" mode). A long offline demo could lose cached models or run history if the browser evicts origin data.
Optional hardening: call navigator.storage.persist() once at startup to request persistent storage, which opts the origin out of eviction under pressure (the browser may still show a permission prompt or grant based on engagement heuristics). modelCacheSizeEstimate() in modelCache.ts already reads navigator.storage.estimate(); a persistence request would complement it. Not implemented because eviction has not been observed in practice and the API's grant behavior varies by browser.
Prototype vs benchy¶
| Aspect | Prototype (prototype/) |
benchy (benchy/) |
|---|---|---|
| Run storage | localStorage (mlbench_runs, JSON array) |
IndexedDB via idb-keyval |
| Size ceiling | ~5–10 MB per origin | Origin storage quota (hundreds of MB+) |
| Model caching | None explicit (CDN + HTTP cache only) | Cache API, cache-first, sessions from bytes |
| Download gating | None | Preflight gate: Start disabled until all models cached |
| Worker inference | n/a | Cache-first (fixed per runtime audit, see research/audit-benchy-runtimes-aug-2026.md in the repo) |
| Consent/calibration | n/a | sessionStorage / localStorage |