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

GET /changelog answers under 50 ms with 500 entries, timed in a test #10733

Merged opened this

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

Merged

scout/forge:scout/10724-get-changelog-answers-under-50-ms-with-5 → main 2 commits fork

  • Merged. scout/10724-get-changelog-answers-under-50-ms-with-5 was merged into main as bd659b4.

Review

Sign in to review this change.
2 files changed +103 −1
src/web/pages/changelog.ts +26 −1
@@ -108,7 +108,14 @@ export const localDaysJs = /* js */ `
108108 })();
109109 `;
110110
111 /** The region: the top line, what is on its way, and what went live by day. */
111 /** Rendered regions kept by what they were rendered from; a few views (repo filters) are in use at once. */
112 const REGIONS = 8;
113 const rendered = new Map<string, Html>();
114
115 /**
116 * The region, kept per input: the same landings, view, day and minute (rows
117 * speak in "5m ago") render the same markup, and 500 rows cost ~50 ms to build.
118 */
112119 export function shippedRegion(
113120 read: Landings,
114121 choice: string,
@@ -116,6 +123,24 @@ export function shippedRegion(
116123 zone = "UTC",
117124 toLive?: { version: string; seconds: number },
118125 readable = true,
126 ): Html {
127 const key = `${Bun.hash(JSON.stringify([read, choice, zone, toLive ?? null, readable])).toString(36)}:${Math.floor(now.getTime() / 60_000)}`;
128 const kept = rendered.get(key);
129 if (kept !== undefined) return kept;
130 const made = renderRegion(read, choice, now, zone, toLive, readable);
131 if (rendered.size >= REGIONS) rendered.delete(rendered.keys().next().value as string);
132 rendered.set(key, made);
133 return made;
134 }
135
136 /** The region: the top line, what is on its way, and what went live by day. */
137 function renderRegion(
138 read: Landings,
139 choice: string,
140 now: Date = new Date(),
141 zone = "UTC",
142 toLive?: { version: string; seconds: number },
143 readable = true,
119144 ): Html {
120145 const keep = (l: Landing) => choice === "all" || l.repo === choice;
121146 const live = read.live.filter(keep);
test/changelog-timing.test.ts added +77
@@ -0,0 +1,77 @@
1 /** GET /changelog stays instant as history grows: 500 release entries, median of warm GETs under budget (#10724). */
2 import { afterAll, beforeAll, expect, 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 { createUser } from "../src/core/users.ts";
9 import { forgetLive, keepLive } from "../src/release/landings.ts";
10 import { serve, type Servers } from "../src/http/server.ts";
11
12 /** The page's budget in milliseconds, as the palette's timing test states its own. */
13 const BUDGET_MS = 50;
14 const ENTRIES = 500;
15 const RUNS = 3;
16
17 let dataDir: string;
18 let app: App;
19 let servers: Servers;
20 const saved = process.env["FORGE_COMMIT"];
21
22 beforeAll(async () => {
23 process.env["FORGE_COMMIT"] = "a".repeat(40);
24 dataDir = await mkdtemp(join(tmpdir(), "forge-changelog-timing-"));
25 app = createApp({ dataDir, port: 0, forgeRepo: "alice/forge" });
26 servers = await serve(app);
27 const u = await createUser(app.db, {
28 origin: "seed", trusted: true, username: "alice", email: "[email protected]", password: "a-good-passphrase", adminIfFirst: true,
29 });
30 if (!u.ok) throw new Error(u.error);
31 const made = await createRepo(app, u.value, { name: "forge", visibility: "public" });
32 if (!made.ok) throw new Error(String(made.error));
33 const insertIssue = app.db.query(
34 `INSERT INTO issue (id, repo_id, number, kind, title, author_id, state) VALUES (?1, ?2, ?3, 'pull', ?4, ?5, 'closed')`,
35 );
36 const insertPull = app.db.query(
37 `INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid, merged_at, merged_oid) VALUES (?1, ?2, 'b', 'base', '', ?3, ?4)`,
38 );
39 const oids: string[] = [];
40 app.db.transaction(() => {
41 for (let n = 1; n <= ENTRIES; n++) {
42 insertIssue.run(`i${n}`, made.value.id, n, `Release entry ${n}`, u.value.id);
43 const at = new Date(Date.UTC(2026, 0, 1) + n * 3_600_000).toISOString();
44 insertPull.run(`i${n}`, made.value.defaultBranch, at, `oid-${n}`);
45 oids.push(`oid-${n}`);
46 }
47 })();
48 keepLive("alice/forge", oids, 0);
49 }, 60_000);
50
51 afterAll(async () => {
52 forgetLive();
53 if (saved === undefined) delete process.env["FORGE_COMMIT"];
54 else process.env["FORGE_COMMIT"] = saved;
55 servers.stop();
56 app.close();
57 await rm(dataDir, { recursive: true, force: true });
58 });
59
60 const median = (xs: number[]) => xs.slice().sort((a, b) => a - b)[Math.floor(xs.length / 2)]!;
61
62 test(`${ENTRIES} entries: GET /changelog answers in under ${BUDGET_MS} ms (median of ${RUNS} warm runs)`, async () => {
63 const warm = await fetch(`${servers.url}/changelog`);
64 expect(warm.status).toBe(200);
65 expect(await warm.text()).toContain("Release entry");
66 const runs: number[] = [];
67 for (let i = 0; i < RUNS; i++) {
68 const t = performance.now();
69 const res = await fetch(`${servers.url}/changelog`);
70 await res.arrayBuffer();
71 runs.push(performance.now() - t);
72 expect(res.status).toBe(200);
73 }
74 const m = median(runs);
75 console.log(`changelog median ${m.toFixed(1)} ms over ${ENTRIES} entries (${runs.map((r) => r.toFixed(1)).join(", ")})`);
76 expect(m).toBeLessThan(BUDGET_MS);
77 }, 60_000);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs