diff --git a/docker/frontend/src/engine/library.ts b/docker/frontend/src/engine/library.ts index 1796b46..6420b4f 100644 --- a/docker/frontend/src/engine/library.ts +++ b/docker/frontend/src/engine/library.ts @@ -35,6 +35,23 @@ const BATCH = 50; // the decoder are the limit and the page has less room to breathe. const LANES = 4; +// One LibRaw open at a time, whoever asks for it. A RAW is opened inside a worker +// the library builds with a quarter of a gigabyte of linear memory of its own +// (libraw-wasm instantiates emscripten's shared memory at 256MB), and the open +// copies the frame into it whole: the four lanes are what a folder of RAW would +// otherwise read at once, and measured on a roll of 48 22.6MB frames that is a +// 1335MB page against a 565MB one before the roll is picked — past what a page is +// given, and the page goes with it, which is the crash this answers. One at a time +// the same roll peaks at 1180MB: the frames around the one being opened still have +// their bytes read, their decodes run and their tiles encoded side by side, because +// only the open queues. A folder of JPEGs never touches this. +let rawTurn: Promise = Promise.resolve(); +function oneRawAtATime(fn: () => Promise): Promise { + const turn = rawTurn.then(fn, fn); + rawTurn = turn.catch(() => undefined); + return turn; +} + export interface LibraryFolder { // The directory's own name: the store's key, and the prefix of every frame id // found in it. A rename never touches this. @@ -544,7 +561,13 @@ export async function scanFolder( const raw = isRawName(file.name); const bytes = new Uint8Array(await (raw ? file.arrayBuffer() : file.slice(0, HEAD_BYTES).arrayBuffer())); const preview = raw ? await rawThumbnail(bytes) : null; - const thumb = await (raw ? (preview ? tile(new Blob([preview as BlobPart], { type: 'image/jpeg' })) : null) : tile(file)); + // The tile is the preview the camera wrote inside a RAW, or the frame itself. + // Both carry their own size in their first few hundred bytes — the same bytes + // the date comes out of — so the decoder is handed the size instead of being + // asked with a second decode of the whole frame to learn which edge is long. + const src = preview ? new Blob([preview as BlobPart], { type: 'image/jpeg' }) : raw ? null : file; + const size = preview ? jpegSize(preview) : raw ? null : jpegSize(bytes); + const thumb = src ? await tile(src, size) : null; const taken = (await readCapturedAt(bytes)) ?? file.lastModified; const cut = rel.lastIndexOf('/'); batch.push({ @@ -591,7 +614,10 @@ export async function scanFolder( progress.done++; count(rel); } - const lane = readOne(handle, rel) + // A RAW takes the one open there is before its lane does anything, so a + // folder of them is read a frame at a time whatever the lanes say. + const read = isRawName(handle.name) ? oneRawAtATime(() => readOne(handle, rel)) : readOne(handle, rel); + const lane = read .catch(() => undefined) .finally(() => lanes.delete(lane)); lanes.add(lane);