aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-09 01:25:04 -0700
committerAdam Soltys <[email protected]>2026-10-09 01:25:04 -0700
commitff1a8a72c398c77fbac005f1fde25ad8f43503ef (patch)
tree43b8f1d87239193667d59ac6071c488065f44cb2 /tools
parent817bcc3ba7b2aaad388bc5b2e5cda5ccf7b27766 (diff)
downloadcoinosv3-ff1a8a72c398c77fbac005f1fde25ad8f43503ef.tar.gz
coinosv3-ff1a8a72c398c77fbac005f1fde25ad8f43503ef.zip
Feed images: the column copy and the grid tile are decoded, resized and WebP-encoded in a worker, one picture at a time, instead of on the page thread between scroll frames (a profile of the live feed showed 1.8 s of convertToBlob there, in tasks up to 383 ms). A picture that hangs or crashes the worker is given up and the worker restarts for the next; three in a row and originals paint for the session, as they do wherever workers can't run
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'tools')
-rw-r--r--tools/image-thumbnails-test.js55
1 files changed, 55 insertions, 0 deletions
diff --git a/tools/image-thumbnails-test.js b/tools/image-thumbnails-test.js
new file mode 100644
index 0000000..ede7e11
--- /dev/null
+++ b/tools/image-thumbnails-test.js
@@ -0,0 +1,55 @@
+// Real minified blob worker: both thumbnail sizes, animated originals,
+// bounded concurrency and graceful fallback, with no page-thread encoding.
+import assert from 'node:assert/strict';
+import puppeteer from './puppeteer.js';
+const bundle = await Bun.build({ entrypoints: ['src/image-thumbnails.js'], target: 'browser', minify: true });
+assert(bundle.success);
+const js = await bundle.outputs[0].text();
+const server = Bun.serve({ port: 0, fetch: r => new URL(r.url).pathname === '/worker-client.js'
+ ? new Response(js, { headers: { 'Content-Type': 'text/javascript' } })
+ : new Response('<script type="module">import {makeImageThumbnails} from "/worker-client.js";window.thumbnails=makeImageThumbnails;</script>', { headers: { 'Content-Type': 'text/html' } }) });
+const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] });
+try {
+ const page = await browser.newPage();
+ await page.goto(server.url.href); await page.waitForFunction(() => window.thumbnails);
+ const result = await page.evaluate(async () => {
+ const canvas = new OffscreenCanvas(3000,2000), ctx=canvas.getContext('2d');
+ ctx.fillStyle='red';ctx.fillRect(0,0,3000,2000);ctx.fillStyle='blue';ctx.fillRect(500,0,2000,2000);
+ const blob=await canvas.convertToBlob({type:'image/png'});
+ const decode = createImageBitmap, encode = OffscreenCanvas.prototype.convertToBlob;
+ let mainCalls=0, pending=0, maxPending=0;
+ const RealWorker = Worker;
+ window.Worker = class extends RealWorker {
+ constructor(...args) { super(...args); this.addEventListener('message',()=>pending--); }
+ postMessage(...args) { pending++;maxPending=Math.max(maxPending,pending);super.postMessage(...args); }
+ };
+ window.createImageBitmap = () => {mainCalls++;throw Error('main-thread decode');};
+ OffscreenCanvas.prototype.convertToBlob = () => {mainCalls++;throw Error('main-thread encode');};
+ const [big, tiles, bad, recovered] = await Promise.all([
+ thumbnails(blob,{width:1100,tileSize:360}), thumbnails(blob,{tileSize:360}),
+ thumbnails(new Blob(['broken'],{type:'image/jpeg'}),{width:1100}), thumbnails(blob,{width:1100}),
+ ]);
+ const media = await decode(big.media), tile = await decode(big.tile);
+ const square = new OffscreenCanvas(360,360);square.getContext('2d').drawImage(tile,0,0);
+ const pixel=[...square.getContext('2d').getImageData(0,180,1,1).data];
+ const smallCanvas=new OffscreenCanvas(32,20);smallCanvas.getContext('2d');
+ const smallBlob=await encode.call(smallCanvas,{type:'image/png'});
+ const small=await thumbnails(smallBlob,{width:1100,tileSize:360});
+ const smallTile=await decode(small.tile);
+ const gif=new Blob([Uint8Array.from(atob('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'),c=>c.charCodeAt(0))],{type:'image/gif'});
+ const animated=await thumbnails(gif,{width:1100,tileSize:360});
+ const out={mainCalls,maxPending,big:[big.width,big.height],media:[media.width,media.height],tile:[tile.width,tile.height],pixel,
+ tileOnly:!tiles.media&&!!tiles.tile,bad,recovered:!!recovered.media,smallOriginal:small.media===null,smallTile:[smallTile.width,smallTile.height],
+ animated: animated && [animated.width,animated.height,animated.media,animated.tile]};
+ media.close();tile.close();smallTile.close();return out;
+ });
+ console.log(result);
+ assert.equal(result.mainCalls,0);assert.equal(result.maxPending,1);
+ assert.deepEqual(result.big,[3000,2000]);assert.deepEqual(result.media,[1100,733]);assert.deepEqual(result.tile,[360,360]);
+ assert(result.pixel[2]>200&&result.pixel[0]<30,'tile crops the center');
+ assert(result.tileOnly&&result.recovered&&result.smallOriginal);assert.equal(result.bad,null);
+ assert.deepEqual(result.smallTile,[20,20]);assert.deepEqual(result.animated,[1,1,null,null]);
+ const fallback=await browser.newPage();await fallback.goto(server.url.href);await fallback.waitForFunction(()=>window.thumbnails);
+ assert.equal(await fallback.evaluate(async()=>{window.Worker=class {constructor(){throw Error('blocked');}};return thumbnails(new Blob(['x']),{width:1100});}),null);
+ console.log('✓ thumbnails stay off the page thread; sizes, crop, originals, queue, bad-image recovery and worker fallback pass');
+} finally { await browser.close();server.stop(true); }