mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 06:16:20 -06:00
Ingest asked for metadata() then writeThumbnail(), which decoded the file twice.
That pairing was free under sharp, whose .metadata() only parses the header, but
every decode here is a full one — ~1s for a 12MP photo — so the naive translation
doubled the most expensive thing on the ingest path.
image-ops.measureAndThumbnail() returns both from one decode. Full ingest of a
12MP photo: 2 decodes/~1.9s -> 1150ms, still with zero event-loop stalls.
The subtlety is the failure contract. In the two-call version width and height
were assigned BEFORE the thumbnail was attempted, so a failed thumbnail still left
usable dimensions on the row — the player needs them to letterbox. Merging naively
would have turned any thumbnail failure into total metadata loss. So a WRITE
failure is reported ({thumbnailWritten:false, thumbnailError}) with the dimensions
intact, and the caller sets thumbnail_path only when the write succeeded, keeping
the phantom-path discipline. A DECODE failure still throws — there is nothing to
report about an unreadable image.
backfill-rotation-dims.js deliberately keeps the separate calls: it probes every
image row but regenerates a thumbnail only for the few whose dimensions changed,
so pairing them there would decode files it has no reason to thumbnail.
Tests count decodes rather than timing them — an exact property, and a wall-clock
comparison would be flaky under load. The count filters for reads of the file under
test: Node's ESM loader also goes through fs.promises.readFile, so a raw call count
picks up jimp's and the WASM codecs' lazy loading and reads 30 instead of 1.
1649/1649 pass. Ingest re-verified across all 7 formats with sharp moved aside.
31 lines
1.2 KiB
JavaScript
31 lines
1.2 KiB
JavaScript
'use strict';
|
|
|
|
/*
|
|
* Worker-thread host for image-ops-core. One job per message, one reply per job, keyed by id.
|
|
*
|
|
* Deliberately thin: every decision (queueing, lifecycle, fallback) lives in ../lib/image-ops so
|
|
* there is one place to reason about them. This end only does the work and reports what happened.
|
|
*
|
|
* Errors come back as a message rather than a thrown exception, so one undecodable upload does
|
|
* not tear down the worker and take the queued jobs of unrelated callers with it.
|
|
*/
|
|
|
|
const { parentPort } = require('worker_threads');
|
|
const core = require('./image-ops-core');
|
|
|
|
const OPS = {
|
|
metadata: (job) => core.metadata(job.src),
|
|
writeThumbnail: (job) => core.writeThumbnail(job.src, job.dest, job.width, job.quality),
|
|
measureAndThumbnail: (job) => core.measureAndThumbnail(job.src, job.dest, job.width, job.quality),
|
|
};
|
|
|
|
parentPort.on('message', async (job) => {
|
|
try {
|
|
const op = OPS[job.op];
|
|
if (!op) throw new Error(`unknown image op: ${job.op}`);
|
|
parentPort.postMessage({ id: job.id, ok: true, result: await op(job) });
|
|
} catch (err) {
|
|
parentPort.postMessage({ id: job.id, ok: false, error: err && err.message ? err.message : String(err) });
|
|
}
|
|
});
|