Skip to content

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:

  1. Preflight (prepareModels() in downloadManager.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.
  2. Adapters create sessions from cached ArrayBuffers — not URLs:
  3. ONNX: ort.InferenceSession.create(modelBytes, ...) (src/core/runtimes/onnx.ts)
  4. LiteRT: loadAndCompile(modelBytes, { accelerator }) (src/core/runtimes/litert.ts)
  5. Worker path is cache-first toosrc/workers/inference.worker.ts routes through downloadModelWithProgress(), so offloaded inference never re-downloads (Cache API is available in dedicated workers via WorkerGlobalScope.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/Response pairs — 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-cache key (env.useBrowserCache defaults to on when the Cache API is available; confirmed against the transformers.js source).
  • MediaPipe Tasks fetches modelAssetPath through 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-v1v2) 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