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

The map's share card resolves under 100 ms: /og/map/trevato~forge.png and its SVG draw what is open #6160

Merged opened this

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

Merged

scout/forge:scout/6150-the-map-s-share-card-resolves-under-100 → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/6150-the-map-s-share-card-resolves-under-100 was merged into main as e289020.

Review

Sign in to review this change.
3 files changed +72 −7
src/web/og/map.ts +7 −1
@@ -109,8 +109,14 @@ export function mapInput(title: string, graph: Graph, marks: Marks): CardInput {
109109 const readySet = new Set(ready.map((r) => r.issue.id));
110110 const words = new Map(graph.issues.map((i) => [i.id, wordFor(i, readySet, marks)]));
111111 const open = graph.issues.filter((i) => i.state === "open").length;
112 // The thumbnail draws what is open: done issues are counted in the caption, not drawn.
113 const live = new Set(graph.issues.filter((i) => i.state === "open").map((i) => i.id));
112114 const figure = graphFigure(
113 graph,
115 {
116 ...graph,
117 issues: graph.issues.filter((i) => live.has(i.id)),
118 edges: graph.edges.filter((e) => live.has(e.issueId) && live.has(e.blockedById)),
119 },
114120 words,
115121 path.map((i) => i.id),
116122 );
test/og-designs.test.ts +6 −6
@@ -93,18 +93,18 @@ describe("the map's thumbnail", () => {
9393 expect(card.lines).toEqual(["5 open issues", "Critical path: 3 issues, #1 → #5", "Next: #6 Issue 6"]);
9494 const fig = card.figure;
9595 if (fig?.kind !== "graph") throw new Error("no graph");
96 // Only the chains are drawn: 6, on no edge, is counted, not drawn.
97 expect(fig.nodes.length).toBe(5);
96 // Only the open chains are drawn: 6, on no edge, and 2, done, are counted, not drawn.
97 expect(fig.nodes.length).toBe(4);
9898 expect(fig.levels).toBe(3);
9999 expect(fig.nodes.filter((n) => n.critical).length).toBe(3);
100100 expect(fig.edges.filter((e) => e.critical).length).toBe(2);
101 expect(fig.edges.filter((e) => e.done).length).toBe(1);
102 expect(fig.nodes.map((n) => n.word).toSorted()).toEqual(["blocked", "blocked", "done", "waiting", "working"]);
101 expect(fig.edges.filter((e) => e.done).length).toBe(0);
102 expect(fig.nodes.map((n) => n.word).toSorted()).toEqual(["blocked", "blocked", "waiting", "working"]);
103103 const svg = renderCard(card);
104104 wellFormed(svg);
105 // Five cards in the figure, the two critical edges in the accent.
105 // Four cards in the figure, the two critical edges in the accent.
106106 const figure = svg.slice(svg.indexOf("<g>"), svg.indexOf("</g>"));
107 expect(figure.match(/<rect /g)?.length).toBe(5);
107 expect(figure.match(/<rect /g)?.length).toBe(4);
108108 expect(figure.match(new RegExp(`<path [^>]*stroke="${T.accent}"`, "g"))?.length).toBe(2);
109109 expect(svg).toContain(">MAP<");
110110 });
test/og-map.test.ts added +59
@@ -0,0 +1,59 @@
1 /** The map's share card on 2,000 closed and 300 open issues stays small and quick (#6150). */
2 import { afterAll, beforeAll, expect, setDefaultTimeout, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp, type App } from "../src/app.ts";
7 import { createRepo } from "../src/actions/repos.ts";
8 import { addDependency } from "../src/core/dependencies.ts";
9 import { createIssue } from "../src/core/issues.ts";
10 import { serve, type Servers } from "../src/http/server.ts";
11 import { seedAdmin } from "./helpers/admin.ts";
12
13 setDefaultTimeout(60_000);
14
15 let dataDir = "";
16 let app: App;
17 let servers: Servers;
18
19 beforeAll(async () => {
20 dataDir = await mkdtemp(join(tmpdir(), "forge-ogmap-"));
21 app = createApp({ dataDir, port: 0 });
22 servers = await serve(app);
23 const admin = await seedAdmin(app, { username: "alice", email: "[email protected]", password: "correct-horse-battery" });
24 if (!admin.ok) throw new Error(admin.error);
25 const made = await createRepo(app, admin.value, { name: "notes", visibility: "public", description: "" });
26 if (!made.ok) throw new Error(String(made.error));
27 const repoId = made.value.id;
28 const issues = Array.from({ length: 2300 }, (_, i) => {
29 const r = createIssue(app.db, { repoId, authorId: admin.value.id, title: `Issue ${i}`, body: "" });
30 if (!r.ok) throw new Error(r.error);
31 return r.value;
32 });
33 app.db.transaction(() => {
34 // The first 2,000 are chained and closed; the last 300 are chained in tens and open.
35 for (let i = 1; i < 2000; i++) addDependency(app.db, issues[i]!, issues[i - 1]!, admin.value.id);
36 for (let i = 2000; i < 2300; i++)
37 if (i % 10 !== 0) addDependency(app.db, issues[i]!, issues[i - 1]!, admin.value.id);
38 for (const w of issues.slice(0, 2000))
39 app.db.run("UPDATE issue SET state = 'closed' WHERE id = ?1", [w.id]);
40 })();
41 });
42
43 afterAll(async () => {
44 await servers.stop();
45 await rm(dataDir, { recursive: true, force: true });
46 });
47
48 test("the card draws open issues only, names the done count, and resolves fast", async () => {
49 await fetch(`${servers.url}/og/map/alice~notes.svg`).then((r) => r.arrayBuffer()); // warm
50 const res = await fetch(`${servers.url}/og/map/alice~notes.svg`);
51 const svg = await res.text();
52 expect(res.status).toBe(200);
53 expect(new TextEncoder().encode(svg).length).toBeLessThan(200_000);
54 expect(svg).toContain("2000 done");
55 expect(svg).toContain("300 open issues");
56 const resolve = /resolve;dur=([\d.]+)/.exec(res.headers.get("server-timing") ?? "");
57 expect(resolve).not.toBeNull();

Wall-clock bound on shared CI can flake. Consider asserting on node count or SVG size, which are deterministic, plus a generous time bound. Add the .png route too.

58 expect(Number(resolve![1])).toBeLessThan(100);
59 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs