fix(library): read a frame's date with one set of options
exifr keeps a page-long cache of the options object it is handed, keyed by that object itself, so a fresh literal per frame pinned a frame's worth of tag sets (about thirty Sets) for as long as the tab lived: 3601 frames left ~16MB of heap behind, 9601 left ~50MB, and a roll of a hundred thousand frames is what a tab cannot hold. One options object per call site now, which is what that cache is for. Measured on the deployed bundle: 3601 frames rest at 8MB, not 23MB. scripts/ram-check.mjs reads a roll of many folders, queues folders behind the reading in front of them and reports the heap left after a forced collection, which is how those numbers were taken.
This commit is contained in:
@@ -151,11 +151,31 @@ async function shrinkOrKeep(bytes: Uint8Array, maxDim: number, maxBytes: number)
|
||||
// hands it back as a Date in ours — which is what a camera prints on a stamp, and
|
||||
// what someone opening a 2019 photo today expects to read under the coordinates.
|
||||
// Null when the file carries no date at all; the caller falls back to its clock.
|
||||
// `exifr` keeps a private cache of the parsed options it is handed, keyed by the
|
||||
// options object itself, and every entry in it lives as long as the page. A
|
||||
// fresh literal per frame is therefore a frame's worth of tag sets held forever:
|
||||
// reading a roll of a hundred thousand frames is what fills the tab. So the
|
||||
// options are written once here and the same object is handed over every time —
|
||||
// which is also what the cache is for.
|
||||
const DATE_OPTIONS = { pick: ['DateTimeOriginal', 'CreateDate', 'ModifyDate'] };
|
||||
const SPEC_OPTIONS = {
|
||||
pick: [
|
||||
'Make',
|
||||
'Model',
|
||||
'ISO',
|
||||
'ISOSpeedRatings',
|
||||
'FocalLength',
|
||||
'FNumber',
|
||||
'ExposureTime',
|
||||
'ExifImageWidth',
|
||||
'ExifImageHeight',
|
||||
],
|
||||
};
|
||||
const ISO_OPTIONS = { pick: ['ISO', 'ISOSpeedRatings'] };
|
||||
|
||||
export async function readCapturedAt(bytes: Uint8Array): Promise<number | null> {
|
||||
try {
|
||||
const tags = (await exifr.parse(bytes, {
|
||||
pick: ['DateTimeOriginal', 'CreateDate', 'ModifyDate'],
|
||||
})) as Record<string, unknown> | undefined;
|
||||
const tags = (await exifr.parse(bytes, DATE_OPTIONS)) as Record<string, unknown> | undefined;
|
||||
for (const tag of ['DateTimeOriginal', 'CreateDate', 'ModifyDate']) {
|
||||
const value = tags?.[tag];
|
||||
if (value instanceof Date && Number.isFinite(value.getTime())) return value.getTime();
|
||||
@@ -187,9 +207,7 @@ export interface ShotSpecs {
|
||||
|
||||
export async function readSpecs(bytes: Uint8Array): Promise<ShotSpecs | null> {
|
||||
try {
|
||||
const tags = (await exifr.parse(bytes, {
|
||||
pick: ['Make', 'Model', 'ISO', 'ISOSpeedRatings', 'FocalLength', 'FNumber', 'ExposureTime', 'ExifImageWidth', 'ExifImageHeight'],
|
||||
})) as Record<string, unknown> | undefined;
|
||||
const tags = (await exifr.parse(bytes, SPEC_OPTIONS)) as Record<string, unknown> | undefined;
|
||||
if (!tags) return null;
|
||||
const num = (v: unknown): number | null => {
|
||||
const n = Number(Array.isArray(v) ? v[0] : v);
|
||||
@@ -263,7 +281,7 @@ export async function readGps(bytes: Uint8Array): Promise<GPSInfo | null> {
|
||||
// the caller then prints "AUTO" instead of inventing a number.
|
||||
export async function readIso(bytes: Uint8Array): Promise<number | null> {
|
||||
try {
|
||||
const tags = (await exifr.parse(bytes, { pick: ['ISO', 'ISOSpeedRatings'] })) as
|
||||
const tags = (await exifr.parse(bytes, ISO_OPTIONS)) as
|
||||
| { ISO?: unknown; ISOSpeedRatings?: unknown }
|
||||
| undefined;
|
||||
const raw = tags?.ISO ?? tags?.ISOSpeedRatings;
|
||||
|
||||
Reference in New Issue
Block a user