Skip to content

Cache RAW-to-TIFF conversions in OPFS so they survive a reload #243

Description

@adulbrich

Split out of #232 (Phase 4), which is otherwise complete.

What exists

convert_raw_img's replacement rides on the dcraw_emu WebAssembly build, and #242 made the resulting TIFF shared between the viewer's preview and the pipeline, so a RAW file is converted once per session rather than twice.

Per session. The cache lives in memory and dies with the tab. Reload the page, or come back tomorrow, and every RAW in the bracket is converted again from scratch.

Why it matters

RAW conversion is the most expensive single thing the app does on a CR2 set, and it is pure recomputation: the same input bytes always produce the same TIFF. The desktop build used to keep this on disk (src-tauri/src/image_cache/, keyed by compute_hash_for_file) and lost that when the pipeline moved to WebAssembly, so this is a regression for desktop users as well as a gap for browser ones.

Depends on the RAW conversion worker

FileSystemFileHandle.createSyncAccessHandle() -- the fast OPFS path -- is callable only from a dedicated Web Worker. Not the main thread, not an iframe, not a SharedWorker. It is excluded there deliberately, because synchronous I/O on the main thread blocks rendering.

So this needs a dedicated worker that holds the RAW bytes. Moving dcraw_emu off the main thread (src/lib/raw-worker.ts) is being done separately, for its own reason -- callMain is synchronous, so converting a 10-frame CR2 bracket for thumbnails froze the tab for ~20 s. That worker is where this belongs: it already receives the source bytes, so it can hash them without a second read, and the persistent tier never has to cross back to the page.

Scope

  • Back the RAW-to-TIFF cache with OPFS, so a conversion survives a reload.
  • Two tiers, two keys. The session cache stays keyed on path|size:mtime, which costs no file read at all when a frame is already resident. Only the OPFS tier is keyed by a content hash, and it is consulted just on a session-cache miss -- where the bytes have been read anyway. Hashing on every request instead would mean reading and digesting ~250 MB per session for a 10-frame bracket to avoid conversions the in-memory cache had already avoided.
  • Key the OPFS tier by content hash rather than path, so a file that moved is still a hit and a file that changed is not.
  • Bound it, and evict least-recently-used first. A 10-frame CR2 bracket is hundreds of megabytes of TIFF, so an unbounded cache is a way to fill someone's disk quietly.
  • Keep an index. OPFS exposes no access time, so eviction needs sizes and last-used stamps recorded alongside the blobs -- a small record in OPFS or in IndexedDB. Naming the mechanism here because "evict oldest-first" is not implementable from OPFS metadata on its own.
  • Serve the desktop build from the same code. There is one pipeline now, and a second caching implementation behind isTauri() would be the thing that drifts.

Open question

Whether OPFS behaves in all three Tauri v2 webviews: WKWebView on macOS, WebView2 on Windows, WebKitGTK on Linux. Sharing one implementation across desktop and browser is a design constraint above, not an observation, and it should be verified before it is relied on -- WebKitGTK especially, and given the OPFS write bugs that have shown up in Safari.

Notes

OPFS is available in every target browser including Safari. It needs no permission prompt, unlike the File System Access API, which Safari does not implement at all.

Worth considering later, not worth building first: memoising fingerprint -> content hash would let an unchanged file skip the read and the digest on a later session.

Not a blocker for the deployment: the app is correct without it, just slower on a second visit.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions