Public: anyone can read this; the crew can change it.
bench/results records the flows view at 200 lines: warm p50 and p95 against the 50 ms budget #9673
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/9670-bench-results-records-the-flows-view-at → main 1 commit fork 1 thread · 1 unresolved
2 files
changed +70
−0
bench/flows-200.ts added +54
| @@ -0,0 +1,54 @@ | ||
| 1 | /** | |
| 2 | * `/:owner/:name/flows` at 200 lines (#9670), warm, as the admin: p50 and p95 | |
| 3 | * of the whole request and of every Server-Timing phase, as a markdown table. | |
| 4 | * 100 followed sources flow in, 100 forks flow out. The first request is | |
| 5 | * thrown away (it warms the page's caches); the next `n` are measured. | |
| 6 | * | |
| 7 | * bun run bench/flows-200.ts [n] # n defaults to 200 | |
| 8 | */ | |
| 9 | import { createRepo } from "../src/actions/repos.ts"; | |
| 10 | import { setForkFollowMode } from "../src/core/fork_follow.ts"; | |
| 11 | import { snappyFixture } from "./snappy.ts"; | |
| 12 | ||
| 13 | const n = Number(process.argv[2]) > 0 ? Number(process.argv[2]) : 200; | |
| 14 | const LINES = 200; | |
| 15 | const f = await snappyFixture({ repos: 1, issuesPerRepo: 0 }); | |
| 16 | try { | |
| 17 | const repo = f.repos[0]!; | |
| 18 | const ins = f.app.db.query( | |
| 19 | "INSERT INTO repo_source (id, repo_id, url, branch, by_id, created_at, checked_at, behind) VALUES (?1, ?2, ?3, 'main', ?4, ?5, ?6, ?7)", | |
| 20 | ); | |
| 21 | for (let i = 0; i < LINES / 2; i++) | |
| 22 | ins.run(`s${i}`, repo.id, `https://src${i}.example/x/y${i}.git`, f.owner.id, `2026-11-01T00:00:${String(i % 60).padStart(2, "0")}Z`, "2026-11-07T00:00:00Z", i % 4); | |
| 23 | for (let i = 0; i < LINES / 2; i++) { | |
| 24 | const kid = await createRepo(f.app, f.owner, { name: `fork-${i}`, visibility: "public", description: "" }); | |
| 25 | if (!kid.ok) throw new Error(JSON.stringify(kid.error)); | |
| 26 | f.app.db.query("UPDATE repo SET fork_parent_id = ?1 WHERE id = ?2").run(repo.id, kid.value.id); | |
| 27 | if (i % 2 === 0) setForkFollowMode(f.app.db, kid.value.id, "take-updates"); | |
| 28 | } | |
| 29 | const url = `${f.base}/${f.owner.username}/${repo.name}/flows`; | |
| 30 | const total: number[] = []; | |
| 31 | const seen = new Map<string, number[]>(); | |
| 32 | let rows = 0; | |
| 33 | for (let i = 0; i < n + 1; i++) { | |
| 34 | const t = performance.now(); | |
| 35 | const res = await fetch(url, { headers: { cookie: f.ownerCookie } }); | |
| 36 | const body = await res.text(); | |
| 37 | const ms = performance.now() - t; | |
| 38 | if (res.status !== 200) throw new Error(`status ${res.status}`); | |
| 39 | if (i === 0) { rows = new Set(body.match(/src\d+\.example|olive\/fork-\d+/g)).size; continue; } | |
| 40 | total.push(ms); | |
| 41 | for (const m of (res.headers.get("server-timing") ?? "").matchAll(/([a-z-]+);dur=([\d.]+)/g)) | |
| 42 | (seen.get(m[1]!) ?? seen.set(m[1]!, []).get(m[1]!)!).push(Number(m[2])); | |
| 43 | } | |
| 44 | const at = (xs: number[], p: number) => { | |
| 45 | const s = [...xs].sort((a, b) => a - b); | |
| 46 | return s[Math.min(s.length - 1, Math.floor((s.length * p) / 100))]!; | |
| 47 | }; | |
| 48 | console.log(`flow lines on the page: ${rows}, requests: ${n}`); | |
| 49 | console.log("| measure | p50 ms | p95 ms |\n| ------- | -----: | -----: |"); | |
| 50 | console.log(`| whole request (client) | ${at(total, 50).toFixed(1)} | ${at(total, 95).toFixed(1)} |`); | |
| 51 | for (const [name, xs] of seen) console.log(`| Server-Timing \`${name}\` | ${at(xs, 50).toFixed(1)} | ${at(xs, 95).toFixed(1)} |`); | |
| 52 | } finally { | |
| 53 | await f.close(); | |
| 54 | } |
bench/results/flows-200.md added +16
| @@ -0,0 +1,16 @@ | ||
| 1 | # `/:owner/:name/flows` at 200 lines, warm, as admin (#9670) | |
| 2 | ||
| 3 | Measured with `bun run bench/flows-200.ts` on main at b4137c8c, in the sandbox: one repository, 100 followed sources flowing in and 100 forks (half taking updates) flowing out — 200 lines on the page, counted from the response. One discarded warm-up request, then 200 requests as the admin over loopback. Budget: 50 ms. | |
| 4 | ||
| 5 | | measure | p50 ms | p95 ms | | |
| 6 | | ------- | -----: | -----: | | |
| 7 | | whole request (client) | 19.6 | 25.8 | | |
| 8 | | Server-Timing `app` | 14.9 | 20.1 | | |
| 9 | | Server-Timing `session` | 0.1 | 0.2 | | |
| 10 | | Server-Timing `render` | 12.7 | 17.4 | | |
| 11 | ||
| 12 | **Verdict: within budget.** p95 is 26 ms against 50 ms for the whole request. The slowest phase is `render` (~13 ms p50, ~17 ms p95 — it includes `readFlows` and the radar read, then building ~200 rows); `session` is 0.1 ms. The rest of `app` is routing and the security floor. The client time includes the loopback fetch and reading the ~body, which is why it is a few ms above `app`. | |
| 13 | ||
| 14 | Nothing in src/ changed; every phase the page spends time in (`session`, `render`, `app`) is already named in Server-Timing. #9658's test still bounds the view's SQL statements. | |
| 15 | ||
| 16 | Not measured: a cold first request, a non-admin viewer (fewer actions per row, so lighter), and more than 200 lines. If the page ever needs to go faster, the idea (not built) is to cap each side to a first screenful with a "show all" link, since `render` grows with rows. |
Hardcoded
oliveowner; use f.owner.username, and throw if rows !== LINES so the 200-line claim is enforced rather than printed.