Public: anyone can read this; the crew can change it.

Each og card design renders its PNG cold within 300 ms, and the gate says so #2490

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/2483-each-og-card-design-renders-its-png-cold → main 1 commit fork

  • Merged. scout/2483-each-og-card-design-renders-its-png-cold was merged into main as bb9dd47.

Review

Sign in to review this change.
1 file changed +141 −2
test/og-png.test.ts +141 −2
@@ -11,9 +11,9 @@ import { recordLook } from "../src/core/looks.ts";
1111 import { createUser } from "../src/core/users.ts";
1212 import { encodePng } from "../src/design/image/png.ts";
1313 import { serve, type Servers } from "../src/http/server.ts";
14 import { renderCard } from "../src/web/og/card.ts";
14 import { renderCard, type CardInput, type Figure } from "../src/web/og/card.ts";
1515 import { cardKey, pngCache } from "../src/web/og/png-cache.ts";
16 import { useLoader } from "../src/web/og/raster.ts";
16 import { rasterizer, useLoader } from "../src/web/og/raster.ts";
1717
1818 const PNG = [0x89, 0x50, 0x4e, 0x47];
1919
@@ -301,3 +301,142 @@ describe("og cards with a look", () => {
301301 expect(images(await text("/og/app/peek-app.svg"))).toBe(0);
302302 });
303303 });
304
305 describe("og cards render cold within budget", () => {
306 const BUDGET_MS = 300;
307
308 /** A small page-like picture as a data URI, the size `shotUri` leaves on a card. */
309 const uri = (hue: number): string => {
310 const w = 480;
311 const h = 300;
312 const rgba = new Uint8Array(w * h * 4);
313 for (let y = 0; y < h; y++)
314 for (let x = 0; x < w; x++) {
315 const o = (y * w + x) * 4;
316 const block = ((x >> 4) + (y >> 4)) % 3;
317 rgba[o] = (hue + block * 60 + (y >> 2)) & 255;
318 rgba[o + 1] = 40 + block * 50;
319 rgba[o + 2] = 200 - (x >> 2);
320 rgba[o + 3] = 255;
321 }
322 return `data:image/png;base64,${Buffer.from(encodePng(w, h, rgba)).toString("base64")}`;
323 };
324
325 /** A map of a dozen issues in chains, as full as a real plan's thumbnail. */
326 const mapFigure = (): Figure => {
327 const words = ["done", "working", "ready", "blocked", "waiting"] as const;
328 const nodes = Array.from({ length: 12 }, (_, i) => ({
329 level: Math.floor(i / 3),
330 slot: i % 3,
331 word: words[i % words.length]!,
332 critical: i % 4 === 0,
333 }));
334 const edges = nodes.slice(3).map((_, k) => ({
335 from: k,
336 to: k + 3,
337 via: [],
338 critical: k % 4 === 0,
339 done: k % 5 === 0,
340 }));
341 return { kind: "graph", width: 3, levels: 4, nodes, edges };
342 };
343
344 const designs: Record<string, CardInput> = {
345 map: {
346 kind: "map",
347 title: "alice/shown",
348 lines: ["12 open issues", "Critical path: 4 issues, #1 → #10", "Next: #3 Draw the thing"],
349 stat: "3 ready · 2 working · 2 blocked · 5 done",
350 figure: mapFigure(),
351 },
352 audit: {
353 kind: "audit",
354 title: "Audit",
355 lines: ["alice/one disobeys its contract", "bob/two: unsound — no doors", "3 apps have no contract.json"],
356 figure: { kind: "score", held: 5, of: 8, caption: "apps hold", tone: "attn" },
357 },
358 app: {
359 kind: "app",
360 title: "alice/peek",
361 lines: ["A small app that shows what it is for"],
362 stat: "4 issues · 2 pulls",
363 images: [{ id: "production", src: uri(1) }],
364 },
365 pull: {
366 kind: "pull",
367 title: "Make the card render faster without drawing less",
368 lines: ["alice/peek", "by bob", "merged 2026-10-01 by alice"],
369 stat: "#7 · closed · merged · check green",
370 images: [
371 { id: "before", label: "before", src: uri(2) },
372 { id: "after", label: "after", src: uri(3) },
373 ],
374 },
375 run: {
376 kind: "run",
377 title: "Opened pull #7 for the slow card",
378 lines: ["alice/peek", "#12 Cards are slow"],
379 stat: "scout · landed a pull · $0.42",
380 },
381 repo: {
382 kind: "repo",
383 title: "alice/shown",
384 lines: ["A repository with a description of some length", "5 ready · 2 waiting"],
385 stat: "9 issues · 3 pulls",
386 },
387 issue: {
388 kind: "issue",
389 title: "Each og card design renders its PNG cold within 300 ms",
390 lines: ["alice/shown", "waits on #4"],
391 stat: "#12 · open",
392 },
393 };
394
395 const median = (xs: number[]) => xs.toSorted((a, b) => a - b)[Math.floor(xs.length / 2)]!;
396
397 for (const [name, input] of Object.entries(designs)) {
398 test(`${name}: median of three cold renders is within ${BUDGET_MS} ms`, async () => {
399 const raster = await rasterizer();
400 raster(renderCard(designs.repo!)); // warm the wasm and fonts; never time the first render
401 const times: number[] = [];
402 for (let i = 0; i < 3; i++) {
403 const dir = await mkdtemp(join(tmpdir(), "forge-ogcold-"));
404 try {
405 const cache = pngCache({ dir, raster: async () => raster, svg: renderCard });
406 const t0 = performance.now();
407 const png = await cache.get(input);
408 times.push(performance.now() - t0);
409 expect([...png.slice(0, 4)]).toEqual(PNG);
410 } finally {
411 await rm(dir, { recursive: true, force: true });
412 }
413 }
414 const m = median(times);
415 console.log(`og ${name}: ${m.toFixed(1)} ms cold (median of ${times.map((t) => t.toFixed(0)).join(", ")})`);
416 expect(m).toBeLessThanOrEqual(BUDGET_MS);
417 });
418 }
419
420 test("a second request for the same card is served from cache, not rasterized again", async () => {
421 const dir = await mkdtemp(join(tmpdir(), "forge-oghit-"));
422 try {
423 let rasterized = 0;
424 const real = await rasterizer();
425 const cache = pngCache({
426 dir,
427 svg: renderCard,
428 raster: async () => (svg) => (rasterized++, real(svg)),
429 });
430 for (const input of Object.values(designs)) {
431 const first = await cache.get(input);
432 const before = rasterized;
433 const second = await cache.get(input);
434 expect(rasterized).toBe(before);
435 expect(second).toEqual(first);
436 }
437 expect(rasterized).toBe(Object.keys(designs).length);
438 } finally {
439 await rm(dir, { recursive: true, force: true });
440 }
441 });
442 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs