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

Observability 4: /boards, /map and home answer fast - boards 8-10 s and /map 34.7 s cold today #3710

Merged opened this in answer to

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

Merged

torque/forge:torque/3693-observability-4-boards-map-and-home-answ → main 11 commits fork 5 threads · 5 unresolved

  • Merged. torque/3693-observability-4-boards-map-and-home-answ was merged into main as e4c4084.

Review

Sign in to review this change.
11 files changed +528 −29
.gitignore +4
@@ -41,5 +41,9 @@ typesafe-api-key
4141 # when the default temp directory is read-only (#2706)
4242 /.bun-tmp/
4343
44 # a suite whose data would crowd a check cell's 256 MiB /tmp keeps it
45 # here, on the checkout's disk (test/helpers/scratch.ts)
46 /.test-scratch/
47
4448 # a sandbox's bun/bunx scratch directory; never source
4549 /.tmpx/
src/core/migrations/20261022120000-flow-indexes.ts added +21
@@ -0,0 +1,21 @@
1 // The flow readers' indexes (#3693). `/boards`, `/system` and home read the
2 // pipeline and its throughput on every view; without these, each merged
3 // pull's "first production deploy since" walked every run of its repository,
4 // and each stage's outflow scanned the whole run table, and so did
5 // finding the pulls a bring-up-to-date run is working on; a ready ask's
6 // "no open pull from its crew branch" scanned every pull. Home's figures
7 // summed today's crew spend over every run and counted each day's visitors
8 // from the page views themselves. Additive.
9 export default `
10 CREATE INDEX run_deploy_since ON run(repo_id, env, created_at) WHERE kind = 'deploy';
11 CREATE INDEX run_kind_started ON run(kind, started_at) WHERE started_at IS NOT NULL;
12 CREATE INDEX run_kind_finished ON run(kind, finished_at) WHERE finished_at IS NOT NULL;
13 CREATE INDEX run_serving ON run(serving_at) WHERE serving_at IS NOT NULL;
14 CREATE INDEX run_conflict ON run(issue_id, created_at) WHERE kind = 'agent' AND reason = 'conflict';
15 CREATE INDEX run_agent_created ON run(created_at) WHERE kind = 'agent';
16 CREATE INDEX pull_head_branch ON pull(head_branch);
17 CREATE INDEX pull_merged ON pull(merged_at) WHERE merged_at IS NOT NULL;
18 CREATE INDEX page_vital_day_visitor ON page_vital(day, visitor);
19 CREATE INDEX page_vital_day_vitals ON page_vital(day, load, inp);
20 CREATE INDEX session_seen ON session(last_seen_at, user_id);
21 `;
src/flow/pipeline.ts +4 −2
@@ -99,6 +99,8 @@ function waitingTally(db: Database, sql: string, ...args: string[]): Tally {
9999 return { waiting: r?.n ?? 0, active: 0, oldest: r?.oldest ?? null };
100100 }
101101
102 // CROSS JOIN keeps SQLite's order: the ask's runs, their branch's pulls by
103 // `pull_head_branch`, then each pull's issue, never every open issue (#3693).
102104 const READY = `
103105 SELECT i.created_at AS since
104106 FROM issue i JOIN repo r ON r.id = i.repo_id
@@ -108,8 +110,8 @@ const READY = `
108110 WHERE d.issue_id = i.id AND b.state = 'open')
109111 AND NOT EXISTS (SELECT 1 FROM run x WHERE x.issue_id = i.id AND x.kind = 'agent'
110112 AND x.state IN ('queued','running'))
111 AND NOT EXISTS (SELECT 1 FROM run x JOIN pull p ON p.head_branch = x.branch
112 JOIN issue pi ON pi.id = p.issue_id AND pi.repo_id = x.repo_id
113 AND NOT EXISTS (SELECT 1 FROM run x CROSS JOIN pull p ON p.head_branch = x.branch
114 CROSS JOIN issue pi ON pi.id = p.issue_id AND pi.repo_id = x.repo_id
113115 WHERE x.issue_id = i.id AND x.kind = 'agent' AND x.branch != ''
114116 AND pi.state = 'open')`;
115117
src/flow/reach.ts +27 −16
@@ -29,20 +29,30 @@ export interface Reach {
2929
3030 export const NO_REACH: Reach = { usersToday: 0, usersTotal: 0, visitorsToday: 0, visitors7d: 0, visitorDays: new Array<number>(DAYS).fill(0), loadP75: null, inpP75: null };
3131
32 /** The 75th percentile, nearest rank, of a column over the days since `fromDay`. */
33 function p75(db: Database, column: "load" | "inp", fromDay: string): number | null {
34 const n = db
35 .query<{ n: number }, [string]>(`SELECT COUNT(${column}) AS n FROM page_vital WHERE day >= ?1`)
36 .get(fromDay)?.n ?? 0;
37 if (n === 0) return null;
38 const rank = Math.ceil(n * 0.75);
39 return (
40 db
41 .query<{ v: number }, [string, number]>(
42 `SELECT ${column} AS v FROM page_vital WHERE day >= ?1 AND ${column} IS NOT NULL ORDER BY ${column} LIMIT 1 OFFSET ?2`,
43 )
44 .get(fromDay, rank - 1)?.v ?? null
45 );
32 /** The 75th percentile, nearest rank: the value at rank ⌈0.75·n⌉ of `values` sorted. */
33 export function p75(values: number[]): number | null {
34 if (values.length === 0) return null;
35 values.sort((a, b) => a - b);
36 return values[Math.ceil(values.length * 0.75) - 1]!;
37 }
38
39 /**
40 * Page load and INP over the days since `fromDay`, read in one pass (#3693):
41 * two counts and two sorted OFFSET reads cost home 70 ms on a month of views.
42 */
43 function vitals(db: Database, fromDay: string): { load: number[]; inp: number[] } {
44 const load: number[] = [];
45 const inp: number[] = [];
46 // Rows as arrays: a week of views is tens of thousands, and objects cost.
47 for (const [l, i] of db
48 .query<[number | null, number | null], [string]>(
49 "SELECT load, inp FROM page_vital WHERE day >= ?1 AND (load IS NOT NULL OR inp IS NOT NULL)",
50 )
51 .values(fromDay) as [number | null, number | null][]) {
52 if (l !== null) load.push(l);
53 if (i !== null) inp.push(i);
54 }
55 return { load, inp };
4656 }
4757
4858 export function readReach(db: Database, now: Date): Reach {
@@ -65,13 +75,14 @@ export function readReach(db: Database, now: Date): Reach {
6575 perDay.get(new Date(now.getTime() - (DAYS - 1 - i) * DAY_MS).toISOString().slice(0, 10)) ?? 0,
6676 );
6777 const days = month.filter((d) => d.day >= weekFrom);
78 const week = vitals(db, weekFrom);
6879 return {
6980 usersToday: users.today,
7081 usersTotal: users.total,
7182 visitorsToday: days.find((d) => d.day === today)?.n ?? 0,
7283 visitorDays,
7384 visitors7d: days.reduce((a, d) => a + d.n, 0),
74 loadP75: p75(db, "load", weekFrom),
75 inpP75: p75(db, "inp", weekFrom),
85 loadP75: p75(week.load),
86 inpP75: p75(week.inp),
7687 };
7788 }
src/web/pages/boards-view.ts +8 −3
@@ -80,7 +80,8 @@ const denied = (ctx: Ctx) => {
8080
8181 // GET /boards
8282 export async function boardsList(ctx: Ctx): Promise<Response> {
83 const listed = await listAllBoards(ctx.app, ctx.auth.user);
83 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
84 const listed = await timePhaseAsync(ctx.request, "boards", () => listAllBoards(ctx.app, ctx.auth.user));
8485 if (!listed.ok) return notFoundPage(ctx);
8586 const make = html`<form method="post" action="/boards/new" class="bform">
8687 <label class="qf"><span>New board</span><input class="input" name="title" required maxlength="${String(MAX_TITLE)}" placeholder="Title" /></label>
@@ -100,13 +101,17 @@ export async function boardsList(ctx: Ctx): Promise<Response> {
100101 </ul>`,
101102 { pad: false },
102103 );
103 return page({
104 const flow = timePhase(ctx.request, "flow", () => flowPanel(ctx.app, { kind: "owner" }, ctx.url, "/boards"));

Nit: render times only the page() call. If the shell cost matters in production, move flow inside the same span or name it shell.

105 const rendering = performance.now();
106 const response = page({
104107 ctx,
105108 title: "Boards",
106109 body: html`<div class="page-head"><h1>Boards</h1>
107110 ${btn("Explore the data", { href: "/boards/data", variant: "ghost" })}</div>
108 ${card(make)} ${body} ${flowPanel(ctx.app, { kind: "owner" }, ctx.url, "/boards")}`,
111 ${card(make)} ${body} ${flow}`,
109112 });
113 notePhase(ctx.request, "render", performance.now() - rendering);
114 return response;
110115 }
111116
112117 function panelForm(slug: string, op: string, p: Panel | undefined, index: number, fields: Html): Html {
src/web/pages/map.ts +47 −7
@@ -129,7 +129,7 @@ export interface Layout {
129129 }
130130
131131 /** One hop of an edge between adjacent levels: the unit `order` sees. */
132 interface Seg {
132 export interface Seg {
133133 readonly from: string;
134134 readonly to: string;
135135 }
@@ -304,15 +304,55 @@ function cross(
304304 return top * bottom < 0;
305305 }
306306
307 function countCrossings(
307 /**
308 * The same count as comparing every pair of hops with `cross`, without
309 * doing so (#3693: 2,266 edges drew 30 s of layout). A hop down one level crosses
310 * another in the same gap exactly when their ends are in opposite order,
311 * so each gap is an inversion count over a Fenwick tree, O(k log k). Hops
312 * that point up or skip levels (only a cycle draws them) keep the pairwise
313 * test against every hop; hops within one level never cross anything.
314 */
315 export function countCrossings(
308316 segs: readonly Seg[],
309317 level: Map<string, number>,
310318 pos: Map<string, number>,
311319 ): number {
320 const gaps = new Map<number, [number, number][]>();
321 const odd: Seg[] = [];
322 const spanning: Seg[] = [];
323 for (const s of segs) {
324 const a = level.get(s.from)!;
325 const b = level.get(s.to)!;
326 if (a === b) continue;
327 spanning.push(s);
328 if (b === a + 1) push(gaps, a, [pos.get(s.from)!, pos.get(s.to)!]);
329 else odd.push(s);
330 }
312331 let n = 0;
313 for (let i = 0; i < segs.length; i++)
314 for (let j = i + 1; j < segs.length; j++)
315 if (cross(segs[i]!, segs[j]!, level, pos)) n++;
332 for (const hops of gaps.values()) {
333 // Top end ascending, bottom ascending on ties: an equal top or an equal
334 // bottom is a shared card, which meets rather than crosses.
335 hops.sort((x, y) => x[0] - y[0] || x[1] - y[1]);
336 const size = hops.reduce((most, h) => Math.max(most, h[1]), 0) + 2;
337 const tree = new Int32Array(size + 1);

Math.max(...hops.map(...)) spreads one argument per hop. A very wide gap could hit the argument limit. A plain loop or reduce costs nothing and removes that risk.

338 let seen = 0;
339 for (const [, bottom] of hops) {
340 // Hops already seen with a bottom strictly right of this one's.
341 let atMost = 0;
342 for (let i = bottom + 1; i > 0; i -= i & -i) atMost += tree[i]!;
343 n += seen - atMost;
344 for (let i = bottom + 1; i <= size; i += i & -i) tree[i]!++;
345 seen++;
346 }
347 }
348 // An odd hop against every hop, each pair of odd hops once.
349 const oddAt = new Map(odd.map((s, i) => [s, i]));
350 odd.forEach((s, i) => {
351 for (const t of spanning) {
352 const j = oddAt.get(t);
353 if ((j === undefined || j > i) && cross(s, t, level, pos)) n++;
354 }
355 });
316356 return n;
317357 }
318358
@@ -383,8 +423,8 @@ function order(
383423
384424 const touching = new Map<string, Seg[]>();
385425 for (const s of segs) {
386 touching.set(s.to, [...(touching.get(s.to) ?? []), s]);
387 touching.set(s.from, [...(touching.get(s.from) ?? []), s]);
426 push(touching, s.to, s);
427 push(touching, s.from, s);
388428 }
389429 const between = (a: string, b: string): number => {
390430 let n = 0;
test/helpers/flow-fixture.ts added +117
@@ -0,0 +1,117 @@
1 /**
2 * A forge sized like production's for the flow readers (#3693): one app
3 * repository with a month of runs (checks, crew, reviews, deploys) and two
4 * weeks of merged pulls, the open asks the crew is on and the edges
5 * between them, and a month of page views, written straight into the tables so a suite can build it
6 * in seconds. Deterministic: the same call writes the same rows.
7 */
8 import type { App } from "../../src/app.ts";
9 import { createRepo } from "../../src/actions/repos.ts";
10 import type { User } from "../../src/core/users.ts";
11
12 export interface FlowFixture {
13 readonly runs: number;
14 readonly pulls: number;
15 readonly open: number;
16 readonly edges: number;
17 /** Page views over the month. */
18 readonly vitals: number;
19 }
20
21 const DAY = 86_400_000;
22
23 export async function seedFlowFixture(
24 app: App,
25 owner: User,
26 size: FlowFixture = {
27 runs: 100_000,
28 pulls: 1_500,
29 open: 400,
30 edges: 2_266,
31 vitals: 30_000,
32 },
33 now = Date.now(),
34 ): Promise<FlowFixture> {
35 const made = await createRepo(app, owner, { name: "busy", visibility: "public" });
36 if (!made.ok) throw new Error(`no repo: ${made.error}`);
37 const repo = made.value;
38 const db = app.db;
39 db.run("UPDATE repo SET autonomy = 'auto', autonomy_slots = 2 WHERE id = ?1", [repo.id]);
40 db.run("INSERT INTO app (id, repo_id, slug, access) VALUES ('app-busy', ?1, 'busy', 'public')", [repo.id]);
41 let s = 3693;
42 const r = (): number => {
43 s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
44 return s / 2 ** 32;
45 };
46 const iso = (ms: number): string => new Date(ms).toISOString();
47 db.transaction(() => {
48 const issue = db.prepare(
49 `INSERT INTO issue (id, repo_id, number, kind, title, body, author_id, state, created_at)
50 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)`,
51 );
52 const pull = db.prepare(
53 `INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid, merged_at)
54 VALUES (?1, 'main', ?2, '', '', ?3)`,
55 );
56 for (let n = 1; n <= size.pulls; n++) {
57 const asked = now - r() * 21 * DAY;
58 issue.run(`fi${n}`, repo.id, 2 * n - 1, "issue", `Ask ${n}`, "", owner.id, "closed", iso(asked));
59 const opened = asked + r() * DAY;
60 const merged = Math.min(now - 60_000, opened + r() * 3 * DAY);
61 issue.run(`fp${n}`, repo.id, 2 * n, "pull", `Pull ${n}`, `Closes #${2 * n - 1}`, owner.id, "closed", iso(opened));
62 pull.run(`fp${n}`, `crew/${n}`, iso(merged));
63 }
64 // Open asks, each with a finished crew run on a branch of its own.
65 for (let n = 1; n <= size.open; n++)
66 issue.run(`fo${n}`, repo.id, 2 * size.pulls + n, "issue", `Open ${n}`, "", owner.id, "open", iso(now - r() * 30 * DAY));
67 const run = db.prepare(
68 `INSERT INTO run (id, repo_id, kind, issue_id, env, branch, state, reason, created_at, started_at, finished_at, serving_at)
69 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)`,
70 );
71 for (let n = 0; n < size.runs; n++) {
72 const at = now - r() * 30 * DAY;
73 const pick = r();
74 const kind = pick < 0.7 ? "check" : pick < 0.95 ? "agent" : "deploy";
75 const pullN = 1 + Math.floor(r() * size.pulls);
76 const open = kind === "agent" && size.open > 0 && r() < 0.05 ? 1 + Math.floor(r() * size.open) : 0;
77 const env = kind === "deploy" ? (r() < 0.5 ? "production" : "preview") : "";
78 run.run(
79 `fr${String(n).padStart(7, "0")}`,
80 repo.id,
81 kind,
82 kind === "deploy" ? null : open > 0 ? `fo${open}` : `fp${pullN}`,
83 env,
84 kind === "deploy" ? "main" : open > 0 ? `crew/open-${open}` : `crew/${pullN}`,
85 r() < 0.9 ? "ok" : "failed",
86 kind === "agent" && r() < 0.4 ? "review" : "",
87 iso(at),
88 iso(at + 1_000),
89 iso(at + 60_000),
90 env === "production" ? iso(at + 90_000) : null,
91 );
92 }
93 // Production's map: 2,266 edges, each ask waiting on an older one.
94 const asks = [
95 ...Array.from({ length: size.pulls }, (_, n) => `fi${n + 1}`),
96 ...Array.from({ length: size.open }, (_, n) => `fo${n + 1}`),
97 ];
98 const edge = db.prepare("INSERT OR IGNORE INTO issue_dependency (issue_id, blocked_by_id) VALUES (?1, ?2)");
99 for (let n = 0; n < size.edges; n++) {
100 const later = 1 + Math.floor(r() * (asks.length - 1));
101 edge.run(asks[later]!, asks[Math.floor(r() * later)]!);
102 }
103 // A month of page views, as the beacon writes them: 1,000 a day.
104 const vital = db.prepare(
105 "INSERT INTO page_vital (day, at, route, visitor, ttfb, load, lcp, inp, cls) VALUES (?1, ?2, '/', ?3, 20, ?4, ?4, ?5, 0)",
106 );
107 for (let n = 0; n < size.vitals; n++) {
108 const at = now - r() * 30 * DAY;
109 vital.run(iso(at).slice(0, 10), at, `v${Math.floor(r() * 400)}`, 100 + r() * 900, r() < 0.5 ? null : 10 + r() * 200);
110 }
111 })();
112 // The seed went through the WAL: fold it back and give its pages up, so
113 // the suite holds one copy of the data on disk and not two. The gate runs
114 // many suites side by side on one disk (#3693).
115 db.run("PRAGMA wal_checkpoint(TRUNCATE)");
116 return { ...size };
117 }
test/helpers/scratch.ts added +18
@@ -0,0 +1,18 @@
1 /**
2 * A temporary directory on the checkout's own disk, for a suite whose data
3 * would crowd `/tmp`. A check cell's `/tmp` is a 256 MiB tmpfs
4 * (`TMP_MIB` in src/apps/contain.ts) shared by every file the gate runs at
5 * once and by their coverage; a suite that writes tens of MB there fills it
6 * as its neighbours start, and they fail in setup with no test named (#3693).
7 * The checkout is on the cell's disk. `/.test-scratch/` is ignored by git.
8 */
9 import { mkdirSync, mkdtempSync } from "node:fs";
10 import { join } from "node:path";
11
12 export const SCRATCH_ROOT = join(import.meta.dir, "..", "..", ".test-scratch");
13
14 /** A fresh directory under {@link SCRATCH_ROOT}; the suite removes it. */
15 export function scratchDir(prefix: string): string {
16 mkdirSync(SCRATCH_ROOT, { recursive: true });
17 return mkdtempSync(join(SCRATCH_ROOT, prefix));
18 }
test/map-layout-budget.test.ts added +168
@@ -0,0 +1,168 @@
1 /**
2 * The map's layout at the forge's own size (#3693): production's graph,
3 * 1,314 issues and 2,266 edges, took 34 s of `layout` on a cold `/map`
4 * because every ordering pass compared every pair of hops. The count is now
5 * an inversion count per gap between levels; these tests hold it to the
6 * pairwise definition, and hold the layout to a budget measured against a
7 * same-run baseline rather than the wall clock (#3069).
8 */
9 import { describe, expect, test } from "bun:test";
10 import type { Graph } from "../src/core/dependencies.ts";
11 import type { Issue } from "../src/core/issues.ts";
12 import { countCrossings, layout, type Seg } from "../src/web/pages/map.ts";
13
14 /**
15 * The definition `countCrossings` must agree with, written out here so the
16 * oracle shares no code with what it checks: two hops cross when they share
17 * no end, their spans of levels overlap, and their ends are in opposite order.
18 */
19 function pairwiseCrossings(
20 segs: readonly Seg[],
21 level: Map<string, number>,
22 pos: Map<string, number>,
23 ): number {
24 let n = 0;
25 for (let i = 0; i < segs.length; i++)
26 for (let j = i + 1; j < segs.length; j++) {
27 const s = segs[i]!;
28 const t = segs[j]!;
29 if (s.from === t.from || s.to === t.to || s.from === t.to || s.to === t.from) continue;
30 const [sa, sb, ta, tb] = [s.from, s.to, t.from, t.to].map((id) => level.get(id)!) as [number, number, number, number];
31 if (Math.max(Math.min(sa, sb), Math.min(ta, tb)) >= Math.min(Math.max(sa, sb), Math.max(ta, tb))) continue;
32 const top = pos.get(s.from)! - pos.get(t.from)!;
33 const bottom = pos.get(s.to)! - pos.get(t.to)!;
34 if (top * bottom < 0) n++;
35 }
36 return n;
37 }
38
39 /** A seeded generator, so a failure reproduces. */
40 function rng(seed: number): () => number {
41 let s = seed >>> 0;
42 return () => {
43 s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
44 return s / 2 ** 32;
45 };
46 }
47
48 /** Production's shape: about 1.7 edges an issue, each on an older issue. */
49 function productionGraph(issues = 1314, edges = 2266): Graph {
50 const r = rng(3693);
51 const seen = new Set<string>();
52 const list: { issueId: string; blockedById: string }[] = [];
53 while (list.length < edges) {
54 const issue = 2 + Math.floor(r() * (issues - 1));
55 const blockedBy = 1 + Math.floor(r() * (issue - 1));
56 const key = `${issue}>${blockedBy}`;
57 if (seen.has(key)) continue;
58 seen.add(key);
59 list.push({ issueId: `i${issue}`, blockedById: `i${blockedBy}` });
60 }
61 return {
62 issues: Array.from(
63 { length: issues },
64 (_, n) => ({ id: `i${n + 1}`, number: n + 1 }) as unknown as Issue,
65 ),
66 edges: list,
67 held: [],
68 deferred: new Map(),
69 unadopted: new Set(),
70 structural: new Set(),
71 } as unknown as Graph;
72 }
73
74 function median(run: () => void, times = 3): number {
75 const spent: number[] = [];
76 for (let n = 0; n < times; n++) {
77 const started = performance.now();
78 run();
79 spent.push(performance.now() - started);
80 }
81 return spent.sort((a, b) => a - b)[Math.floor(times / 2)]!;
82 }
83
84 describe("crossings, counted per gap", () => {
85 test("agree with every pair compared, cycles and long hops included", () => {
86 const r = rng(7);
87 for (let round = 0; round < 200; round++) {
88 const levels = 1 + Math.floor(r() * 6);
89 const level = new Map<string, number>();
90 const pos = new Map<string, number>();
91 const ids: string[] = [];
92 for (let l = 0; l < levels; l++) {
93 const wide = 1 + Math.floor(r() * 7);
94 for (let p = 0; p < wide; p++) {
95 const id = `n${l}.${p}`;
96 level.set(id, l);
97 pos.set(id, p);
98 ids.push(id);
99 }
100 }
101 const segs: Seg[] = [];
102 const hops = Math.floor(r() * 30);
103 for (let h = 0; h < hops; h++) {
104 const from = ids[Math.floor(r() * ids.length)]!;
105 // Mostly one level down, as `layout` splits them; sometimes any.
106 const below = ids.filter((id) => level.get(id) === level.get(from)! + 1);
107 const to =
108 below.length > 0 && r() < 0.7
109 ? below[Math.floor(r() * below.length)]!
110 : ids[Math.floor(r() * ids.length)]!;
111 segs.push({ from, to });
112 }
113 expect(countCrossings(segs, level, pos)).toBe(
114 pairwiseCrossings(segs, level, pos),
115 );
116 }
117 });
118
119 test("a known braid: two hops swapping ends cross once, sharing an end never", () => {
120 const level = new Map([
121 ["a", 0],
122 ["b", 0],
123 ["c", 1],
124 ["d", 1],
125 ]);
126 const pos = new Map([
127 ["a", 0],
128 ["b", 1],
129 ["c", 0],
130 ["d", 1],
131 ]);
132 const crossed = [
133 { from: "a", to: "d" },
134 { from: "b", to: "c" },
135 ];
136 const shared = [
137 { from: "a", to: "c" },
138 { from: "a", to: "d" },
139 { from: "b", to: "d" },
140 ];
141 expect(countCrossings(crossed, level, pos)).toBe(1);
142 expect(countCrossings(shared, level, pos)).toBe(0);
143 });
144 });
145
146 describe("the layout at production's size", () => {
147 const graph = productionGraph();
148
149 test("is drawn deep and wide, like production's", () => {
150 const placed = layout(graph);
151 expect(placed.levels).toBeGreaterThan(10);
152 expect(placed.routes.length).toBeGreaterThan(500);
153 });
154
155 test("costs a few pairwise counts of its own card-to-card edges at most", () => {
156 // Before #3693 the layout ran that pairwise count at least five times,
157 // over more hops than this (every waypoint is one): about 50 baselines
158 // on this graph. Now the whole layout is under one; three is the slack
159 // a loaded machine gets.
160 const placed = layout(graph);
161 const level = new Map(placed.nodes.map((n) => [n.issue.id, n.level]));
162 const pos = new Map(placed.nodes.map((n) => [n.issue.id, n.slot]));
163 const segs = graph.edges.map((e) => ({ from: e.blockedById, to: e.issueId }));
164 const baseline = median(() => pairwiseCrossings(segs, level, pos));
165 const spent = median(() => layout(graph));
166 expect(spent).toBeLessThan(3 * baseline);
167 });
168 });
test/observability-budget.test.ts added +114
@@ -0,0 +1,114 @@
1 /**
2 * `/boards`, `/map` and home on a forge sized like production's (#3693):
3 * a month of 100,000 runs, 1,500 merged pulls, 400 open asks and 2,266
4 * edges between them. The owner saw /boards take 8–10 s, a cold /map 34.7 s
5 * and home 9.2 s. Each budget is a multiple of one full pass over the run
6 * table, timed in the same run, so a loaded machine slows both sides and
7 * the wall clock is never the judge (#3069). Before the fix each page cost
8 * hundreds of those passes.
9 */
10 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
11 import { rm } from "node:fs/promises";
12 import { tmpdir } from "node:os";
13 import { createApp, type App } from "../src/app.ts";
14 import { createSession } from "../src/core/sessions.ts";
15 import { serve, type Servers } from "../src/http/server.ts";
16 import { clearMapCache } from "../src/web/map_cache.ts";
17 import { p75 } from "../src/flow/reach.ts";
18 import { seedAdmin } from "./helpers/admin.ts";
19 import { seedFlowFixture } from "./helpers/flow-fixture.ts";
20 import { SCRATCH_ROOT, scratchDir } from "./helpers/scratch.ts";
21
22 let dataDir = "";
23 let app: App, servers: Servers, cookie = "";
24
25 beforeAll(async () => {
26 // ~49 MB of database: on the checkout's disk, not the cell's shared /tmp.
27 dataDir = scratchDir("forge-obs-budget-");
28 app = createApp({ dataDir, port: 0 });
29 servers = await serve(app);
30 const made = await seedAdmin(app, { username: "alice", email: "[email protected]", password: "correct-horse-battery" });
31 if (!made.ok) throw new Error(made.error);
32 await seedFlowFixture(app, made.value);
33 cookie = `forge_session=${createSession(app.db, made.value.id).token}`;
34 }, 120_000);
35
36 afterAll(async () => {
37 // A setup that failed part way leaves these unset: say only what it said.
38 servers?.stop();
39 app?.close();
40 if (dataDir !== "") await rm(dataDir, { recursive: true, force: true });
41 });
42
43 /** One full pass over every run: the unit the budgets are counted in. */
44 function scan(): number {
45 const started = performance.now();
46 app.db.query("SELECT COUNT(*), SUM(length(branch)), SUM(length(created_at)) FROM run").get();
47 return performance.now() - started;
48 }
49
50 function baseline(): number {
51 const spent = [scan(), scan(), scan()].sort((a, b) => a - b);
52 return spent[1]!;
53 }
54
55 async function get(path: string): Promise<{ ms: number; status: number; timing: string }> {
56 const started = performance.now();
57 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
58 await res.arrayBuffer();
59 return { ms: performance.now() - started, status: res.status, timing: res.headers.get("server-timing") ?? "" };
60 }
61
62 const names = (timing: string): string[] => timing.split(", ").map((p) => p.split(";")[0]!);
63
64 describe("on production's size", () => {
65 test("the fixture lives on the checkout's disk, not in the shared /tmp", () => {
66 expect(dataDir.startsWith(SCRATCH_ROOT)).toBe(true);
67 expect(dataDir.startsWith(tmpdir())).toBe(false);
68 });
69
70 test("the fixture is production's size", () => {
71 const n = (sql: string) => (app.db.query(sql).get() as { n: number }).n;
72 expect(n("SELECT COUNT(*) AS n FROM run")).toBe(100_000);
73 expect(n("SELECT COUNT(*) AS n FROM issue_dependency")).toBeGreaterThan(2_000);

A 30x multiple of one scan is a loose bound on a loaded machine; fine now, but watch it for flakiness.

74 });
75
76 test("/boards answers within a few passes over the runs, and names its phases", async () => {
77 await get("/boards");
78 const unit = baseline();
79 const res = await get("/boards");
80 expect(res.status).toBe(200);
81 for (const n of ["session", "boards", "flow", "render"]) expect(names(res.timing)).toContain(n);
82 expect(res.ms).toBeLessThan(30 * unit);
83 });
84
85 test("a cold /map answers within a few passes over the runs", async () => {
86 await get("/map");
87 clearMapCache();
88 const unit = baseline();
89 const res = await get("/map");
90 expect(res.status).toBe(200);
91 expect(names(res.timing)).toContain("layout");
92 expect(res.ms).toBeLessThan(60 * unit);
93 });
94
95 test("a cold home answers within a few passes over the runs, kpis read afresh", async () => {
96 await get("/");
97 // A write moves the clock home's caches are keyed by: this view is cold.
98 app.db.run("UPDATE repo SET description = description || ''");
99 const unit = baseline();
100 const res = await get("/");
101 expect(res.status).toBe(200);
102 expect(names(res.timing)).toContain("kpis");
103 expect(res.timing).not.toMatch(/kpis;dur=[\d.]+;desc="hit"/);
104 expect(res.ms).toBeLessThan(30 * unit);
105 });
106 });
107
108 test("home's p75 is nearest rank, read in one pass: the value at ⌈0.75·n⌉", () => {
109 expect(p75([])).toBeNull();
110 expect(p75([7])).toBe(7);
111 // n = 4: rank 3. n = 5: rank ⌈3.75⌉ = 4. Order of arrival does not matter.
112 expect(p75([40, 10, 30, 20])).toBe(30);
113 expect(p75([50, 10, 40, 20, 30])).toBe(40);
114 });
test/server-timing-sweep.test.ts −1
@@ -40,7 +40,6 @@ const UNNAMED: Array<[path: string, issue: string]> = [
4040 ["/alice/forge/toolbar", "#3390"],
4141 ["/apps/open", "#3390"],
4242 ["/board", "#3378"],
43 ["/boards", "#3390"],
4443 ["/claim", "#3390"],
4544 ["/front", "#3311"],
4645 ["/live/apps", "#3390"],

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs