Files
gods-eye-view/src/layers/weather/infraredImage.test.mjs
T
Mazeyar Moeini FeizabadiandClaude Fable 5.2 0b5ae6386e perf(weather): apply the infrared cloud transfer from a lookup table, in place
The Satellite clouds transfer ran a power curve per pixel and wrote the
result back from a full-size copy. On the 4096x2048 images the 3D Tiles
host draws, that held the page for about 150 ms per image. The scale
depends only on the brightest channel, so it is now worked out once per
byte value and applied to the canvas pixels in place. Output bytes are
unchanged.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-13 14:42:57 +01:00

360 lines
10 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
acquireInfraredMosaic,
acquireWeatherImage,
decodeInfraredImage,
processInfraredImage,
MAX_MOSAIC_BYTES,
} from './infraredImage.js';
function canvas() {
const output = {};
output.getContext = () => ({
setTransform: (...matrix) => {
output.transforms = [...(output.transforms ?? []), matrix];
},
drawImage: (image) => {
output.image = image;
output.drawnWith = output.transforms?.at(-1) ?? null;
},
getImageData: () => ({ data: new Uint8ClampedArray([194, 0, 0, 200]) }),
putImageData: (pixels) => {
output.pixels = pixels.data;
},
});
return output;
}
test('the transfer is applied to the canvas pixels in place, with no copy written back', () => {
let read = null;
const output = {};
output.getContext = () => ({
drawImage() {},
getImageData: () => {
read = { data: new Uint8ClampedArray([194, 0, 0, 200]) };
// A second full-size buffer copied over the first is what this guards.
read.data.set = () => {
throw new Error('the canvas pixels were overwritten from a copy');
};
return read;
},
putImageData: (pixels) => {
output.pixels = pixels;
},
});
processInfraredImage({ width: 1, height: 1 }, 'filtered', () => output);
assert.equal(output.pixels, read);
assert.deepEqual([...read.data], [194, 0, 0, 98]);
});
test('decoded pixels are processed once on a same-sized canvas', () => {
const image = { width: 1, height: 1 };
const output = processInfraredImage(image, 'filtered', canvas);
assert.equal(output.width, 1);
assert.equal(output.height, 1);
assert.equal(output.image, image);
assert.deepEqual([...output.pixels], [194, 0, 0, 98]);
assert.deepEqual(
[...processInfraredImage(image, 50, canvas).pixels],
[194, 0, 0, 149],
'half-strength filter retains a midpoint haze',
);
assert.deepEqual(
[...processInfraredImage(image, 0, canvas).pixels],
[194, 0, 0, 200],
);
assert.deepEqual(
[...processInfraredImage(image, 'full', canvas).pixels],
[194, 0, 0, 200],
);
});
test('flipY draws a bottom-up image upright, then restores the transform', () => {
const image = { width: 4, height: 8 };
assert.equal(processInfraredImage(image, 'full', canvas).drawnWith, null);
const output = processInfraredImage(image, 'full', canvas, { flipY: true });
assert.deepEqual(output.drawnWith, [1, 0, 0, -1, 0, 8]);
assert.deepEqual(output.transforms.at(-1), [1, 0, 0, 1, 0, 0]);
});
test('mosaic fetch is capped at 4 MiB, forwards cancellation, and releases decoded images', async () => {
const controller = new AbortController();
let closes = 0,
fetches = 0,
clock = 10,
fetched = false;
const options = {
signal: controller.signal,
mode: 'filtered',
createCanvas: canvas,
now: () => clock,
onFetched: () => {
fetched = true;
},
fetchImpl: async (url, { signal }) => {
fetches++;
assert.match(url, /^\/api\/weather\/image\?product=clouds&time=/);
assert.equal(signal, controller.signal);
return new Response(new Uint8Array([1, 2, 3]));
},
decodeImage: async (blob) => {
assert.equal(fetched, true);
assert.equal(blob.size, 3);
assert.equal(blob.type, 'image/png');
clock = 25;
return {
width: 2048,
height: 1024,
close() {
closes++;
},
};
},
};
const result = await acquireInfraredMosaic(
'2026-09-15T20:00:00.000Z',
options,
);
assert.equal(fetches, 1);
assert.equal(closes, 1);
assert.equal(result.decodeMs, 15);
assert.equal(result.texture.width, 2048);
for (const response of [
new Response('', {
headers: { 'content-length': String(MAX_MOSAIC_BYTES + 1) },
}),
new Response(new Uint8Array(MAX_MOSAIC_BYTES + 1)),
]) {
await assert.rejects(
acquireInfraredMosaic('2026-09-15', {
...options,
fetchImpl: async () => response,
decodeImage: () => assert.fail('oversized image must not decode'),
}),
{ code: 'RESPONSE_TOO_LARGE' },
);
}
await assert.rejects(
acquireInfraredMosaic('2026-09-15', {
...options,
decodeImage: async () => {
controller.abort();
return {
width: 2048,
height: 1024,
close() {
closes++;
},
};
},
}),
{ name: 'AbortError' },
);
assert.equal(closes, 2);
});
test('bitmap decoding and Image fallback release resources on success, error and abort', async (t) => {
const original = Object.getOwnPropertyDescriptor(
globalThis,
'createImageBitmap',
);
t.after(() => {
if (original)
Object.defineProperty(globalThis, 'createImageBitmap', original);
else delete globalThis.createImageBitmap;
});
const bitmap = {};
globalThis.createImageBitmap = async () => bitmap;
assert.equal(
await decodeInfraredImage(new Blob(), new AbortController().signal),
bitmap,
);
globalThis.createImageBitmap = undefined;
const images = [],
revoked = [];
const originalImage = globalThis.Image;
globalThis.Image = class {
constructor() {
images.push(this);
}
};
t.after(() => {
if (originalImage) globalThis.Image = originalImage;
else delete globalThis.Image;
});
t.mock.method(URL, 'createObjectURL', () => 'blob:test');
t.mock.method(URL, 'revokeObjectURL', (url) => revoked.push(url));
for (const action of ['load', 'error', 'abort']) {
const controller = new AbortController();
const pending = decodeInfraredImage(new Blob(), controller.signal);
const image = images.at(-1);
assert.equal(image.src, 'blob:test');
if (action === 'load') {
image.onload();
assert.equal(await pending, image);
} else if (action === 'error') {
image.onerror();
await assert.rejects(pending, /decode failed/);
} else {
controller.abort();
await assert.rejects(pending, { name: 'AbortError' });
assert.equal(image.src, '');
}
assert.equal(image.onload, null);
assert.equal(image.onerror, null);
}
assert.deepEqual(revoked, ['blob:test', 'blob:test', 'blob:test']);
});
const pixels = (width, height) => ({
width,
height,
closes: 0,
close() {
this.closes++;
},
});
test('a frame source supplies the image: nothing is fetched or decoded and the pixels are released', async () => {
const controller = new AbortController();
const requests = [];
const bitmap = pixels(2048, 1024);
let clock = 5,
fetched = false;
const result = await acquireInfraredMosaic('2026-09-15T20:00:00.000Z', {
signal: controller.signal,
mode: 'filtered',
createCanvas: canvas,
now: () => clock,
onFetched: () => {
fetched = true;
},
fetchImpl: () => assert.fail('a supplied frame is not fetched'),
decodeImage: () => assert.fail('a supplied frame is not decoded'),
frameSource: {
getImage: async (request) => {
requests.push(request);
clock = 12;
return bitmap;
},
},
});
assert.deepEqual(requests, [
{
product: 'clouds',
time: '2026-09-15T20:00:00.000Z',
size: { width: 2048, height: 1024 },
bbox: null,
signal: controller.signal,
},
]);
assert.equal(fetched, true);
assert.equal(result.decodeMs, 7);
assert.equal(result.texture.width, 2048);
assert.equal(result.texture.height, 1024);
assert.equal(result.texture.image, bitmap);
assert.deepEqual(
[...result.texture.pixels],
[194, 0, 0, 98],
'infrared frames get the display transfer',
);
assert.equal(bitmap.closes, 1);
});
test('supplied detail windows carry their bbox; ImageData is put, radar keeps its pixels', async (t) => {
const had = Object.hasOwn(globalThis, 'ImageData');
const previous = globalThis.ImageData;
globalThis.ImageData = class ImageData {
width = 4096;
height = 2048;
data = new Uint8ClampedArray([1, 2, 3, 4]);
};
t.after(() => {
if (had) globalThis.ImageData = previous;
else delete globalThis.ImageData;
});
const data = new globalThis.ImageData();
const bbox = { west: -102, south: 34, east: -96, north: 37 };
let request;
const { texture } = await acquireWeatherImage('radar', 't', {
signal: new AbortController().signal,
size: { width: 4096, height: 2048 },
bbox,
createCanvas: canvas,
fetchImpl: () => assert.fail('no fetch'),
frameSource: {
getImage: async (value) => {
request = value;
return data;
},
},
});
assert.equal(request.bbox, bbox);
assert.equal(
texture.pixels,
data.data,
'ImageData is put as it is; radar has no transfer',
);
assert.equal(texture.image, undefined, 'ImageData is not drawn as an image');
assert.equal(texture.width, 4096);
});
test('a supplied frame of the wrong size is refused and still released', async () => {
const wrong = pixels(1024, 512);
await assert.rejects(
acquireInfraredMosaic('t', {
signal: new AbortController().signal,
mode: 'filtered',
createCanvas: canvas,
frameSource: { getImage: async () => wrong },
}),
/Invalid weather image dimensions/,
);
assert.equal(wrong.closes, 1);
await assert.rejects(
acquireInfraredMosaic('t', {
signal: new AbortController().signal,
mode: 'filtered',
createCanvas: canvas,
frameSource: { getImage: async () => undefined },
}),
/Invalid weather image dimensions/,
);
});
test('cancellation reaches the frame source, and a frame that arrives after it is released', async () => {
const controller = new AbortController();
const late = pixels(2048, 1024);
let seen;
const pending = acquireInfraredMosaic('t', {
signal: controller.signal,
mode: 'filtered',
createCanvas: () => assert.fail('a cancelled frame is not drawn'),
frameSource: {
getImage: async ({ signal }) => {
seen = signal;
await flushMicrotasks();
return late;
},
},
});
controller.abort();
await assert.rejects(pending, { name: 'AbortError' });
assert.equal(seen, controller.signal);
assert.equal(late.closes, 1);
const aborted = new AbortController();
aborted.abort();
await assert.rejects(
acquireInfraredMosaic('t', {
signal: aborted.signal,
frameSource: { getImage: () => assert.fail('already cancelled') },
}),
{ name: 'AbortError' },
);
});
async function flushMicrotasks() {
for (let i = 0; i < 5; i++) await Promise.resolve();
}