Public: anyone can read this; the crew can change it.
The capacity settings page's Server-Timing names its phases: session, each read, render #3522
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3506-the-capacity-settings-page-s-server-timi → main 2 commits fork 1 thread · 1 unresolved
2 files
changed +80
−4
src/web/pages/capacity.ts +15 −4
| @@ -9,6 +9,7 @@ | ||
| 9 | 9 | import { changeCapacity, listCapacity, type CapacityRow } from "../../actions/capacity.ts"; |
| 10 | 10 | import { CAPACITY, type CapacityKey } from "../../core/capacity.ts"; |
| 11 | 11 | import type { Ctx } from "../../http/context.ts"; |
| 12 | import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts"; | |
| 12 | 13 | import { notFoundPage, signInFirst } from "../chrome.ts"; |
| 13 | 14 | import { field, readForm } from "../form.ts"; |
| 14 | 15 | import { html, when } from "../html.ts"; |
| @@ -66,12 +67,22 @@ function input(key: CapacityKey, value: string) { | ||
| 66 | 67 | |
| 67 | 68 | // GET /settings/capacity |
| 68 | 69 | export function capacityPage(ctx: Ctx): Response { |
| 70 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 69 | 71 | const denied = requireAdmin(ctx); |
| 70 | 72 | if (denied) return denied; |
| 71 | const listed = listCapacity(ctx.app, ctx.auth.user); | |
| 73 | const listed = timePhase(ctx.request, "capacity", () => | |
| 74 | listCapacity(ctx.app, ctx.auth.user), | |
| 75 | ); | |
| 72 | 76 | if (!listed.ok) return notFoundPage(ctx); |
| 73 | 77 | const { settings, changes } = listed.value; |
| 78 | const errs = timePhase(ctx.request, "errors", () => | |
| 79 | ctx.app.signals.errors.recent(10), | |
| 80 | ); | |
| 81 | const refused = timePhase(ctx.request, "refused", () => | |
| 82 | ctx.app.signals.refused.recent(), | |
| 83 | ); | |
| 74 | 84 | const now = Date.now(); |
| 85 | const rendering = performance.now(); | |
| 75 | 86 | const rows = settings.map((s) => |
| 76 | 87 | row({ |
| 77 | 88 | lead: icon("activity"), |
| @@ -119,8 +130,6 @@ export function capacityPage(ctx: Ctx): Response { | ||
| 119 | 130 | const reported = settings.some( |
| 120 | 131 | (s) => s.key === "cells.egress" && s.value === "report", |
| 121 | 132 | ); |
| 122 | const errs = ctx.app.signals.errors.recent(10); | |
| 123 | const refused = ctx.app.signals.refused.recent(); | |
| 124 | 133 | const happened = card( |
| 125 | 134 | html`<div class="card-body"> |
| 126 | 135 | <p class="muted small" id="errors"> |
| @@ -187,7 +196,9 @@ export function capacityPage(ctx: Ctx): Response { | ||
| 187 | 196 | pad: false, |
| 188 | 197 | }, |
| 189 | 198 | )}`; |
| 190 | return usageShell(ctx, "capacity", body); | |
| 199 | const response = usageShell(ctx, "capacity", body); | |
| 200 | notePhase(ctx.request, "render", performance.now() - rendering); | |
| 201 | return response; | |
| 191 | 202 | } |
| 192 | 203 | |
| 193 | 204 | const ERRORS: Record<string, string> = { |
test/capacity-timing.test.ts added +65
| @@ -0,0 +1,65 @@ | ||
| 1 | /** The capacity settings page names its phases in Server-Timing (#3506). */ | |
| 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 3 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import { createApp, type App } from "../src/app.ts"; | |
| 7 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 8 | import { seedAdmin } from "./helpers/admin.ts"; | |
| 9 | ||
| 10 | let dataDir = ""; | |
| 11 | let app: App, servers: Servers, bearer = ""; | |
| 12 | ||
| 13 | beforeAll(async () => { | |
| 14 | dataDir = await mkdtemp(join(tmpdir(), "forge-captime-")); | |
| 15 | app = createApp({ dataDir, port: 0 }); | |
| 16 | servers = await serve(app); | |
| 17 | const admin = await seedAdmin(app, { username: "alice", email: "[email protected]", password: "correct-horse-battery" }); | |
| 18 | if (!admin.ok) throw new Error(admin.error); | |
| 19 | const minted = await fetch(`${servers.url}/api/v1/tokens`, { | |
| 20 | method: "POST", | |
| 21 | headers: { "content-type": "application/json" }, | |
| 22 | body: JSON.stringify({ adminConfirm: true, username: "alice", password: "correct-horse-battery", name: "cli", scope: "admin" }), | |
| 23 | }); | |
| 24 | bearer = ((await minted.json()) as { token: string }).token; | |
| 25 | }); | |
| 26 | ||
| 27 | afterAll(async () => { | |
| 28 | servers.stop(); | |
| 29 | app.close(); | |
| 30 | await rm(dataDir, { recursive: true, force: true }); | |
| 31 | }); | |
| 32 | ||
| 33 | test("the capacity page names session, capacity, errors, refused and render once each", async () => { | |
| 34 | const res = await fetch(`${servers.url}/settings/capacity`, { headers: { authorization: `Bearer ${bearer}` } }); | |
| 35 | const page = await res.text(); | |
| 36 | expect(res.status).toBe(200); | |
| 37 | expect(page).toContain("Errors and refused calls"); | |
| 38 | const parts = (res.headers.get("server-timing") ?? "").split(", "); | |
| 39 | const names = parts.map((p) => p.split(";")[0]!); | |
| 40 | for (const n of ["session", "capacity", "errors", "refused", "render"]) { | |
| 41 | expect(names.filter((x) => x === n).length).toBe(1); | |
| 42 | const part = parts.find((p) => p.startsWith(`${n};`))!; | |
| 43 | expect(Number(/dur=([\d.]+)/.exec(part)?.[1])).not.toBeNaN(); | |
| 44 | } | |
| 45 | }); | |
| 46 | ||
| 47 | test("render does not include the time already named as errors", async () => { | |
| 48 | const errors = app.signals.errors; | |
| 49 | const real = errors.recent.bind(errors); | |
| 50 | errors.recent = ((n: number) => { | |
| 51 | const until = performance.now() + 80; | |
| 52 | while (performance.now() < until); | |
| 53 | return real(n); | |
| 54 | }) as typeof errors.recent; | |
| 55 | try { | |
| 56 | const res = await fetch(`${servers.url}/settings/capacity`, { headers: { authorization: `Bearer ${bearer}` } }); | |
| 57 | await res.text(); | |
| 58 | const dur = (n: string) => | |
| 59 | Number(new RegExp(`(?:^|, )${n};dur=([\\d.]+)`).exec(res.headers.get("server-timing") ?? "")?.[1]); | |
| 60 | expect(dur("errors")).toBeGreaterThanOrEqual(80); | |
| 61 | expect(dur("render")).toBeLessThan(dur("errors")); | |
| 62 | } finally { | |
| 63 | errors.recent = real; | |
| 64 | } | |
| 65 | }); |
new above
offline
renderingstarts here, but theerrorsandrefusedreads further down run inside this window, sorenderincludes the time already reported aserrorsandrefused. Server-Timing phases should be disjoint, or the header misleads whoever reads it. Move the clock to just before therowsbuild and take the signal reads before it, or hoisterrsandrefusedabove this line. Then add a test that would fail on the overlap.