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

Every response's Server-Timing names the worst event-loop lag of the last 10 s, so a stall outside the handler shows #10292

Merged opened this

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

Merged

torque/forge:torque/10288-every-response-s-server-timing-names-the → main 3 commits fork 1 thread · 1 unresolved

  • Merged. torque/10288-every-response-s-server-timing-names-the was merged into main as 27b44da.

Review

Sign in to review this change.
82 files changed +423 −81
AGENTS.md +2
@@ -16,4 +16,6 @@ A new `/settings…` page route goes in `src/http/settings_routes.ts` (`settings
1616
1717 State shared between modules through process memory, not the database, is named here. `src/perf/views_report.ts`: `readVisits` holds each scope and window's visits for 15 seconds (dropped by `flushViews`), so a page and its probe share one read; `src/perf/views_store.ts`: the view buffer, flushed every 10 seconds. The traffic sources view's terminal door is `src/tui/screens/trafficview.ts` (palette entry for admins, per-app command for a repository's admins). `src/runs/held.ts`: each dispatcher pass (`src/crew/dispatch.ts`, `pump`) records which queued agent runs it held and why (valve, pace, limit), and vitals (`src/kernel/vitals.ts`) leaves those out of `queue:agent`. A record older than `HELD_FRESH_MS` is ignored, so a stopped dispatcher cannot hide its queue.
1818
19 `src/perf/loop_worst.ts`: `loopWorst`, the worst event-loop lag of the last 10 s in ten one-second slots, fed by one unref'd 50 ms timer that the first `serve` starts (`startLoopWorst`), and read by every response's `lag;dur` and by `/healthz` asked for JSON (`loopLagMs`). Tests listing a request's phases read `requestTiming` (`test/helpers/request-timing.ts`), which leaves `lag` out.
20
1921 `src/core/transfer.ts`: the old owner/name pairs a transfer left behind (`repo_redirect`), held per database after the first request reads them and dropped by `transferRepo`, so a request under a path nothing moved from costs no statement (#569).
docs/features/performance-watcher.md +1 −1
@@ -1,5 +1,5 @@
11 # Performance watcher (#7559)
22
3 `src/perf/` keeps a capped in-memory record of real requests per route pattern (total, each Server-Timing phase, signed in or out; no paths, ids or queries). `budgets.ts` holds the budgets in words with the number beside them. Once a minute `duty.ts` files one issue per route and phase in the forge's repo when p95 has been over budget for 15 min with 20+ real requests; the finding names the slowest phase, its share and the cache hit rate. `git.ts` keeps the same record for every git process the forge spawns (`git/env.ts`, `git/local.ts`), keyed `git <subcommand>` from a fixed list (never an argument, path or ref; anything else is `git other`), timed from before the spawn gate so a queue counts; a subcommand whose p95 passes its budget the same way (250 ms; 10 s for the pack-moving ones: clone, fetch, push, gc, repack, pack-objects and the like) files one issue naming it, its count, p50 and p95. `loop.ts` names the routes running across an event-loop stall in the `loop` log line; nothing is kept beyond that line.
3 `src/perf/` keeps a capped in-memory record of real requests per route pattern (total, each Server-Timing phase, signed in or out; no paths, ids or queries). `budgets.ts` holds the budgets in words with the number beside them. Once a minute `duty.ts` files one issue per route and phase in the forge's repo when p95 has been over budget for 15 min with 20+ real requests; the finding names the slowest phase, its share and the cache hit rate. `git.ts` keeps the same record for every git process the forge spawns (`git/env.ts`, `git/local.ts`), keyed `git <subcommand>` from a fixed list (never an argument, path or ref; anything else is `git other`), timed from before the spawn gate so a queue counts; a subcommand whose p95 passes its budget the same way (250 ms; 10 s for the pack-moving ones: clone, fetch, push, gc, repack, pack-objects and the like) files one issue naming it, its count, p50 and p95. `loop.ts` names the routes running across an event-loop stall in the `loop` log line; nothing is kept beyond that line. `loop_worst.ts` keeps the worst event-loop lag of the last 10 s (ten one-second slots fed by the 50 ms sampler) and every response's Server-Timing names it as `lag;dur`; `/healthz` answers it as `loopLagMs` to `Accept: application/json`, so a slow time to first byte with a small `app;dur` reads as a stall outside the handler.
44
55 Not yet: a synthetic probe for routes with no traffic (a client-written label would let anyone hide requests; it needs an admin-only door first), live route stats in the terminal and MCP (findings reach both doors as issues), browser timings, 24 h/7 d history, the board page, release before/after.
src/core/route_timing.ts +10
@@ -210,6 +210,16 @@ export function serverTiming(
210210 ].join(", ");
211211 }
212212
213 /**
214 * `header` with the worst event-loop lag of the last 10 s appended as
215 * `lag;dur=<ms>` (#10288): a request that waited on a loop someone else
216 * blocked shows it here, though its own `app` was fast.
217 */
218 export function withLag(header: string, lagMs: number): string {
219 if (!Number.isFinite(lagMs) || lagMs < 0) return header;
220 return `${header}, lag;dur=${lagMs.toFixed(1)}`;
221 }
222
213223 /** One named stretch of a request, in milliseconds. */
214224 export interface Phase {
215225 readonly name: string;
src/http/health.ts +8 −1
@@ -87,8 +87,13 @@ export function startDraining(log: Log, why: string, fields: Record<string, numb
8787 * Liveness: `ping` runs one trivial query against the database. However
8888 * long it takes, an answer is 200; only a throw is 503, logged when it
8989 * starts and when it ends.
90 *
91 * Given `loopLagMs` (the caller asked for JSON), the 200 is
92 * `{"ok":true,"loopLagMs":n}`: the worst event-loop lag of the last 10 s
93 * (`perf/loop_worst.ts`, #10288). Otherwise it stays the plain `ok` a
94 * probe and a stranger hear (#2917).
9095 */
91 export function liveResponse(ping: () => unknown, log: Log): Response {
96 export function liveResponse(ping: () => unknown, log: Log, loopLagMs?: number): Response {
9297 try {
9398 ping();
9499 } catch (error) {
@@ -101,6 +106,8 @@ export function liveResponse(ping: () => unknown, log: Log): Response {
101106 }
102107 if (databaseDown) log.info("forge alive again: the database answers");
103108 databaseDown = false;
109 if (loopLagMs !== undefined && Number.isFinite(loopLagMs))
110 return Response.json({ ok: true, loopLagMs: Math.round(loopLagMs * 10) / 10 }, { status: 200 });
104111 return new Response("ok", { status: 200 });
105112 }
106113
src/http/server.ts +13 −2
@@ -480,6 +480,7 @@ import {
480480 noteStart,
481481 takePhases,
482482 serverTiming,
483 withLag,
483484 takeDbTime,
484485 type RouteTiming,
485486 } from "../core/route_timing.ts";
@@ -493,6 +494,7 @@ import { appSourcesPage } from "../web/pages/sources.ts";
493494 import { createViewWriter } from "../perf/views_store.ts";
494495 import { createReadWriter, readOf } from "../perf/reads.ts";
495496 import { parseServerTiming } from "../perf/record.ts";
497 import { currentLoopLag, startLoopWorst } from "../perf/loop_worst.ts";
496498 import { methodOf, noteUsage, roleOf, surfaceOf, takeUsage } from "./usage_tag.ts";
497499 import { bearerChallenge, oauthRoutes } from "./oauth.ts";
498500 import { flushAccess, recordAccess } from "./access_log.ts";
@@ -2357,6 +2359,8 @@ export async function serve(app: App): Promise<Servers> {
23572359 const featureSchedule = startFeatureSchedule(app);
23582360 const pullSchedule = startPullSchedule(app);
23592361 const readWriter = createReadWriter(app.db, readLedger, app.log);
2362 // The worst event-loop lag of the last 10 s, on every response (#10288).
2363 startLoopWorst();
23602364 const counted = (
23612365 owner: App,
23622366 request: Request,
@@ -2368,7 +2372,8 @@ export async function serve(app: App): Promise<Servers> {
23682372 finishedStrangerDeep(owner, request);
23692373 try {
23702374 const header = serverTiming(ms, takeDbTime(request), takePhases(request));
2371 response.headers.set("server-timing", header);
2375 // `lag` is the loop's, not this request's: kept out of the record's phases.
2376 response.headers.set("server-timing", withLag(header, currentLoopLag()));
23722377 const end = performance.now();
23732378 const family = familyOf(request.headers.get("user-agent") ?? "");
23742379 perfRecord.add(
@@ -2730,7 +2735,13 @@ async function answerGet(
27302735 if (url.pathname === "/healthz") {
27312736 noteUsage(request, { pattern: "/healthz", role: "anon" });
27322737 const dbStart = performance.now();
2733 const live = liveResponse(() => app.db.query("select 1").get(), app.log);
2738 // `Accept: application/json` adds the loop's worst lag of the last 10 s (#10288).
2739 const wantsJson = (request.headers.get("accept") ?? "").includes("application/json");
2740 const live = liveResponse(
2741 () => app.db.query("select 1").get(),
2742 app.log,
2743 wantsJson ? currentLoopLag() : undefined,
2744 );
27342745 noteDbTime(request, performance.now() - dbStart);
27352746 // The release proof learns which release answered, old pod or new; a
27362747 // stranger is told only that one did (#2917).
src/perf/loop_worst.ts added +100
@@ -0,0 +1,100 @@
1 /**
2 * The worst event-loop lag of the last ten seconds (#10288), written on every
3 * response as `lag;dur=<ms>` and told by `/healthz` as `loopLagMs`. A request
4 * whose `app;dur` is 4 ms but whose time to first byte is 300 ms waited on a
5 * loop blocked by something else; this names how long that block was.
6 *
7 * Lag is the lateness `kernel/loop_lag.ts` measures: a timer asks to wake
8 * every 50 ms, and how much later than asked it wakes is how long the loop
9 * was busy. A block of B ms is read as B − (time already waited, 0–50 ms),
10 * so at 50 ms the reading undercounts a block by at most one interval.
11 *
12 * The window is a ring of ten one-second slots, each the worst lag noted in
13 * its second: `note` is O(1) (a division, a compare, two array writes) and
14 * `worst` reads ten numbers, so a tick costs well under 0.1 ms. A stall
15 * stays named for 9–10 s after the wake that saw it, then falls out.
16 */
17
18 import { LAG_EVERY_MS, startLoopLag } from "../kernel/loop_lag.ts";
19
20 /** The window `worst` covers, ms. */
21 export const LAG_WINDOW_MS = 10_000;
22 /** One slot of the ring, ms: the window's resolution. */

The sub-0.1 ms tick cost is only asserted in prose here. Add a test that bounds it (see the review body).

23 export const LAG_SLOT_MS = 1_000;
24
25 export interface LoopWorst {
26 /** A wake `lateMs` late, seen at `at` (ms, the clock `worst` reads). */
27 note(lateMs: number, at: number): void;
28 /** The worst lateness noted in the window ending at `at`, ms; 0 when none. */
29 worst(at: number): number;
30 }
31
32 export function createLoopWorst(
33 windowMs = LAG_WINDOW_MS,
34 slotMs = LAG_SLOT_MS,
35 ): LoopWorst {
36 const n = Math.max(1, Math.round(windowMs / slotMs));
37 // slot[i] is which second (at / slotMs, floored) max[i] holds; -1 is empty.
38 const slot = new Float64Array(n).fill(-1);
39 const max = new Float64Array(n);
40 return {
41 note(lateMs, at) {
42 if (!Number.isFinite(lateMs) || lateMs < 0 || !Number.isFinite(at)) return;
43 const s = Math.floor(at / slotMs);
44 const i = ((s % n) + n) % n;
45 if (slot[i] !== s) {
46 slot[i] = s;
47 max[i] = lateMs;
48 } else if (lateMs > max[i]!) max[i] = lateMs;
49 },
50 worst(at) {
51 const now = Math.floor(at / slotMs);
52 let w = 0;
53 for (let i = 0; i < n; i++)
54 if (slot[i]! > now - n && slot[i]! <= now && max[i]! > w) w = max[i]!;
55 return w;
56 },
57 };
58 }
59
60 /** The process's one window, fed by `startLoopWorst`'s timer and read by every response. */
61 export const loopWorst = createLoopWorst();
62
63 let sampling: (() => void) | null = null;
64 /** The clock the window is read on; a test injects one to step ten seconds at once. */
65 let windowClock: () => number = () => performance.now();
66
67 export interface LoopWorstOptions {
68 /** The window's clock, ms; lateness is always measured on `performance.now`. */
69 readonly clock?: () => number;
70 /** How often the timer asks to wake, ms. */
71 readonly everyMs?: number;
72 }
73
74 /**
75 * Start the one sampler feeding `loopWorst` (idempotent: a second server in
76 * the same process shares it). Its timer is unref'd, so it never holds a
77 * test or a shutdown open. Returns the stop, which also restores the clock.
78 */
79 export function startLoopWorst(options: LoopWorstOptions = {}): () => void {
80 if (sampling === null) {
81 if (options.clock !== undefined) windowClock = options.clock;
82 const lag = startLoopLag(options.everyMs ?? LAG_EVERY_MS, () => performance.now(), (late) =>
83 loopWorst.note(late, windowClock()),
84 );
85 sampling = lag.stop;
86 }
87 return stopLoopWorst;
88 }
89
90 /** Stop the sampler, if one runs, and put the window back on `performance.now`. */
91 export function stopLoopWorst(): void {
92 sampling?.();
93 sampling = null;
94 windowClock = () => performance.now();
95 }
96
97 /** The worst lag of the window ending now, ms, for a header or `/healthz`. */
98 export function currentLoopLag(): number {
99 return loopWorst.worst(windowClock());
100 }
test/account-lists-timing.test.ts +2 −1
@@ -6,6 +6,7 @@ import { join } from "node:path";
66 import { createApp, type App } from "../src/app.ts";
77 import { serve, type Servers } from "../src/http/server.ts";
88 import { seedAdmin } from "./helpers/admin.ts";
9 import { requestTiming } from "./helpers/request-timing.ts";
910
1011 let dataDir = "";
1112 let app: App, servers: Servers, bearer = "";
@@ -31,7 +32,7 @@ afterAll(async () => {
3132
3233 const basic = `Basic ${btoa("alice:correct-horse-battery")}`;
3334 const phases = (res: Response) =>
34 (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!).filter((n) => n && n !== "app");
35 requestTiming(res).split(", ").map((p) => p.split(";")[0]!).filter((n) => n && n !== "app");
3536
3637 for (const [path, read] of [
3738 ["/api/v1/tokens", "tokens"],
test/admin-lists-timing.test.ts +2 −1
@@ -6,6 +6,7 @@ import { join } from "node:path";
66 import { createApp, type App } from "../src/app.ts";
77 import { serve, type Servers } from "../src/http/server.ts";
88 import { seedAdmin } from "./helpers/admin.ts";
9 import { requestTiming } from "./helpers/request-timing.ts";
910
1011 let dataDir = "";
1112 let app: App, servers: Servers, bearer = "";
@@ -30,7 +31,7 @@ afterAll(async () => {
3031 });
3132
3233 const names = (res: Response) =>
33 (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!).filter(Boolean);
34 requestTiming(res).split(", ").map((p) => p.split(";")[0]!).filter(Boolean);
3435
3536 for (const [path, read] of [["accounts", "accounts"], ["people", "people"], ["invites", "invites"], ["teams", "teams"]] as const) {
3637 test(`GET /admin/${path} names session, ${read}, render in order`, async () => {
test/api-agent-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -49,7 +50,7 @@ afterAll(async () => {
4950 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5051 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5152 const body = await res.text();
52 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
53 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5354 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5455 }
5556
test/api-agents-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -49,7 +50,7 @@ afterAll(async () => {
4950 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5051 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5152 const body = await res.text();
52 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
53 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5354 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5455 }
5556
test/api-app-open-timing.test.ts +2 −1
@@ -11,6 +11,7 @@ import { createSession } from "../src/core/sessions.ts";
1111 import { createUser } from "../src/core/users.ts";
1212 import { serve, type Servers } from "../src/http/server.ts";
1313 import { quietLogs } from "./helpers/globals.ts";
14 import { requestTiming } from "./helpers/request-timing.ts";
1415
1516 quietLogs();
1617
@@ -61,7 +62,7 @@ afterAll(async () => {
6162 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
6263 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
6364 const body = await res.text();
64 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
65 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
6566 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
6667 }
6768
test/api-apps-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -49,7 +50,7 @@ afterAll(async () => {
4950 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5051 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5152 const body = await res.text();
52 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
53 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5354 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5455 }
5556
test/api-asked-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -49,7 +50,7 @@ afterAll(async () => {
4950 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5051 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5152 const body = await res.text();
52 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
53 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5354 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5455 }
5556
test/api-assigned-timing.test.ts +2 −1
@@ -8,6 +8,7 @@ import { createSession } from "../src/core/sessions.ts";
88 import { createUser } from "../src/core/users.ts";
99 import { serve, type Servers } from "../src/http/server.ts";
1010 import { quietLogs } from "./helpers/globals.ts";
11 import { requestTiming } from "./helpers/request-timing.ts";
1112
1213 quietLogs();
1314
@@ -46,7 +47,7 @@ afterAll(async () => {
4647 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
4748 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
4849 const body = await res.text();
49 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
50 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5051 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5152 }
5253
test/api-boards-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -43,7 +44,7 @@ afterAll(async () => {
4344 async function phases(path: string, withCookie = true): Promise<{ status: number; names: string[]; body: string }> {
4445 const res = await fetch(`${servers.url}${path}`, { headers: withCookie ? { cookie } : {} });
4546 const body = await res.text();
46 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
47 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
4748 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
4849 }
4950
test/api-branches-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -49,7 +50,7 @@ afterAll(async () => {
4950 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5051 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5152 const body = await res.text();
52 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
53 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5354 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5455 }
5556
test/api-ccheck-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -49,7 +50,7 @@ afterAll(async () => {
4950 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5051 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5152 const body = await res.text();
52 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
53 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5354 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5455 }
5556
test/api-comments-timing.test.ts +2 −1
@@ -10,6 +10,7 @@ import { createSession } from "../src/core/sessions.ts";
1010 import { createUser } from "../src/core/users.ts";
1111 import { serve, type Servers } from "../src/http/server.ts";
1212 import { quietLogs } from "./helpers/globals.ts";
13 import { requestTiming } from "./helpers/request-timing.ts";
1314
1415 quietLogs();
1516
@@ -50,7 +51,7 @@ afterAll(async () => {
5051 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5152 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5253 const body = await res.text();
53 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
54 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5455 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5556 }
5657
test/api-commit-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -57,7 +58,7 @@ afterAll(async () => {
5758 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5859 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5960 const body = await res.text();
60 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
61 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
6162 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
6263 }
6364
test/api-commits-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -54,7 +55,7 @@ afterAll(async () => {
5455 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5556 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5657 const body = await res.text();
57 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
58 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5859 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5960 }
6061
test/api-crew-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -49,7 +50,7 @@ afterAll(async () => {
4950 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5051 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5152 const body = await res.text();
52 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
53 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5354 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5455 }
5556
test/api-decisions-timing.test.ts +2 −1
@@ -11,6 +11,7 @@ import { createSession } from "../src/core/sessions.ts";
1111 import { createUser, getUserById } from "../src/core/users.ts";
1212 import { serve, type Servers } from "../src/http/server.ts";
1313 import { quietLogs } from "./helpers/globals.ts";
14 import { requestTiming } from "./helpers/request-timing.ts";
1415
1516 quietLogs();
1617
@@ -51,7 +52,7 @@ afterAll(async () => {
5152 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5253 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5354 const body = await res.text();
54 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
55 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5556 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5657 }
5758
test/api-deps-timing.test.ts +2 −1
@@ -10,6 +10,7 @@ import { createSession } from "../src/core/sessions.ts";
1010 import { createUser } from "../src/core/users.ts";
1111 import { serve, type Servers } from "../src/http/server.ts";
1212 import { quietLogs } from "./helpers/globals.ts";
13 import { requestTiming } from "./helpers/request-timing.ts";
1314
1415 quietLogs();
1516
@@ -52,7 +53,7 @@ afterAll(async () => {
5253 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5354 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5455 const body = await res.text();
55 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
56 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5657 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5758 }
5859
test/api-egress-grants-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -65,7 +66,7 @@ async function phases(
6566 ): Promise<{ status: number; names: string[]; body: string }> {
6667 const res = await fetch(`${servers.url}${path}`, { headers });
6768 const body = await res.text();
68 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
69 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
6970 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
7071 }
7172
test/api-explore-timing.test.ts +2 −1
@@ -8,6 +8,7 @@ import { createSession } from "../src/core/sessions.ts";
88 import { createUser } from "../src/core/users.ts";
99 import { serve, type Servers } from "../src/http/server.ts";
1010 import { quietLogs } from "./helpers/globals.ts";
11 import { requestTiming } from "./helpers/request-timing.ts";
1112
1213 quietLogs();
1314
@@ -41,7 +42,7 @@ afterAll(async () => {
4142 test("the API explore read names session, explore and render in order", async () => {
4243 const res = await fetch(`${servers.url}/api/v1/explore`, { headers: { cookie } });
4344 const body = JSON.parse(await res.text());
44 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
45 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
4546 expect(res.status).toBe(200);
4647 expect(names.slice(names.indexOf("app") + 1)).toEqual(["session", "explore", "render"]);
4748 expect(Array.isArray(body.items)).toBe(true);
test/api-family-timing.test.ts +2 −1
@@ -8,6 +8,7 @@ import { createSession } from "../src/core/sessions.ts";
88 import { createUser } from "../src/core/users.ts";
99 import { serve, type Servers } from "../src/http/server.ts";
1010 import { quietLogs } from "./helpers/globals.ts";
11 import { requestTiming } from "./helpers/request-timing.ts";
1112
1213 quietLogs();
1314
@@ -45,7 +46,7 @@ afterAll(async () => {
4546 async function phases(path: string, withCookie = true) {
4647 const res = await fetch(`${servers.url}${path}`, { headers: withCookie ? { cookie } : {} });
4748 const body = await res.text();
48 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
49 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
4950 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5051 }
5152
test/api-flow-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -49,7 +50,7 @@ afterAll(async () => {
4950 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5051 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5152 const body = await res.text();
52 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
53 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5354 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5455 }
5556
test/api-following-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -56,7 +57,7 @@ afterAll(async () => {
5657 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5758 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5859 const body = await res.text();
59 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
60 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
6061 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
6162 }
6263
test/api-keys-timing.test.ts +2 −1
@@ -8,6 +8,7 @@ import { createSession } from "../src/core/sessions.ts";
88 import { createUser } from "../src/core/users.ts";
99 import { serve, type Servers } from "../src/http/server.ts";
1010 import { quietLogs } from "./helpers/globals.ts";
11 import { requestTiming } from "./helpers/request-timing.ts";
1112
1213 quietLogs();
1314
@@ -46,7 +47,7 @@ afterAll(async () => {
4647 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
4748 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
4849 const body = await res.text();
49 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
50 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5051 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5152 }
5253
test/api-me-timing.test.ts +3 −2
@@ -8,6 +8,7 @@ import { createSession } from "../src/core/sessions.ts";
88 import { createUser } from "../src/core/users.ts";
99 import { serve, type Servers } from "../src/http/server.ts";
1010 import { quietLogs } from "./helpers/globals.ts";
11 import { requestTiming } from "./helpers/request-timing.ts";
1112
1213 quietLogs();
1314
@@ -46,7 +47,7 @@ afterAll(async () => {
4647 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
4748 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
4849 const body = await res.text();
49 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
50 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5051 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5152 }
5253
@@ -60,6 +61,6 @@ test("the profile read names session, user, render in order", async () => {
6061 test("an anonymous read is a 401 naming session", async () => {
6162 const res = await fetch(`${servers.url}/api/v1/user`);
6263 expect(res.status).toBe(401);
63 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
64 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
6465 expect(names.slice(names.indexOf("app") + 1)).toEqual(["session"]);
6566 });
test/api-milestones-timing.test.ts +2 −1
@@ -11,6 +11,7 @@ import { createSession } from "../src/core/sessions.ts";
1111 import { createUser } from "../src/core/users.ts";
1212 import { serve, type Servers } from "../src/http/server.ts";
1313 import { quietLogs } from "./helpers/globals.ts";
14 import { requestTiming } from "./helpers/request-timing.ts";
1415
1516 quietLogs();
1617
@@ -63,7 +64,7 @@ afterAll(async () => {
6364 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
6465 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
6566 const body = await res.text();
66 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
67 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
6768 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
6869 }
6970
test/api-origin-repos-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { newFakeState, serveFakeForge, type FakeForge } from "./fake-origin.ts";
99 import { seedAdmin } from "./helpers/admin.ts";
1010 import { quietLogs } from "./helpers/globals.ts";
1111 import { proveAll } from "./helpers/verified.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -54,7 +55,7 @@ afterAll(async () => {
5455 async function get(path: string): Promise<{ status: number; names: string[]; body: string }> {
5556 const res = await fetch(`${servers.url}${path}`, { headers: { authorization: auth } });
5657 const body = await res.text();
57 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
58 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5859 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5960 }
6061
test/api-panel-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createToken } from "../src/core/tokens.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -51,7 +52,7 @@ afterAll(async () => {
5152 async function phases(path: string): Promise<{ status: number; names: string[]; res: Response }> {
5253 const res = await fetch(`${servers.url}${path}`, { headers: { authorization: `Bearer ${token}` } });
5354 await res.text();
54 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
55 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5556 return { status: res.status, names: names.slice(names.indexOf("app") + 1), res };
5657 }
5758
test/api-pchecks-timing.test.ts +2 −1
@@ -10,6 +10,7 @@ import { createSession } from "../src/core/sessions.ts";
1010 import { createUser } from "../src/core/users.ts";
1111 import { serve, type Servers } from "../src/http/server.ts";
1212 import { quietLogs } from "./helpers/globals.ts";
13 import { requestTiming } from "./helpers/request-timing.ts";
1314
1415 quietLogs();
1516
@@ -67,7 +68,7 @@ afterAll(async () => {
6768 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
6869 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
6970 const body = await res.text();
70 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
71 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
7172 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
7273 }
7374
test/api-person-timing.test.ts +2 −1
@@ -8,6 +8,7 @@ import { createSession } from "../src/core/sessions.ts";
88 import { createUser } from "../src/core/users.ts";
99 import { serve, type Servers } from "../src/http/server.ts";
1010 import { quietLogs } from "./helpers/globals.ts";
11 import { requestTiming } from "./helpers/request-timing.ts";
1112
1213 quietLogs();
1314
@@ -46,7 +47,7 @@ afterAll(async () => {
4647 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
4748 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
4849 const body = await res.text();
49 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
50 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5051 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5152 }
5253
test/api-personrepos-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -49,7 +50,7 @@ afterAll(async () => {
4950 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5051 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5152 const body = await res.text();
52 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
53 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5354 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5455 }
5556
test/api-pins-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -49,7 +50,7 @@ afterAll(async () => {
4950 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5051 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5152 const body = await res.text();
52 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
53 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5354 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5455 }
5556
test/api-previews-timing.test.ts +2 −1
@@ -10,6 +10,7 @@ import { createSession } from "../src/core/sessions.ts";
1010 import { createUser } from "../src/core/users.ts";
1111 import { serve, type Servers } from "../src/http/server.ts";
1212 import { quietLogs } from "./helpers/globals.ts";
13 import { requestTiming } from "./helpers/request-timing.ts";
1314
1415 quietLogs();
1516
@@ -67,7 +68,7 @@ afterAll(async () => {
6768 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
6869 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
6970 const body = await res.text();
70 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
71 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
7172 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
7273 }
7374
test/api-prompts-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -49,7 +50,7 @@ afterAll(async () => {
4950 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5051 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5152 const body = await res.text();
52 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
53 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5354 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5455 }
5556
test/api-publicrepos-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -49,7 +50,7 @@ afterAll(async () => {
4950 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5051 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5152 const body = await res.text();
52 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
53 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5354 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5455 }
5556
test/api-pull-timing.test.ts +2 −1
@@ -10,6 +10,7 @@ import { createSession } from "../src/core/sessions.ts";
1010 import { createUser } from "../src/core/users.ts";
1111 import { serve, type Servers } from "../src/http/server.ts";
1212 import { quietLogs } from "./helpers/globals.ts";
13 import { requestTiming } from "./helpers/request-timing.ts";
1314
1415 quietLogs();
1516
@@ -67,7 +68,7 @@ afterAll(async () => {
6768 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
6869 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
6970 const body = await res.text();
70 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
71 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
7172 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
7273 }
7374
test/api-pulls-timing.test.ts +2 −1
@@ -13,6 +13,7 @@ import { createUser, type User } from "../src/core/users.ts";
1313 import { serve, type Servers } from "../src/http/server.ts";
1414 import { expectWork, requestStatements } from "./helpers/work.ts";
1515 import { quietLogs } from "./helpers/globals.ts";
16 import { requestTiming } from "./helpers/request-timing.ts";
1617
1718 quietLogs();
1819
@@ -76,7 +77,7 @@ afterAll(async () => {
7677 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
7778 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
7879 const body = await res.text();
79 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
80 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
8081 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
8182 }
8283
test/api-reports-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -49,7 +50,7 @@ afterAll(async () => {
4950 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5051 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5152 const body = await res.text();
52 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
53 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5354 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5455 }
5556
test/api-returns-timing.test.ts +2 −1
@@ -8,6 +8,7 @@ import { createSession } from "../src/core/sessions.ts";
88 import { createUser } from "../src/core/users.ts";
99 import { serve, type Servers } from "../src/http/server.ts";
1010 import { quietLogs } from "./helpers/globals.ts";
11 import { requestTiming } from "./helpers/request-timing.ts";
1112
1213 quietLogs();
1314
@@ -46,7 +47,7 @@ afterAll(async () => {
4647 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
4748 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
4849 const body = await res.text();
49 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
50 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5051 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5152 }
5253
test/api-rstates-timing.test.ts +2 −1
@@ -10,6 +10,7 @@ import { createSession } from "../src/core/sessions.ts";
1010 import { createUser } from "../src/core/users.ts";
1111 import { serve, type Servers } from "../src/http/server.ts";
1212 import { quietLogs } from "./helpers/globals.ts";
13 import { requestTiming } from "./helpers/request-timing.ts";
1314
1415 quietLogs();
1516
@@ -67,7 +68,7 @@ afterAll(async () => {
6768 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
6869 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
6970 const body = await res.text();
70 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
71 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
7172 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
7273 }
7374
test/api-runs-timing.test.ts +2 −1
@@ -10,6 +10,7 @@ import { createSession } from "../src/core/sessions.ts";
1010 import { createUser } from "../src/core/users.ts";
1111 import { serve, type Servers } from "../src/http/server.ts";
1212 import { quietLogs } from "./helpers/globals.ts";
13 import { requestTiming } from "./helpers/request-timing.ts";
1314
1415 quietLogs();
1516
@@ -52,7 +53,7 @@ afterAll(async () => {
5253 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5354 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5455 const body = await res.text();
55 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
56 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5657 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5758 }
5859
test/api-scorecard-timing.test.ts +2 −1
@@ -8,6 +8,7 @@ import { createSession } from "../src/core/sessions.ts";
88 import { createUser } from "../src/core/users.ts";
99 import { serve, type Servers } from "../src/http/server.ts";
1010 import { quietLogs } from "./helpers/globals.ts";
11 import { requestTiming } from "./helpers/request-timing.ts";
1112
1213 quietLogs();
1314
@@ -46,7 +47,7 @@ afterAll(async () => {
4647 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
4748 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
4849 const body = await res.text();
49 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
50 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5051 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5152 }
5253
test/api-search-timing.test.ts +2 −1
@@ -6,6 +6,7 @@ import { join } from "node:path";
66 import { createApp, type App } from "../src/app.ts";
77 import { serve, type Servers } from "../src/http/server.ts";
88 import { quietLogs } from "./helpers/globals.ts";
9 import { requestTiming } from "./helpers/request-timing.ts";
910
1011 quietLogs();
1112
@@ -32,7 +33,7 @@ afterAll(async () => {
3233 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
3334 const res = await fetch(`${servers.url}${path}`);
3435 const body = await res.text();
35 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
36 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
3637 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
3738 }
3839
test/api-tags-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -54,7 +55,7 @@ afterAll(async () => {
5455 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5556 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5657 const body = await res.text();
57 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
58 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5859 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5960 }
6061
test/api-threads-timing.test.ts +2 −1
@@ -10,6 +10,7 @@ import { createSession } from "../src/core/sessions.ts";
1010 import { createUser } from "../src/core/users.ts";
1111 import { serve, type Servers } from "../src/http/server.ts";
1212 import { quietLogs } from "./helpers/globals.ts";
13 import { requestTiming } from "./helpers/request-timing.ts";
1314
1415 quietLogs();
1516
@@ -67,7 +68,7 @@ afterAll(async () => {
6768 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
6869 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
6970 const body = await res.text();
70 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
71 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
7172 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
7273 }
7374
test/api-upstream-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -49,7 +50,7 @@ afterAll(async () => {
4950 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5051 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5152 const body = await res.text();
52 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
53 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5354 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5455 }
5556
test/api-usage-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -49,7 +50,7 @@ afterAll(async () => {
4950 async function phases(path: string): Promise<{ status: number; names: string[]; body: string }> {
5051 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
5152 const body = await res.text();
52 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
53 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5354 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
5455 }
5556
test/api-waves.test.ts +2 −1
@@ -12,6 +12,7 @@ import { serve, type Servers } from "../src/http/server.ts";
1212 import { writeWave, type Wave } from "../src/story/wave.ts";
1313 import { quietLogs } from "./helpers/globals.ts";
1414 import { mergedPull, waveNaming } from "./helpers/wave-pulls.ts";
15 import { requestTiming } from "./helpers/request-timing.ts";
1516
1617 quietLogs();
1718
@@ -113,7 +114,7 @@ async function get(path: string, withCookie: boolean | string = true) {
113114 headers: withCookie === false ? {} : { cookie: withCookie === true ? cookie : withCookie },
114115 });
115116 const body = await res.text();
116 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
117 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
117118 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
118119 }
119120
test/app-read-timing.test.ts +2 −1
@@ -8,6 +8,7 @@ import { AppsRuntime } from "../src/apps/runtime.ts";
88 import { serve, type Servers } from "../src/http/server.ts";
99 import { seedAdmin } from "./helpers/admin.ts";
1010 import { tokenFor } from "./helpers/tokens.ts";
11 import { requestTiming } from "./helpers/request-timing.ts";
1112
1213 let dataDir = "";
1314 let app: App, servers: Servers, bearer = "";
@@ -21,7 +22,7 @@ const call = (path: string, init: RequestInit = {}, token = bearer) =>
2122 async function phases(path: string, token = bearer): Promise<{ status: number; names: string[] }> {
2223 const res = await call(path, {}, token);
2324 await res.arrayBuffer();
24 const all = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
25 const all = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
2526 // The server's own leading `app` (and `db`) entries are not route phases.
2627 return { status: res.status, names: all.slice(all.indexOf("app") + 1) };
2728 }
test/conflicts-timing.test.ts +2 −1
@@ -10,6 +10,7 @@ import { createSession } from "../src/core/sessions.ts";
1010 import { createUser } from "../src/core/users.ts";
1111 import { serve, type Servers } from "../src/http/server.ts";
1212 import { quietLogs } from "./helpers/globals.ts";
13 import { requestTiming } from "./helpers/request-timing.ts";
1314
1415 quietLogs();
1516
@@ -71,7 +72,7 @@ test("a conflicted pull's resolve page names its phases after app, in order", as
7172 const res = await fetch(`${servers.url}/alice/r/pulls/${number}/resolve`, { headers: { cookie } });
7273 expect(res.status).toBe(200);
7374 expect(await res.text()).toContain("Block 1 of 1");
74 const parts = (res.headers.get("server-timing") ?? "").split(", ");
75 const parts = requestTiming(res).split(", ");
7576 const names = parts.map((p) => p.split(";")[0]!);
7677 expect(names.slice(names.indexOf("app") + 1)).toEqual([
7778 "session", "pull", "checkout", "status", "file", "crew", "render",
test/fleet-timing.test.ts +2 −1
@@ -6,6 +6,7 @@ import { join } from "node:path";
66 import { createApp, type App } from "../src/app.ts";
77 import { serve, type Servers } from "../src/http/server.ts";
88 import { seedAdmin } from "./helpers/admin.ts";
9 import { requestTiming } from "./helpers/request-timing.ts";
910
1011 let dataDir = "";
1112 let app: App, servers: Servers, bearer = "";
@@ -32,7 +33,7 @@ afterAll(async () => {
3233 async function phases(path: string): Promise<{ status: number; names: string[] }> {
3334 const res = await fetch(`${servers.url}${path}`, { headers: { authorization: `Bearer ${bearer}` } });
3435 await res.arrayBuffer();
35 const names = (res.headers.get("server-timing") ?? "").split(", ").filter(Boolean).filter((p) => !p.startsWith("app;")).map((p) => p.split(";")[0]!);
36 const names = requestTiming(res).split(", ").filter(Boolean).filter((p) => !p.startsWith("app;")).map((p) => p.split(";")[0]!);
3637 return { status: res.status, names };
3738 }
3839
test/forks-api-drift.test.ts +2 −1
@@ -8,6 +8,7 @@ import { createApp, type App } from "../src/app.ts";
88 import { createUser } from "../src/core/users.ts";
99 import { serve, type Servers } from "../src/http/server.ts";
1010 import { promote } from "./helpers/members.ts";
11 import { requestTiming } from "./helpers/request-timing.ts";
1112
1213 let dataDir = "";
1314 let app: App, servers: Servers;
@@ -48,7 +49,7 @@ test("a fork one commit ahead reads ahead 1, behind 0, with the parent named", a
4849 expect(page.items[0]!.updatedAt).not.toBeNull();
4950 expect(page.next_cursor).toBeNull();
5051 expect(page.parent).toBeNull(); // the source itself has no parent
51 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!).filter((n) => n !== "app");
52 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!).filter((n) => n !== "app");
5253 expect(names).toEqual(["session", "forks", "drift", "render"]);
5354 const down = await fetch(`${servers.url}/api/v1/repos/bob/app/forks`);
5455 expect(((await down.json()) as { parent: string | null }).parent).toBe("alice/app");
test/forks-api-timing.test.ts +2 −1
@@ -6,6 +6,7 @@ import { join } from "node:path";
66 import { createApp, type App } from "../src/app.ts";
77 import { serve, type Servers } from "../src/http/server.ts";
88 import { seedAdmin } from "./helpers/admin.ts";
9 import { requestTiming } from "./helpers/request-timing.ts";
910
1011 let dataDir = "";
1112 let app: App, servers: Servers, bearer = "";
@@ -30,7 +31,7 @@ afterAll(async () => {
3031 });
3132
3233 const names = (res: Response) =>
33 (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!).filter((n) => n !== "app");
34 requestTiming(res).split(", ").map((p) => p.split(";")[0]!).filter((n) => n !== "app");
3435
3536 test("the forks list names session, forks, render in order", async () => {
3637 const made = await fetch(`${servers.url}/api/v1/repos`, {
test/forks-timing.test.ts +2 −1
@@ -7,6 +7,7 @@ import { createApp, type App } from "../src/app.ts";
77 import { createRepo } from "../src/actions/repos.ts";
88 import { serve, type Servers } from "../src/http/server.ts";
99 import { seedAdmin } from "./helpers/admin.ts";
10 import { requestTiming } from "./helpers/request-timing.ts";
1011
1112 let dataDir = "";
1213 let app: App, servers: Servers, bearer = "";
@@ -34,7 +35,7 @@ afterAll(async () => {
3435 });
3536
3637 const names = (res: Response): string[] =>
37 (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!).filter((n) => n !== "app");
38 requestTiming(res).split(", ").map((p) => p.split(";")[0]!).filter((n) => n !== "app");
3839
3940 test("the forks page names session, repository, forks, fork-tree, lineage, drift and render, in order", async () => {
4041 const res = await fetch(`${servers.url}/alice/notes/forks`, { headers: { authorization: `Bearer ${bearer}` } });
test/front-api-timing.test.ts +2 −1
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
55 import { join } from "node:path";
66 import { createApp, type App } from "../src/app.ts";
77 import { serve, type Servers } from "../src/http/server.ts";
8 import { requestTiming } from "./helpers/request-timing.ts";
89
910 let dataDir = "";
1011 let app: App, servers: Servers;
@@ -25,7 +26,7 @@ test("the front read names session, front, render in order", async () => {
2526 const res = await fetch(`${servers.url}/api/v1/front`);
2627 await res.arrayBuffer();
2728 expect(res.status).toBe(200);
28 const parts = (res.headers.get("server-timing") ?? "").split(", ");
29 const parts = requestTiming(res).split(", ");
2930 const names = parts.map((p) => p.split(";")[0]!).filter((n) => n !== "app");
3031 expect(names).toEqual(["session", "front", "render"]);
3132 for (const p of parts) expect(Number(/dur=([\d.]+)/.exec(p)?.[1])).not.toBeNaN();
test/front-timing.test.ts +2 −1
@@ -6,6 +6,7 @@ import { join } from "node:path";
66 import { createApp, type App } from "../src/app.ts";
77 import { serve, type Servers } from "../src/http/server.ts";
88 import { quietLogs } from "./helpers/globals.ts";
9 import { requestTiming } from "./helpers/request-timing.ts";
910
1011 quietLogs();
1112
@@ -30,7 +31,7 @@ async function read(): Promise<string[]> {
3031 const res = await fetch(`${servers.url}/`, { redirect: "manual" });
3132 expect(res.status).toBe(200);
3233 await res.text();
33 return (res.headers.get("server-timing") ?? "").split(", ");
34 return requestTiming(res).split(", ");
3435 }
3536
3637 test("GET / signed out names session, key, shelf, tiles, render in order and covers the handler's share of app", async () => {
test/helpers/request-timing.ts added +11
@@ -0,0 +1,11 @@
1 /**
2 * A response's Server-Timing without the loop's `lag` token (#10288): the
3 * phases the request itself named, for a test that lists them in order.
4 * `lag` is the worst event-loop lag of the last 10 s, not this request's.
5 */
6 export function requestTiming(res: { headers: Headers }): string {
7 return (res.headers.get("server-timing") ?? "")
8 .split(", ")
9 .filter((p) => !p.startsWith("lag;"))
10 .join(", ");
11 }
test/here-timing.test.ts +2 −1
@@ -6,6 +6,7 @@ import { join } from "node:path";
66 import { createApp, type App } from "../src/app.ts";
77 import { serve, type Servers } from "../src/http/server.ts";
88 import { seedAdmin } from "./helpers/admin.ts";
9 import { requestTiming } from "./helpers/request-timing.ts";
910
1011 let dataDir = "";
1112 let app: App, servers: Servers, bearer = "";
@@ -30,7 +31,7 @@ afterAll(async () => {
3031 });
3132
3233 const phases = (res: Response) =>
33 (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!).filter((n) => n !== "app");
34 requestTiming(res).split(", ").map((p) => p.split(";")[0]!).filter((n) => n !== "app");
3435
3536 test("the here read names session, here, render in that order", async () => {
3637 const res = await fetch(`${servers.url}/api/v1/here?path=/`, { headers: { authorization: `Bearer ${bearer}` } });
test/home-200.test.ts +2 −1
@@ -5,6 +5,7 @@ import { assignIssue, openIssue } from "../src/actions/issues.ts";
55 import { expectWork } from "./helpers/work.ts";
66 import { statements } from "./helpers/statements.ts";
77 import { snappyFixture, type SnappyFixture } from "../bench/snappy.ts";
8 import { requestTiming } from "./helpers/request-timing.ts";
89
910 let f: SnappyFixture;
1011
@@ -25,7 +26,7 @@ afterAll(async () => {
2526 async function look(): Promise<{ phases: { name: string; ms: number }[] }> {
2627 const res = await fetch(`${f.base}/`, { headers: { cookie: f.ownerCookie } });
2728 await res.arrayBuffer();
28 const phases = (res.headers.get("server-timing") ?? "")
29 const phases = requestTiming(res)
2930 .split(", ")
3031 .map((p) => ({ name: p.split(";")[0]!, ms: Number(/dur=([\d.]+)/.exec(p)?.[1] ?? 0) }));
3132 return { phases };
test/issues-list-timing.test.ts +2 −1
@@ -1,6 +1,7 @@
11 /** A repository's issues list names its Server-Timing phases in order: session, issues, closing, words, labels, render. */
22 import { afterAll, beforeAll, expect, test } from "bun:test";
33 import { snappyFixture, type SnappyFixture } from "../bench/snappy.ts";
4 import { requestTiming } from "./helpers/request-timing.ts";
45
56 let f: SnappyFixture;
67
@@ -19,7 +20,7 @@ for (const who of ["owner", "anonymous"] as const) {
1920 });
2021 await res.arrayBuffer();
2122 expect(res.status).toBe(200);
22 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
23 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
2324 expect(names.filter((n) => n !== "app")).toEqual(["session", "issues", "closing", "words", "labels", "render"]);
2425 });
2526 }
test/loop-worst.test.ts added +128
@@ -0,0 +1,128 @@
1 /**
2 * The worst event-loop lag of the last 10 s (#10288): the window by hand,
3 * then a real 150 ms block seen as `lag;dur` on the next response and as
4 * `loopLagMs` on `/healthz`, and gone once the injected clock steps 10 s.
5 */
6
7 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
8 import { mkdtemp, rm } from "node:fs/promises";
9 import { tmpdir } from "node:os";
10 import { join } from "node:path";
11 import { createApp, type App } from "../src/app.ts";
12 import { withLag } from "../src/core/route_timing.ts";
13 import { serve, type Servers } from "../src/http/server.ts";
14 import { createLoopWorst, startLoopWorst, stopLoopWorst } from "../src/perf/loop_worst.ts";
15
16 function blockFor(ms: number): void {
17 const until = performance.now() + ms;
18 while (performance.now() < until) {
19 // a synchronous job holding the thread, as the lab's stalls do
20 }
21 }
22
23 /** `lag;dur=<n>` out of a Server-Timing header, or null. */
24 function lagOf(header: string | null): number | null {
25 const m = /(?:^|,\s*)lag;dur=([\d.]+)/.exec(header ?? "");
26 return m === null ? null : Number(m[1]);
27 }
28
29 describe("the window, by hand", () => {
30 test("keeps the worst of the last ten one-second slots", () => {
31 const w = createLoopWorst();
32 expect(w.worst(0)).toBe(0);
33 w.note(3, 100);
34 w.note(150, 2_500);
35 w.note(7, 2_900);
36 w.note(12, 9_000);
37 expect(w.worst(9_999)).toBe(150);
38 // Second 2 leaves the window when second 12 begins: slots 3..12 remain.
39 expect(w.worst(11_999)).toBe(150);
40 expect(w.worst(12_000)).toBe(12);
41 expect(w.worst(18_999)).toBe(12);
42 expect(w.worst(19_000)).toBe(0);
43 });
44
45 test("a slot reused a lap later forgets what it held", () => {
46 const w = createLoopWorst();
47 w.note(300, 1_000);
48 w.note(2, 11_000);
49 expect(w.worst(11_000)).toBe(2);
50 });
51
52 test("refuses what is not a lateness", () => {
53 const w = createLoopWorst();
54 for (const bad of [-1, Number.NaN, Number.POSITIVE_INFINITY]) w.note(bad, 0);
55 w.note(5, Number.NaN);
56 expect(w.worst(0)).toBe(0);
57 });
58
59 test("a tick (note then worst) costs under 0.1 ms on average over 100k", () => {
60 const w = createLoopWorst();
61 const ticks = 100_000;
62 let sink = 0;
63 const t0 = performance.now();
64 // Each tick: one wake 50 ms apart on the window clock, then one header read.
65 for (let k = 0; k < ticks; k++) {
66 w.note(k % 97, k * 50);
67 sink += w.worst(k * 50);
68 }
69 const meanMs = (performance.now() - t0) / ticks;
70 expect(sink).toBeGreaterThan(0);
71 expect(meanMs).toBeLessThan(0.1);
72 });
73
74 test("withLag appends one token and ignores a non-number", () => {
75 expect(withLag("app;dur=3.0", 148.26)).toBe("app;dur=3.0, lag;dur=148.3");
76 expect(withLag("app;dur=3.0", Number.NaN)).toBe("app;dur=3.0");
77 });
78 });
79
80 describe("on every response", () => {
81 let dataDir: string;
82 let app: App;
83 let servers: Servers;
84 let skew = 0;
85
86 beforeAll(async () => {
87 dataDir = await mkdtemp(join(tmpdir(), "forge-loop-worst-"));
88 app = createApp({ dataDir, port: 0 });
89 // Wake every 5 ms, so a 150 ms block reads as at least 145; the window
90 // reads a clock the test can step.
91 stopLoopWorst();
92 startLoopWorst({ clock: () => performance.now() + skew, everyMs: 5 });
93 servers = await serve(app);
94 });
95
96 afterAll(async () => {
97 stopLoopWorst();
98 servers.stop();
99 app.close();
100 await rm(dataDir, { recursive: true, force: true });
101 });
102
103 test("a 150 ms block shows as lag;dur ≥ 140 on the next response, then falls out", async () => {
104 blockFor(150);
105 // The overdue wake runs before this one: timers fire in due order.
106 await new Promise((r) => setTimeout(r, 0));
107 const res = await fetch(`${servers.url}/nope`);
108 await res.text();
109 const header = res.headers.get("server-timing");
110 expect(header).toContain("app;dur=");
111 expect(lagOf(header)).toBeGreaterThanOrEqual(140);
112
113 const health = await fetch(`${servers.url}/healthz`, { headers: { accept: "application/json" } });
114 const body = (await health.json()) as { ok: boolean; loopLagMs: number };
115 expect(body.ok).toBe(true);
116 expect(body.loopLagMs).toBeGreaterThanOrEqual(140);
117 // A probe asking for nothing still hears the plain `ok` (#2917).
118 expect(await (await fetch(`${servers.url}/healthz`)).text()).toBe("ok");
119
120 // Ten seconds on the window's clock with no new wake: nothing is left in it.
121 stopLoopWorst();
122 startLoopWorst({ clock: () => performance.now() + skew, everyMs: 60_000 });
123 skew += 10_000;
124 const later = await fetch(`${servers.url}/nope`);
125 await later.text();
126 expect(lagOf(later.headers.get("server-timing"))).toBe(0);
127 });
128 });
test/map-api-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { addDependency } from "../src/core/dependencies.ts";
99 import { createIssue } from "../src/core/issues.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { seedAdmin } from "./helpers/admin.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 let dataDir = "";
1415 let app: App, servers: Servers, bearer = "";
@@ -50,7 +51,7 @@ test("the repository map names its phases in order, with durations", async () =>
5051 expect(res.status).toBe(200);
5152 const body = (await res.json()) as { ready: unknown[] };
5253 expect(body.ready.length).toBeGreaterThan(0);
53 const parts = (res.headers.get("server-timing") ?? "").split(", ");
54 const parts = requestTiming(res).split(", ");
5455 const names = parts.map((p) => p.split(";")[0]!);
5556 expect(names.filter((n) => n !== "app")).toEqual(["session", "graph", "marks", "pillars", "asktolive", "held", "render"]);
5657 for (const p of parts) expect(Number(/dur=([\d.]+)/.exec(p)?.[1])).not.toBeNaN();
test/map-cache.test.ts +2 −1
@@ -19,6 +19,7 @@ import type { Graph } from "../src/core/dependencies.ts";
1919 import { seedFixture, type Fixture, type Shape } from "../bench/map-home.ts";
2020 import { countStatements } from "../bench/run.ts";
2121 import { quietLogs } from "./helpers/globals.ts";
22 import { requestTiming } from "./helpers/request-timing.ts";
2223
2324 // `seedFixture` raises the log floor and never lowers it: borrowed for
2425 // this file, and given back to whichever file runs next (#7419).
@@ -275,7 +276,7 @@ describe("/map's Server-Timing names its phases", () => {
275276 const cookie = who === "owner" ? f.ownerCookie : f.strangerCookie;
276277 for (const pass of ["cold", "warm"]) {
277278 const res = await fetch(`${servers.url}/map`, { headers: { cookie } });
278 const header = res.headers.get("server-timing") ?? "";
279 const header = requestTiming(res);
279280 const dur = new Map(
280281 [...header.matchAll(/([a-z-]+);dur=(\d+\.\d)/g)].map((m) => [m[1]!, Number(m[2])]),
281282 );
test/map-timing.test.ts +2 −1
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
55 import { join } from "node:path";
66 import { createApp, type App } from "../src/app.ts";
77 import { serve, type Servers } from "../src/http/server.ts";
8 import { requestTiming } from "./helpers/request-timing.ts";
89
910 let dataDir = "";
1011 let app: App, servers: Servers;
@@ -25,6 +26,6 @@ test("the forge-wide map names session, graph, render in order", async () => {
2526 const res = await fetch(`${servers.url}/api/v1/map`);
2627 await res.arrayBuffer();
2728 expect(res.status).toBe(200);
28 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]);
29 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]);
2930 expect(names.filter((n) => n !== "app")).toEqual(["session", "graph", "render"]);
3031 });
test/new-pull-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -58,6 +59,6 @@ test("the new pull page names session, compare and render after app", async () =
5859 const res = await fetch(`${servers.url}/alice/r/pulls/new`, { headers: { cookie } });
5960 expect(res.status).toBe(200);
6061 await res.arrayBuffer();
61 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
62 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
6263 expect(names.slice(names.indexOf("app") + 1)).toEqual(["session", "compare", "render"]);
6364 });
test/og-timing.test.ts +2 −1
@@ -7,6 +7,7 @@ import { createApp, type App } from "../src/app.ts";
77 import { createRepo } from "../src/actions/repos.ts";
88 import { createUser } from "../src/core/users.ts";
99 import { serve, type Servers } from "../src/http/server.ts";
10 import { requestTiming } from "./helpers/request-timing.ts";
1011
1112 setDefaultTimeout(30_000);
1213
@@ -39,7 +40,7 @@ afterAll(async () => {
3940 async function get(path: string, headers: Record<string, string> = {}) {
4041 const res = await fetch(servers.url + path, { headers });
4142 await res.arrayBuffer();
42 const names = (res.headers.get("server-timing") ?? "")
43 const names = requestTiming(res)
4344 .split(", ")
4445 .map((p) => /^([a-z-]+);/.exec(p)?.[1])
4546 .filter((n): n is string => n !== undefined && n !== "app");
test/org-timing.test.ts +2 −1
@@ -6,6 +6,7 @@ import { join } from "node:path";
66 import { createApp, type App } from "../src/app.ts";
77 import { serve, type Servers } from "../src/http/server.ts";
88 import { seedAdmin } from "./helpers/admin.ts";
9 import { requestTiming } from "./helpers/request-timing.ts";
910
1011 let dataDir = "";
1112 let app: App, servers: Servers, bearer = "";
@@ -39,7 +40,7 @@ async function phases(path: string, status = 200): Promise<string[]> {
3940 const res = await fetch(`${servers.url}${path}`, { headers: auth() });
4041 await res.arrayBuffer();
4142 expect(res.status).toBe(status);
42 return (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!).filter((n) => n !== "app" && n !== "db");
43 return requestTiming(res).split(", ").map((p) => p.split(";")[0]!).filter((n) => n !== "app" && n !== "db");
4344 }
4445
4546 test("an org names session, org, render", async () => {
test/passport-health-timing.test.ts +2 −1
@@ -6,6 +6,7 @@ import { join } from "node:path";
66 import { createApp, type App } from "../src/app.ts";
77 import { serve, type Servers } from "../src/http/server.ts";
88 import { issue } from "../src/kernel/passport.ts";
9 import { requestTiming } from "./helpers/request-timing.ts";
910
1011 let dataDir = "";
1112 let app: App, servers: Servers;
@@ -23,7 +24,7 @@ afterAll(async () => {
2324 });
2425
2526 const phases = (res: Response) =>
26 (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!).filter((n) => n && n !== "app");
27 requestTiming(res).split(", ").map((p) => p.split(";")[0]!).filter((n) => n && n !== "app");
2728
2829 const ask = (token?: string) =>
2930 fetch(`${servers.url}/api/v1/passport/health`, {
test/pull-page-phases.test.ts +2 −1
@@ -23,6 +23,7 @@ import { serve, type Servers } from "../src/http/server.ts";
2323 import { quietLogs } from "./helpers/globals.ts";
2424 import { PULL_PAGE_PHASES } from "./helpers/pull-phases.ts";
2525 import { PNG } from "./helpers/studio.ts";
26 import { requestTiming } from "./helpers/request-timing.ts";
2627
2728 quietLogs();
2829
@@ -101,7 +102,7 @@ async function timed(): Promise<{ names: string[]; ms: (n: string) => number; bo
101102 const res = await fetch(`${servers.url}/alice/r/pulls/${number}`, { headers: { cookie } });
102103 expect(res.status).toBe(200);
103104 const body = await res.text();
104 const parts = (res.headers.get("server-timing") ?? "").split(", ");
105 const parts = requestTiming(res).split(", ");
105106 const all = parts.map((p) => p.split(";")[0]!);
106107 const ms = (n: string) => Number(parts[all.indexOf(n)]!.match(/dur=([\d.]+)/)![1]);
107108 return { names: all.slice(all.indexOf("app") + 1), ms, body };
test/pull-timing.test.ts +3 −2
@@ -14,6 +14,7 @@ import { createUser } from "../src/core/users.ts";
1414 import { serve, type Servers } from "../src/http/server.ts";
1515 import { quietLogs } from "./helpers/globals.ts";
1616 import { PULL_PAGE_PHASES } from "./helpers/pull-phases.ts";
17 import { requestTiming } from "./helpers/request-timing.ts";
1718
1819 quietLogs();
1920
@@ -105,7 +106,7 @@ test("a pull names its read phases and render after app, summing to no more than
105106 const res = await fetch(`${servers.url}/alice/r/pulls/${number}`, { headers: { cookie } });
106107 expect(res.status).toBe(200);
107108 await res.arrayBuffer();
108 const parts = (res.headers.get("server-timing") ?? "").split(", ");
109 const parts = requestTiming(res).split(", ");
109110 const names = parts.map((p) => p.split(";")[0]!);
110111 const after = names.slice(names.indexOf("app") + 1);
111112 expect(after).toEqual([...PULL_PAGE_PHASES]);
@@ -124,7 +125,7 @@ test("a pull's Files page names session, diff, checks, reviews and render, in th
124125 const html = await res.text();
125126 expect(html).toContain("m39.ts");
126127 expect(html).not.toContain("<b>1</b>");
127 const parts = (res.headers.get("server-timing") ?? "").split(", ");
128 const parts = requestTiming(res).split(", ");
128129 const names = parts.map((p) => p.split(";")[0]!);
129130 expect(names.slice(names.indexOf("app") + 1)).toEqual(["session", "diff", "diff-git", "diff-parse", "diff-highlight", "git", "checks", "reviews", "render"]);
130131 });
test/pulls-list-timing.test.ts +2 −1
@@ -7,6 +7,7 @@ import { createApp, type App } from "../src/app.ts";
77 import { createRepo } from "../src/actions/repos.ts";
88 import { serve, type Servers } from "../src/http/server.ts";
99 import { seedAdmin } from "./helpers/admin.ts";
10 import { requestTiming } from "./helpers/request-timing.ts";
1011
1112 let dataDir = "";
1213 let app: App, servers: Servers, bearer = "";
@@ -34,7 +35,7 @@ afterAll(async () => {
3435 });
3536
3637 function names(res: Response): string[] {
37 return (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
38 return requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
3839 }
3940
4041 test("a repository's pulls list names session, repo, pulls, words, tells, labels, steps and render in order", async () => {
test/repo-pages-timing.test.ts +2 −1
@@ -11,6 +11,7 @@ import { serve, type Servers } from "../src/http/server.ts";
1111 import { proveAll } from "./helpers/verified.ts";
1212 import { quietLogs } from "./helpers/globals.ts";
1313 import { promote } from "./helpers/members.ts";
14 import { requestTiming } from "./helpers/request-timing.ts";
1415
1516 quietLogs();
1617
@@ -79,7 +80,7 @@ for (const [path, who, names] of PAGES)
7980 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
8081 await res.text();
8182 expect(res.status).toBe(200);
82 return (res.headers.get("server-timing") ?? "").split(", ");
83 return requestTiming(res).split(", ");
8384 };
8485 const dur = (part: string | undefined): number => {
8586 expect(part).toMatch(/^[a-z-]+;dur=\d+\.\d$/);
test/repo-reads-timing.test.ts +2 −1
@@ -6,6 +6,7 @@ import { join } from "node:path";
66 import { createApp, type App } from "../src/app.ts";
77 import { serve, type Servers } from "../src/http/server.ts";
88 import { seedAdmin } from "./helpers/admin.ts";
9 import { requestTiming } from "./helpers/request-timing.ts";
910
1011 let dataDir = "";
1112 let app: App, servers: Servers, bearer = "";
@@ -19,7 +20,7 @@ const call = (path: string, init: RequestInit = {}) =>
1920 const phases = async (path: string): Promise<{ status: number; names: string[] }> => {
2021 const res = await call(path);
2122 await res.arrayBuffer();
22 const names = (res.headers.get("server-timing") ?? "").split(", ").filter(Boolean).map((p) => p.split(";")[0]!).filter((n) => n !== "db" && n !== "app");
23 const names = requestTiming(res).split(", ").filter(Boolean).map((p) => p.split(";")[0]!).filter((n) => n !== "db" && n !== "app");
2324 return { status: res.status, names };
2425 };
2526
test/repo-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createSession } from "../src/core/sessions.ts";
99 import { createUser } from "../src/core/users.ts";
1010 import { serve, type Servers } from "../src/http/server.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -55,6 +56,6 @@ test("a repository home names session, appof, refs, tree, readme, following, wav
5556 const res = await fetch(`${servers.url}/alice/r`, { headers: { cookie } });
5657 expect(res.status).toBe(200);
5758 await res.arrayBuffer();
58 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
59 const names = requestTiming(res).split(", ").map((p) => p.split(";")[0]!);
5960 expect(names).toEqual(["app", "session", "appof", "refs", "tree", "readme", "following", "wave", "behind", "render"]);
6061 });
test/route-timing.test.ts +2 −2
@@ -46,8 +46,8 @@ test("a response carries Server-Timing, and /alice/x and /bob/y share one route
4646 const a = await fetch(`${servers.url}/alice/x`);
4747 await fetch(`${servers.url}/bob/y`);
4848 const health = await fetch(`${servers.url}/healthz`);
49 expect(a.headers.get("server-timing")).toMatch(/^app;dur=\d+\.\d$/);
50 expect(health.headers.get("server-timing")).toMatch(/^db;dur=\d+\.\d, app;dur=\d+\.\d$/);
49 expect(a.headers.get("server-timing")).toMatch(/^app;dur=\d+\.\d, lag;dur=\d+\.\d$/);
50 expect(health.headers.get("server-timing")).toMatch(/^db;dur=\d+\.\d, app;dur=\d+\.\d, lag;dur=\d+\.\d$/);
5151 servers.stop();
5252
5353 const rows = readTiming(app.db, "");
test/settings-timing.test.ts +2 −1
@@ -9,6 +9,7 @@ import { createUser } from "../src/core/users.ts";
99 import { serve, type Servers } from "../src/http/server.ts";
1010 import { proveAll } from "./helpers/verified.ts";
1111 import { quietLogs } from "./helpers/globals.ts";
12 import { requestTiming } from "./helpers/request-timing.ts";
1213
1314 quietLogs();
1415
@@ -65,7 +66,7 @@ for (const [path, names] of PAGES)
6566 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
6667 await res.text();
6768 expect(res.status).toBe(200);
68 const parts = (res.headers.get("server-timing") ?? "").split(", ");
69 const parts = requestTiming(res).split(", ");
6970 expect(parts.map((p) => p.split(";")[0]).filter((n) => n !== "app")).toEqual(names);
7071 for (const name of names) expect(parts.find((p) => p.startsWith(`${name};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/);
7172 }
test/telemetry-reads-timing.test.ts +2 −1
@@ -6,6 +6,7 @@ import { join } from "node:path";
66 import { createApp, type App } from "../src/app.ts";
77 import { serve, type Servers } from "../src/http/server.ts";
88 import { seedAdmin } from "./helpers/admin.ts";
9 import { requestTiming } from "./helpers/request-timing.ts";
910
1011 let dataDir = "";
1112 let app: App, servers: Servers, bearer = "";
@@ -30,7 +31,7 @@ afterAll(async () => {
3031 });
3132
3233 const phases = (res: Response) =>
33 (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!).filter((n) => n && n !== "app");
34 requestTiming(res).split(", ").map((p) => p.split(";")[0]!).filter((n) => n && n !== "app");
3435
3536 for (const what of ["system", "history", "flow", "sources"]) {
3637 test(`telemetry/${what} names session, read, render in order`, async () => {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs