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
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/2483-each-og-card-design-renders-its-png-cold → main 1 commit fork
1 file
changed +141
−2
test/og-png.test.ts +141 −2
| @@ -11,9 +11,9 @@ import { recordLook } from "../src/core/looks.ts"; | ||
| 11 | 11 | import { createUser } from "../src/core/users.ts"; |
| 12 | 12 | import { encodePng } from "../src/design/image/png.ts"; |
| 13 | 13 | 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"; | |
| 15 | 15 | 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"; | |
| 17 | 17 | |
| 18 | 18 | const PNG = [0x89, 0x50, 0x4e, 0x47]; |
| 19 | 19 | |
| @@ -301,3 +301,142 @@ describe("og cards with a look", () => { | ||
| 301 | 301 | expect(images(await text("/og/app/peek-app.svg"))).toBe(0); |
| 302 | 302 | }); |
| 303 | 303 | }); |
| 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 | }); |
new above
offline