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

No wall-clock speed asserts in tests: one budget helper that is load-proof, and the gate refuses raw "under N ms" expects #4431

Merged opened this in answer to

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

Merged

scout/forge:scout/4429-no-wall-clock-speed-asserts-in-tests-one → main 5 commits fork 4 threads · 4 unresolved

  • Merged. scout/4429-no-wall-clock-speed-asserts-in-tests-one was merged into main as da58f3c.

Review

Sign in to review this change.
15 files changed +182 −31
CLAUDE.md +5 −1
@@ -98,7 +98,11 @@ bun run src/main.ts # boots on :3000, data in ./data
9898 `EXCLUDED`, so never hand-edit that move. Never add `retry` — fix the
9999 cause: a shared port, a real clock, order dependence, a leaked server, a
100100 timeout near the real duration, a wall-clock bound under 200 ms on a
101 request or a spawn (`test/flake.test.ts` refuses one).
101 request or a spawn (`test/flake.test.ts` refuses one). A speed is bounded only
102 with `expectBudget` (`test/helpers/budget.ts`): relative to a same-run
103 baseline, absolute only when `FORGE_TEST_QUIET=1` (set by hand on a quiet machine; no
outdated

Refers to a nightly perf lane that does not exist; either create it or say that absolute budgets are not enforced anywhere yet.

104 lane sets it yet, so absolute budgets are not enforced anywhere);
105 `test/flake.test.ts` refuses a raw `under N ms` title or bound.
102106 - **Every non-trivial change ships with a test.** Prefer protocol-level tests
103107 that boot a real app (`createApp` + `serve`) over unit mocks. Adversarial
104108 tests (can this be bypassed?) are worth more than happy-path ones.
scripts/flake.ts +25
@@ -288,6 +288,31 @@ export function tightBounds(name: string, source: string): string[] {
288288 return found;
289289 }
290290
291 const BUDGET_TITLE = /\b(?:answers?|renders?|runs?|takes?)\s+(?:in\s+)?under\s+[\d_.]+\s*ms\b/i;
292 const BUDGET_MEASURE =
293 /^(?:dur\(|total\b|best\b|left\b|(?:got|res|out|r)\.(?:ms|app))/;
294
295 /**
296 * Every line of `source` that holds a speed to a raw millisecond number: a
297 * title saying "under N ms", or a Server-Timing/elapsed time bounded by a
298 * literal under the floor (a hang guard above it stays a plain expect). `test/helpers/budget.ts` is the one way (#4429).
299 */
300 export function rawBudgets(name: string, source: string): string[] {
301 const found: string[] = [];
302 source.split("\n").forEach((line, i) => {
303 let raw = BUDGET_TITLE.test(line);
304 for (const [, measured, bound] of line.matchAll(UPPER_BOUND))
305 if (
306 (TIMED.test(measured!) || BUDGET_MEASURE.test(measured!.trim())) &&
307 Number(bound!.replaceAll("_", "")) < WALL_CLOCK_FLOOR_MS
308 )
309 raw = true;
310 if (raw && !/expectBudget/.test(line))
311 found.push(`${name}:${i + 1}: ${line.trim()} — bound speed with expectBudget() from test/helpers/budget.ts`);
312 });
313 return found;
314 }
315
291316 /** The record an admitting pull carries. */
292317 export function admissionRecord(
293318 file: string,
test/app-pages-timing.test.ts +5 −3
@@ -1,5 +1,6 @@
1 /** `/:owner/:name/app` and its `answers`, `now` and `state` fragments name their Server-Timing phases and answer under 50 ms (#4320). */
1 /** `/:owner/:name/app` and its `answers`, `now` and `state` fragments name their Server-Timing phases and answer within budget (#4320). */
22 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { baselineMs, expectBudget } from "./helpers/budget.ts";
34 import { mkdtempSync, rmSync } from "node:fs";
45 import { tmpdir } from "node:os";
56 import { join } from "node:path";
@@ -79,7 +80,8 @@ const get = (path: string) =>
7980 fetch(`${servers.url}${path}`, { headers: { cookie }, redirect: "manual" });
8081
8182 for (const [route, reads] of ROUTES)
82 test(`GET /alice/vault${route} names its phases and answers under 50 ms`, async () => {
83 test(`GET /alice/vault${route} names its phases and answers within its budget`, async () => {
84 const against = await baselineMs(servers.url);
8385 await (await get(`/alice/vault${route}`)).arrayBuffer(); // warm
8486 // Timers and GC jitter on a busy machine: judge the best of several requests.
8587 let total = Infinity;
@@ -94,7 +96,7 @@ for (const [route, reads] of ROUTES)
9496 total = Math.min(total, got.get("app")!);
9597 left = Math.min(left, Math.abs(got.get("app")! - sum));
9698 }
97 expect(total).toBeLessThan(50);
99 expectBudget(total, { ms: 50, against });
98100 expect(left).toBeLessThanOrEqual(1.5); // each is rounded to 0.1
99101 });
100102
test/asked-timing.test.ts +5 −3
@@ -1,5 +1,6 @@
1 /** `/asked` names session, each read and render in Server-Timing, and answers a person with 500 asks in under 50 ms (#3358). */
1 /** `/asked` names session, each read and render in Server-Timing, and answers a person with 500 asks within budget (#3358). */
22 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { baselineMs, expectBudget } from "./helpers/budget.ts";
34 import { mkdtempSync, rmSync } from "node:fs";
45 import { tmpdir } from "node:os";
56 import { join } from "node:path";
@@ -71,7 +72,8 @@ afterAll(() => {
7172 rmSync(root, { recursive: true, force: true });
7273 });
7374
74 test("GET /asked names its phases and answers under 50 ms with 500 asks", async () => {
75 test("GET /asked names its phases and answers within its budget with 500 asks", async () => {
76 const against = await baselineMs(servers.url);
7577 const get = () => fetch(`${servers.url}/asked?show=all`, { headers: { cookie } });
7678 await (await get()).text();
7779 // The best of three: a shared runner's stall hits one request, not the page's cost.
@@ -86,5 +88,5 @@ test("GET /asked names its phases and answers under 50 ms with 500 asks", async
8688 expect(parts.find((p) => p.startsWith(`${name};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/);
8789 best = Math.min(best, Number(/app;dur=([\d.]+)/.exec(parts.join(", "))![1]));
8890 }
89 expect(best).toBeLessThan(50);
91 expectBudget(best, { ms: 50, against });
9092 });
test/budget.test.ts added +39
@@ -0,0 +1,39 @@
1 /** `budgetLimit` is the one place a speed budget is decided (#4429): absolute when quiet, relative and capped when not. */
2 import { afterEach, expect, test } from "bun:test";
3 import { BASELINE_FACTOR, MAX_RELAXATION, budgetLimit, expectBudget } from "./helpers/budget.ts";
4
5 const saved = process.env.FORGE_TEST_QUIET;
6 afterEach(() => {
7 if (saved === undefined) delete process.env.FORGE_TEST_QUIET;
8 else process.env.FORGE_TEST_QUIET = saved;
9 });
10
11 test("a quiet cell holds the absolute ms, whatever the baseline", () => {
12 process.env.FORGE_TEST_QUIET = "1";
13 expect(budgetLimit(50, 0.1)).toBe(50);
14 expect(budgetLimit(50, 100)).toBe(50);
15 });
16
17 test("a loaded cell scales with the baseline, never below ms", () => {
18 delete process.env.FORGE_TEST_QUIET;
19 expect(budgetLimit(50, 0.1)).toBe(50);
20 expect(budgetLimit(50, 6)).toBe(Math.min(50 * MAX_RELAXATION, 6 * BASELINE_FACTOR));
21 expect(budgetLimit(50, 6)).toBe(60);
22 });
23
24 test("a loaded cell never relaxes a budget past MAX_RELAXATION times ms", () => {
25 delete process.env.FORGE_TEST_QUIET;
26 expect(budgetLimit(50, 1000)).toBe(50 * MAX_RELAXATION);
27 expect(budgetLimit(15, 1000)).toBe(15 * MAX_RELAXATION);
28 });
29
30 test("only FORGE_TEST_QUIET=1 means quiet", () => {
31 process.env.FORGE_TEST_QUIET = "true";
32 expect(budgetLimit(50, 1000)).toBe(50 * MAX_RELAXATION);
33 });
34
35 test("expectBudget fails a page that regressed to 150 ms beside a 5 ms baseline", () => {
36 delete process.env.FORGE_TEST_QUIET;
37 expect(() => expectBudget(150, { ms: 50, against: 5 })).toThrow();
38 expect(() => expectBudget(40, { ms: 50, against: 5 })).not.toThrow();
39 });
test/cloud-org-apps-timing.test.ts +6 −4
@@ -1,5 +1,6 @@
1 /** `/settings/cloud/audit`, `/organizations/new` and `/apps/open` name their Server-Timing phases and answer under 50 ms (#4322). */
1 /** `/settings/cloud/audit`, `/organizations/new` and `/apps/open` name their Server-Timing phases and answer within budget (#4322). */
22 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { baselineMs, expectBudget } from "./helpers/budget.ts";
34 import { mkdtempSync, rmSync } from "node:fs";
45 import { tmpdir } from "node:os";
56 import { join } from "node:path";
@@ -52,7 +53,8 @@ const PAGES: [string, string[]][] = [
5253 ];
5354
5455 for (const [path, names] of PAGES)
55 test(`GET ${path} names its phases and answers under 50 ms`, async () => {
56 test(`GET ${path} names its phases and answers within its budget`, async () => {
57 const against = await baselineMs(servers.url);
5658 const get = () => fetch(`${servers.url}${path}`, { headers: { cookie }, redirect: "manual" });
5759 await (await get()).text(); // warm
5860 // Timers jitter on a busy machine: judge the best of several requests.
@@ -69,8 +71,8 @@ for (const [path, names] of PAGES)
6971 // Phases name the whole request: what is left over is small.
7072 left = Math.min(left, Math.abs(dur("app") - names.reduce((sum, n) => sum + dur(n), 0)));
7173 }
72 expect(total).toBeLessThan(50);
73 expect(left).toBeLessThan(1.5);
74 expectBudget(total, { ms: 50, against });
75 expect(left).toBeLessThanOrEqual(1.5); // accounting, not speed: each phase is rounded to 0.1
outdated

A phase-accounting gap of 1.5 ms is not a speed; with the helper it is now bounded at 200 ms and proves nothing. Keep the plain expect.

7476 });
7577
7678 // Signed out, the redirect to sign-in is still a named phase.
test/explore-rank.test.ts +5 −3
@@ -1,4 +1,4 @@
1 /** /explore's rank phase stays under 15 ms with 500 public repositories (#3394). */
1 /** /explore's rank phase stays within budget with 500 public repositories (#3394). */
22 import { afterAll, beforeAll, expect, test } from "bun:test";
33 import { mkdir, mkdtemp, rm } from "node:fs/promises";
44 import { tmpdir } from "node:os";
@@ -10,6 +10,7 @@ import { createApp as insertApp } from "../src/core/apps.ts";
1010 import { createIssue } from "../src/core/issues.ts";
1111 import { createUser, type User } from "../src/core/users.ts";
1212 import { countStatements } from "../bench/run.ts";
13 import { quiet } from "./helpers/budget.ts";
1314 import { quietLogs } from "./helpers/globals.ts";
1415 import { proveAll } from "./helpers/verified.ts";
1516 import { promote } from "./helpers/members.ts";
@@ -84,7 +85,7 @@ afterAll(async () => {
8485
8586 const query = parseExploreQuery("");
8687
87 test("a warm rank of 500 public repositories is a fraction of a cold one and under 15 ms", async () => {
88 test("a warm rank of 500 public repositories is a fraction of a cold one and within its budget", async () => {
8889 const cold: number[] = [];
8990 const warm: number[] = [];
9091 await explore(app, undefined, query);
@@ -96,7 +97,8 @@ test("a warm rank of 500 public repositories is a fraction of a cold one and und
9697 await explore(app, undefined, query);
9798 warm.push(performance.now() - t);
9899 }
99 expect(Math.min(...warm)).toBeLessThan(15);
100 // The relative bound below holds under load; the absolute 15 ms only in a quiet cell.
101 if (quiet()) expect(Math.min(...warm)).toBeLessThan(15);
100102 expect(Math.min(...warm) * 5).toBeLessThan(Math.min(...cold));
101103 });
102104
test/flake.test.ts +20
@@ -30,6 +30,7 @@ import {
3030 ROOT_CAUSES,
3131 shuffled,
3232 tightBounds,
33 rawBudgets,
3334 WALL_CLOCK_FLOOR_MS,
3435 } from "../scripts/flake.ts";
3536
@@ -350,6 +351,25 @@ describe("no tight wall-clock bound in a test", () => {
350351 }, 120_000); // reads every test file: past the 5 s default on a loaded machine
351352 });
352353
354 describe("no raw wall-clock budget in a test (#4429)", () => {
355 test("the detector sees a raw ms bound and a title promising one", () => {
356 expect(rawBudgets("x", "expect(dur(\"app\")).toBeLessThan(50);")).toHaveLength(1);
357 expect(rawBudgets("x", "expect(total).toBeLessThan(50);")).toHaveLength(1);
358 expect(rawBudgets("x", "test(`GET /a answers under 50 ms`, () => {});")).toHaveLength(1);
359 expect(rawBudgets("x", "expectBudget(dur(\"app\"), { ms: 50, against });")).toEqual([]);
360 expect(rawBudgets("x", "expect(elapsed).toBeLessThan(5_000);")).toEqual([]); // a hang guard
361 expect(rawBudgets("x", "expect(res.status).toBeLessThan(400);")).toEqual([]);
362 expect(rawBudgets("x", "expect(lines.length).toBeLessThan(20);")).toEqual([]);
363 });
364
365 test("no test file holds a speed to a raw ms number", () => {
366 const files = readdirSync(`${root}test`)
367 .filter((name) => name.endsWith(".test.ts") && name !== "flake.test.ts")
368 .map((name) => `test/${name}`);
369 expect(files.flatMap((path) => rawBudgets(path, read(path)))).toEqual([]);
370 }, 120_000);
371 });
372
353373 describe("the scheduled run of the files outside the gate", () => {
354374 const present = ["test/a.test.ts", "test/b.test.ts", "test/c.test.ts"];
355375 const excluded: Exclusion[] = [
test/helpers/budget.ts added +44
@@ -0,0 +1,44 @@
1 /**
2 * The one way a test bounds a speed (#4429).
3 *
4 * A raw `expect(ms).toBeLessThan(50)` is a coin flip in a loaded check cell:
5 * the same code is 3 ms on a quiet machine and 80 ms beside six busy cells.
6 * `expectBudget` judges a measured time against a baseline taken in the same
7 * run, so load that slows the page slows the baseline with it. The absolute
8 * `ms` is asserted when the cell is quiet (`FORGE_TEST_QUIET=1`, set by hand on
9 * a quiet machine; no lane sets it yet). Otherwise the limit is relative to the
10 * baseline but never above `MAX_RELAXATION` times `ms`, so a 50 ms budget
11 * cannot drift past 200 ms however slow the baseline.
12 */
13 import { expect } from "bun:test";
14
15 /** How many times slower than the same-run baseline a page may be under load. */
16 export const BASELINE_FACTOR = 10;
17
18 /** The most a loaded cell may stretch a budget: the limit is never above this many times `ms`. */
19 export const MAX_RELAXATION = 4;
20
21 /** Whether the cell is quiet enough to hold a number to its absolute ms. */
22 export const quiet = (): boolean => process.env.FORGE_TEST_QUIET === "1";
23
outdated

The 200 ms floor makes every sub-200 ms budget unenforceable outside the quiet lane, which nothing sets. Cap the relaxed limit relative to ms, and unit-test this function.

24 /** The ms a measured time may take: `ms` when quiet, else relative to `against`. */
25 export function budgetLimit(ms: number, against: number): number {
26 if (quiet()) return ms;
27 return Math.min(ms * MAX_RELAXATION, Math.max(ms, against * BASELINE_FACTOR));
28 }
29
30 /** Asserts `measured` (ms) fits the budget `ms`, judged against `against` (ms, same run) unless quiet. */
31 export function expectBudget(measured: number, opts: { ms: number; against: number }): void {
32 expect(measured).toBeLessThan(budgetLimit(opts.ms, opts.against));
33 }
34
35 /** The best wall-clock ms of a few loopback requests to `/healthz`: what a request costs right now. */
36 export async function baselineMs(url: string, runs = 5): Promise<number> {
37 let best = Infinity;
38 for (let i = 0; i < runs; i++) {
39 const started = performance.now();
40 await (await fetch(`${url}/healthz`)).text();
41 best = Math.min(best, performance.now() - started);
42 }
43 return best;
44 }
test/repo-pages-timing.test.ts +5 −3
@@ -1,5 +1,6 @@
1 /** A repository's new issue, make-it-yours, report and toolbar pages name their phases and answer under 50 ms (#4321). */
1 /** A repository's new issue, make-it-yours, report and toolbar pages name their phases and answer within budget (#4321). */
22 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { baselineMs, expectBudget } from "./helpers/budget.ts";
34 import { mkdtempSync, rmSync } from "node:fs";
45 import { tmpdir } from "node:os";
56 import { join } from "node:path";
@@ -64,7 +65,8 @@ const PAGES: [path: string, who: string, names: string[]][] = [
6465 ];
6566
6667 for (const [path, who, names] of PAGES)
67 test(`GET ${path} names its phases, they fit inside app, and it answers under 50 ms`, async () => {
68 test(`GET ${path} names its phases, they fit inside app, and it answers within its budget`, async () => {
69 const against = await baselineMs(servers.url);
6870 const cookie = await signIn(who);
6971 const read = async (): Promise<string[]> => {
7072 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
@@ -84,7 +86,7 @@ for (const [path, who, names] of PAGES)
8486 expect(named).toEqual(names);
8587 const total = dur(parts.find((p) => p.startsWith("app;")));
8688 const sum = parts.filter((p) => !/^(app|db);/.test(p)).reduce((s, p) => s + dur(p), 0);
87 expect(total).toBeLessThan(50);
89 expectBudget(total, { ms: 50, against });
8890 expect(sum).toBeLessThanOrEqual(total + 0.1 * names.length);
8991 shares.push(total === 0 ? 1 : sum / total);
9092 }
test/settings-repo-timing.test.ts +5 −3
@@ -1,5 +1,6 @@
1 /** A repository's settings pages name session, each read and render in Server-Timing, and answer under 50 ms (#4318). */
1 /** A repository's settings pages name session, each read and render in Server-Timing, and answer within budget (#4318). */
22 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { baselineMs, expectBudget } from "./helpers/budget.ts";
34 import { mkdtempSync, rmSync } from "node:fs";
45 import { tmpdir } from "node:os";
56 import { join } from "node:path";
@@ -56,12 +57,13 @@ const PAGES: [string, string[]][] = [
5657 ];
5758
5859 for (const [path, names] of PAGES)
59 test(`GET ${path} names its phases and answers under 50 ms`, async () => {
60 test(`GET ${path} names its phases and answers within its budget`, async () => {
6061 const get = async () => {
6162 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
6263 await res.text();
6364 return res;
6465 };
66 const against = await baselineMs(servers.url);
6567 await get(); // warm
6668 const res = await get();
6769 expect(res.status).toBe(200);
@@ -73,7 +75,7 @@ for (const [path, names] of PAGES)
7375 expect(Number.isFinite(dur(name))).toBe(true);
7476 }
7577 expect(got.filter((n) => n === "render").length).toBe(1);
76 expect(dur("app")).toBeLessThan(50);
78 expectBudget(dur("app"), { ms: 50, against });
7779 // The named phases cover the request (route and compress are the server's own); the issue's bound is 1 ms.
7880 const sum = names.reduce((t, n) => t + dur(n), 0);
7981 expect(Math.abs(dur("app") - sum)).toBeLessThanOrEqual(1);
test/settings-timing.test.ts +7 −4
@@ -1,5 +1,6 @@
1 /** `/settings/keys`, `/settings/tokens` and `/settings/password` name their phases signed in and answer under 50 ms (#4316). */
1 /** `/settings/keys`, `/settings/tokens` and `/settings/password` name their phases signed in and answer within budget (#4316). */
22 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { baselineMs, expectBudget } from "./helpers/budget.ts";
34 import { mkdtempSync, rmSync } from "node:fs";
45 import { tmpdir } from "node:os";
56 import { join } from "node:path";
@@ -51,7 +52,8 @@ const PAGES: [string, string[]][] = [
5152 ];
5253
5354 for (const [path, names] of PAGES)
54 test(`GET ${path} names its phases and answers under 50 ms`, async () => {
55 test(`GET ${path} names its phases and answers within its budget`, async () => {
56 const against = await baselineMs(servers.url);
5557 await fetch(`${servers.url}${path}`, { headers: { cookie } }); // warm
5658 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5759 await res.text();
@@ -63,9 +65,10 @@ for (const [path, names] of PAGES)
6365 return Number(part!.slice(`${name};dur=`.length));
6466 };
6567 expect(parts.map((p) => p.split(";")[0]).filter((n) => n !== "app")).toEqual(names);
66 expect(dur("app")).toBeLessThan(50);
68 expectBudget(dur("app"), { ms: 50, against });
6769 const sum = names.reduce((t, n) => t + dur(n), 0);
6870 // The rest of `app` is the server's own routing and header work, outside any page (~1-2 ms on every page).
6971 expect(sum).toBeLessThanOrEqual(dur("app") + 0.3);
70 expect(dur("app") - sum).toBeLessThan(5);
72 const gap = dur("app") - sum;
73 expect(gap).toBeLessThanOrEqual(5); // accounting, not speed
7174 });
test/signin-timing.test.ts +5 −3
@@ -1,5 +1,6 @@
1 /** `/signin` and `/signup` name session, each read and render in Server-Timing, and answer under 10 ms (#4106). */
1 /** `/signin` and `/signup` name session, each read and render in Server-Timing, and answer within budget (#4106). */
22 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { baselineMs, expectBudget } from "./helpers/budget.ts";
34 import { mkdtempSync, rmSync } from "node:fs";
45 import { tmpdir } from "node:os";
56 import { join } from "node:path";
@@ -30,7 +31,8 @@ const PAGES: [string, string[]][] = [
3031 ];
3132
3233 for (const [path, names] of PAGES)
33 test(`GET ${path} names its phases and answers under 10 ms`, async () => {
34 test(`GET ${path} names its phases and answers within its budget`, async () => {
35 const against = await baselineMs(servers.url);
3436 await fetch(`${servers.url}${path}`); // warm
3537 const res = await fetch(`${servers.url}${path}`);
3638 await res.text();
@@ -39,5 +41,5 @@ for (const [path, names] of PAGES)
3941 for (const name of ["app", ...names])
4042 expect(parts.find((p) => p.startsWith(`${name};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/);
4143 const app = parts.find((p) => p.startsWith("app;"))!;
42 expect(Number(app.slice("app;dur=".length))).toBeLessThan(10);
44 expectBudget(Number(app.slice("app;dur=".length)), { ms: 10, against });
4345 });
test/thread-render-timing.test.ts +1 −1
@@ -1,4 +1,4 @@
1 /** A thread of 200 comments renders in under 15 ms, and renders the same HTML it always did (#3324). */
1 /** A thread of 200 comments renders no comment body at read time (the memo counters, with a loose 250 ms guard), and renders the same HTML it always did (#3324). */
22 import { afterAll, beforeAll, expect, test } from "bun:test";
33 import { snappyFixture, type SnappyFixture } from "../bench/snappy.ts";
44 import { addComment, openIssue } from "../src/actions/issues.ts";
test/verify-timing.test.ts +5 −3
@@ -1,5 +1,6 @@
1 /** `/verify`, `/recover` and `/claim` name their phases in Server-Timing and answer under 50 ms, signed in (#4317). */
1 /** `/verify`, `/recover` and `/claim` name their phases in Server-Timing and answer within budget, signed in (#4317). */
22 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { baselineMs, expectBudget } from "./helpers/budget.ts";
34 import { mkdtempSync, rmSync } from "node:fs";
45 import { tmpdir } from "node:os";
56 import { join } from "node:path";
@@ -50,7 +51,8 @@ const PAGES: [string, string[]][] = [
5051 ];
5152
5253 for (const [path, names] of PAGES)
53 test(`GET ${path} names its phases and answers under 50 ms`, async () => {
54 test(`GET ${path} names its phases and answers within its budget`, async () => {
55 const against = await baselineMs(servers.url);
5456 const get = () => fetch(`${servers.url}${path}`, { headers: { cookie }, redirect: "manual" });
5557 await (await get()).text(); // warm
5658 const res = await get();
@@ -59,6 +61,6 @@ for (const [path, names] of PAGES)
5961 for (const name of ["app", ...names])
6062 expect(parts.find((p) => p.startsWith(`${name};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/);
6163 const dur = (n: string) => Number(parts.find((p) => p.startsWith(`${n};`))!.split("=")[1]);
62 expect(dur("app")).toBeLessThan(50);
64 expectBudget(dur("app"), { ms: 50, against });
6365 expect(names.reduce((s, n) => s + dur(n), 0)).toBeLessThanOrEqual(dur("app") + 1);
6466 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs