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
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4429-no-wall-clock-speed-asserts-in-tests-one → main 5 commits fork 4 threads · 4 unresolved
15 files
changed +182
−31
CLAUDE.md +5 −1
| @@ -98,7 +98,11 @@ bun run src/main.ts # boots on :3000, data in ./data | ||
| 98 | 98 | `EXCLUDED`, so never hand-edit that move. Never add `retry` — fix the |
| 99 | 99 | cause: a shared port, a real clock, order dependence, a leaked server, a |
| 100 | 100 | 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 | |
| 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. | |
| 102 | 106 | - **Every non-trivial change ships with a test.** Prefer protocol-level tests |
| 103 | 107 | that boot a real app (`createApp` + `serve`) over unit mocks. Adversarial |
| 104 | 108 | 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[] { | ||
| 288 | 288 | return found; |
| 289 | 289 | } |
| 290 | 290 | |
| 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 | ||
| 291 | 316 | /** The record an admitting pull carries. */ |
| 292 | 317 | export function admissionRecord( |
| 293 | 318 | 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). */ | |
| 2 | 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; |
| 3 | import { baselineMs, expectBudget } from "./helpers/budget.ts"; | |
| 3 | 4 | import { mkdtempSync, rmSync } from "node:fs"; |
| 4 | 5 | import { tmpdir } from "node:os"; |
| 5 | 6 | import { join } from "node:path"; |
| @@ -79,7 +80,8 @@ const get = (path: string) => | ||
| 79 | 80 | fetch(`${servers.url}${path}`, { headers: { cookie }, redirect: "manual" }); |
| 80 | 81 | |
| 81 | 82 | 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); | |
| 83 | 85 | await (await get(`/alice/vault${route}`)).arrayBuffer(); // warm |
| 84 | 86 | // Timers and GC jitter on a busy machine: judge the best of several requests. |
| 85 | 87 | let total = Infinity; |
| @@ -94,7 +96,7 @@ for (const [route, reads] of ROUTES) | ||
| 94 | 96 | total = Math.min(total, got.get("app")!); |
| 95 | 97 | left = Math.min(left, Math.abs(got.get("app")! - sum)); |
| 96 | 98 | } |
| 97 | expect(total).toBeLessThan(50); | |
| 99 | expectBudget(total, { ms: 50, against }); | |
| 98 | 100 | expect(left).toBeLessThanOrEqual(1.5); // each is rounded to 0.1 |
| 99 | 101 | }); |
| 100 | 102 |
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). */ | |
| 2 | 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; |
| 3 | import { baselineMs, expectBudget } from "./helpers/budget.ts"; | |
| 3 | 4 | import { mkdtempSync, rmSync } from "node:fs"; |
| 4 | 5 | import { tmpdir } from "node:os"; |
| 5 | 6 | import { join } from "node:path"; |
| @@ -71,7 +72,8 @@ afterAll(() => { | ||
| 71 | 72 | rmSync(root, { recursive: true, force: true }); |
| 72 | 73 | }); |
| 73 | 74 | |
| 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); | |
| 75 | 77 | const get = () => fetch(`${servers.url}/asked?show=all`, { headers: { cookie } }); |
| 76 | 78 | await (await get()).text(); |
| 77 | 79 | // 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 | ||
| 86 | 88 | expect(parts.find((p) => p.startsWith(`${name};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/); |
| 87 | 89 | best = Math.min(best, Number(/app;dur=([\d.]+)/.exec(parts.join(", "))![1])); |
| 88 | 90 | } |
| 89 | expect(best).toBeLessThan(50); | |
| 91 | expectBudget(best, { ms: 50, against }); | |
| 90 | 92 | }); |
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). */ | |
| 2 | 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; |
| 3 | import { baselineMs, expectBudget } from "./helpers/budget.ts"; | |
| 3 | 4 | import { mkdtempSync, rmSync } from "node:fs"; |
| 4 | 5 | import { tmpdir } from "node:os"; |
| 5 | 6 | import { join } from "node:path"; |
| @@ -52,7 +53,8 @@ const PAGES: [string, string[]][] = [ | ||
| 52 | 53 | ]; |
| 53 | 54 | |
| 54 | 55 | 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); | |
| 56 | 58 | const get = () => fetch(`${servers.url}${path}`, { headers: { cookie }, redirect: "manual" }); |
| 57 | 59 | await (await get()).text(); // warm |
| 58 | 60 | // Timers jitter on a busy machine: judge the best of several requests. |
| @@ -69,8 +71,8 @@ for (const [path, names] of PAGES) | ||
| 69 | 71 | // Phases name the whole request: what is left over is small. |
| 70 | 72 | left = Math.min(left, Math.abs(dur("app") - names.reduce((sum, n) => sum + dur(n), 0))); |
| 71 | 73 | } |
| 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. |
||
| 74 | 76 | }); |
| 75 | 77 | |
| 76 | 78 | // 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). */ | |
| 2 | 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; |
| 3 | 3 | import { mkdir, mkdtemp, rm } from "node:fs/promises"; |
| 4 | 4 | import { tmpdir } from "node:os"; |
| @@ -10,6 +10,7 @@ import { createApp as insertApp } from "../src/core/apps.ts"; | ||
| 10 | 10 | import { createIssue } from "../src/core/issues.ts"; |
| 11 | 11 | import { createUser, type User } from "../src/core/users.ts"; |
| 12 | 12 | import { countStatements } from "../bench/run.ts"; |
| 13 | import { quiet } from "./helpers/budget.ts"; | |
| 13 | 14 | import { quietLogs } from "./helpers/globals.ts"; |
| 14 | 15 | import { proveAll } from "./helpers/verified.ts"; |
| 15 | 16 | import { promote } from "./helpers/members.ts"; |
| @@ -84,7 +85,7 @@ afterAll(async () => { | ||
| 84 | 85 | |
| 85 | 86 | const query = parseExploreQuery(""); |
| 86 | 87 | |
| 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 () => { | |
| 88 | 89 | const cold: number[] = []; |
| 89 | 90 | const warm: number[] = []; |
| 90 | 91 | 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 | ||
| 96 | 97 | await explore(app, undefined, query); |
| 97 | 98 | warm.push(performance.now() - t); |
| 98 | 99 | } |
| 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); | |
| 100 | 102 | expect(Math.min(...warm) * 5).toBeLessThan(Math.min(...cold)); |
| 101 | 103 | }); |
| 102 | 104 |
test/flake.test.ts +20
| @@ -30,6 +30,7 @@ import { | ||
| 30 | 30 | ROOT_CAUSES, |
| 31 | 31 | shuffled, |
| 32 | 32 | tightBounds, |
| 33 | rawBudgets, | |
| 33 | 34 | WALL_CLOCK_FLOOR_MS, |
| 34 | 35 | } from "../scripts/flake.ts"; |
| 35 | 36 | |
| @@ -350,6 +351,25 @@ describe("no tight wall-clock bound in a test", () => { | ||
| 350 | 351 | }, 120_000); // reads every test file: past the 5 s default on a loaded machine |
| 351 | 352 | }); |
| 352 | 353 | |
| 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 | ||
| 353 | 373 | describe("the scheduled run of the files outside the gate", () => { |
| 354 | 374 | const present = ["test/a.test.ts", "test/b.test.ts", "test/c.test.ts"]; |
| 355 | 375 | 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). */ | |
| 2 | 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; |
| 3 | import { baselineMs, expectBudget } from "./helpers/budget.ts"; | |
| 3 | 4 | import { mkdtempSync, rmSync } from "node:fs"; |
| 4 | 5 | import { tmpdir } from "node:os"; |
| 5 | 6 | import { join } from "node:path"; |
| @@ -64,7 +65,8 @@ const PAGES: [path: string, who: string, names: string[]][] = [ | ||
| 64 | 65 | ]; |
| 65 | 66 | |
| 66 | 67 | 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); | |
| 68 | 70 | const cookie = await signIn(who); |
| 69 | 71 | const read = async (): Promise<string[]> => { |
| 70 | 72 | const res = await fetch(`${servers.url}${path}`, { headers: { cookie } }); |
| @@ -84,7 +86,7 @@ for (const [path, who, names] of PAGES) | ||
| 84 | 86 | expect(named).toEqual(names); |
| 85 | 87 | const total = dur(parts.find((p) => p.startsWith("app;"))); |
| 86 | 88 | 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 }); | |
| 88 | 90 | expect(sum).toBeLessThanOrEqual(total + 0.1 * names.length); |
| 89 | 91 | shares.push(total === 0 ? 1 : sum / total); |
| 90 | 92 | } |
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). */ | |
| 2 | 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; |
| 3 | import { baselineMs, expectBudget } from "./helpers/budget.ts"; | |
| 3 | 4 | import { mkdtempSync, rmSync } from "node:fs"; |
| 4 | 5 | import { tmpdir } from "node:os"; |
| 5 | 6 | import { join } from "node:path"; |
| @@ -56,12 +57,13 @@ const PAGES: [string, string[]][] = [ | ||
| 56 | 57 | ]; |
| 57 | 58 | |
| 58 | 59 | 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 () => { | |
| 60 | 61 | const get = async () => { |
| 61 | 62 | const res = await fetch(`${servers.url}${path}`, { headers: { cookie } }); |
| 62 | 63 | await res.text(); |
| 63 | 64 | return res; |
| 64 | 65 | }; |
| 66 | const against = await baselineMs(servers.url); | |
| 65 | 67 | await get(); // warm |
| 66 | 68 | const res = await get(); |
| 67 | 69 | expect(res.status).toBe(200); |
| @@ -73,7 +75,7 @@ for (const [path, names] of PAGES) | ||
| 73 | 75 | expect(Number.isFinite(dur(name))).toBe(true); |
| 74 | 76 | } |
| 75 | 77 | expect(got.filter((n) => n === "render").length).toBe(1); |
| 76 | expect(dur("app")).toBeLessThan(50); | |
| 78 | expectBudget(dur("app"), { ms: 50, against }); | |
| 77 | 79 | // The named phases cover the request (route and compress are the server's own); the issue's bound is 1 ms. |
| 78 | 80 | const sum = names.reduce((t, n) => t + dur(n), 0); |
| 79 | 81 | 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). */ | |
| 2 | 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; |
| 3 | import { baselineMs, expectBudget } from "./helpers/budget.ts"; | |
| 3 | 4 | import { mkdtempSync, rmSync } from "node:fs"; |
| 4 | 5 | import { tmpdir } from "node:os"; |
| 5 | 6 | import { join } from "node:path"; |
| @@ -51,7 +52,8 @@ const PAGES: [string, string[]][] = [ | ||
| 51 | 52 | ]; |
| 52 | 53 | |
| 53 | 54 | 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); | |
| 55 | 57 | await fetch(`${servers.url}${path}`, { headers: { cookie } }); // warm |
| 56 | 58 | const res = await fetch(`${servers.url}${path}`, { headers: { cookie } }); |
| 57 | 59 | await res.text(); |
| @@ -63,9 +65,10 @@ for (const [path, names] of PAGES) | ||
| 63 | 65 | return Number(part!.slice(`${name};dur=`.length)); |
| 64 | 66 | }; |
| 65 | 67 | 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 }); | |
| 67 | 69 | const sum = names.reduce((t, n) => t + dur(n), 0); |
| 68 | 70 | // The rest of `app` is the server's own routing and header work, outside any page (~1-2 ms on every page). |
| 69 | 71 | 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 | |
| 71 | 74 | }); |
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). */ | |
| 2 | 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; |
| 3 | import { baselineMs, expectBudget } from "./helpers/budget.ts"; | |
| 3 | 4 | import { mkdtempSync, rmSync } from "node:fs"; |
| 4 | 5 | import { tmpdir } from "node:os"; |
| 5 | 6 | import { join } from "node:path"; |
| @@ -30,7 +31,8 @@ const PAGES: [string, string[]][] = [ | ||
| 30 | 31 | ]; |
| 31 | 32 | |
| 32 | 33 | 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); | |
| 34 | 36 | await fetch(`${servers.url}${path}`); // warm |
| 35 | 37 | const res = await fetch(`${servers.url}${path}`); |
| 36 | 38 | await res.text(); |
| @@ -39,5 +41,5 @@ for (const [path, names] of PAGES) | ||
| 39 | 41 | for (const name of ["app", ...names]) |
| 40 | 42 | expect(parts.find((p) => p.startsWith(`${name};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/); |
| 41 | 43 | 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 }); | |
| 43 | 45 | }); |
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). */ | |
| 2 | 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; |
| 3 | 3 | import { snappyFixture, type SnappyFixture } from "../bench/snappy.ts"; |
| 4 | 4 | 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). */ | |
| 2 | 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; |
| 3 | import { baselineMs, expectBudget } from "./helpers/budget.ts"; | |
| 3 | 4 | import { mkdtempSync, rmSync } from "node:fs"; |
| 4 | 5 | import { tmpdir } from "node:os"; |
| 5 | 6 | import { join } from "node:path"; |
| @@ -50,7 +51,8 @@ const PAGES: [string, string[]][] = [ | ||
| 50 | 51 | ]; |
| 51 | 52 | |
| 52 | 53 | 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); | |
| 54 | 56 | const get = () => fetch(`${servers.url}${path}`, { headers: { cookie }, redirect: "manual" }); |
| 55 | 57 | await (await get()).text(); // warm |
| 56 | 58 | const res = await get(); |
| @@ -59,6 +61,6 @@ for (const [path, names] of PAGES) | ||
| 59 | 61 | for (const name of ["app", ...names]) |
| 60 | 62 | expect(parts.find((p) => p.startsWith(`${name};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/); |
| 61 | 63 | 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 }); | |
| 63 | 65 | expect(names.reduce((s, n) => s + dur(n), 0)).toBeLessThanOrEqual(dur("app") + 1); |
| 64 | 66 | }); |
new above
offline
Refers to a nightly perf lane that does not exist; either create it or say that absolute budgets are not enforced anywhere yet.