What happens
A RAW frame that is already in the persistent cache -- a few milliseconds of work -- still waits its full turn behind every queued conversion, each of which is about 1.9 s.
Why
The cache lookup lives inside the worker. convertWithCache runs in src/lib/raw-worker.ts:96, and the worker only reaches it once the frame has arrived by postMessage:
self.addEventListener("message", (event: MessageEvent<RawConvertRequest>) => {
convert(event.data)
The queue is on the page, in src/lib/raw-worker-client.ts:68, and it is strictly ahead of that:
const conversion = queue.then(() => send(path, bytes, wasmBaseUrl));
So the ordering is: wait for the whole queue, then discover the answer was already on disk.
Why it matters
This is exactly the case #243 was built for -- a user who reopens the app with the same bracket. Every frame is a hit, so the whole set could appear at once. Instead it appears one frame at a time at conversion pace, because each hit is queued behind the ones in front of it, and the queue exists to serialise a ~266 MiB wasm instance that a cache hit never allocates.
The cost is not theoretical: a 10-frame bracket that is entirely cached still takes as long to display as the queue takes to walk it.
Two ways to fix it, and the trade-off is real
Move the lookup to the page. rawCacheKey needs the file bytes and toolTag needs the wasm base URL; both are available in raw-preview.ts, and IndexedDB is reachable from the page. A hit then resolves without touching the queue at all. Cost: key derivation hashes the RAW bytes on the main thread, which is the kind of work the worker exists to keep off it. Worth measuring before assuming it is cheap.
Add a peek message. Keep the lookup in the worker, but answer peeks out of band rather than through the conversion queue. Costs a second channel or a message kind, and the worker is single-threaded, so a peek still waits for whatever callMain is currently blocking on -- which is the very thing being avoided.
Neither is obviously right, which is why this is filed rather than folded into #248.
Note
Found while designing #248, which had assumed the opposite: that once the persistent tier existed, a queued frame might be a cache hit costing milliseconds and dropping it would be pointless. Reading the code showed the hit is only discovered after the wait, so dropping queued frames stays worthwhile -- and this, the reason the assumption was made, is worth fixing separately.
Deliberately out of scope for #248: it reworks the page/worker boundary that #243 has just established.
What happens
A RAW frame that is already in the persistent cache -- a few milliseconds of work -- still waits its full turn behind every queued conversion, each of which is about 1.9 s.
Why
The cache lookup lives inside the worker.
convertWithCacheruns insrc/lib/raw-worker.ts:96, and the worker only reaches it once the frame has arrived bypostMessage:The queue is on the page, in
src/lib/raw-worker-client.ts:68, and it is strictly ahead of that:So the ordering is: wait for the whole queue, then discover the answer was already on disk.
Why it matters
This is exactly the case #243 was built for -- a user who reopens the app with the same bracket. Every frame is a hit, so the whole set could appear at once. Instead it appears one frame at a time at conversion pace, because each hit is queued behind the ones in front of it, and the queue exists to serialise a ~266 MiB wasm instance that a cache hit never allocates.
The cost is not theoretical: a 10-frame bracket that is entirely cached still takes as long to display as the queue takes to walk it.
Two ways to fix it, and the trade-off is real
Move the lookup to the page.
rawCacheKeyneeds the file bytes andtoolTagneeds the wasm base URL; both are available inraw-preview.ts, and IndexedDB is reachable from the page. A hit then resolves without touching the queue at all. Cost: key derivation hashes the RAW bytes on the main thread, which is the kind of work the worker exists to keep off it. Worth measuring before assuming it is cheap.Add a peek message. Keep the lookup in the worker, but answer peeks out of band rather than through the conversion queue. Costs a second channel or a message kind, and the worker is single-threaded, so a peek still waits for whatever
callMainis currently blocking on -- which is the very thing being avoided.Neither is obviously right, which is why this is filed rather than folded into #248.
Note
Found while designing #248, which had assumed the opposite: that once the persistent tier existed, a queued frame might be a cache hit costing milliseconds and dropping it would be pointless. Reading the code showed the hit is only discovered after the wait, so dropping queued frames stays worthwhile -- and this, the reason the assumption was made, is worth fixing separately.
Deliberately out of scope for #248: it reworks the page/worker boundary that #243 has just established.