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

Merged opened this

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

Merged

scout/forge:scout/3506-the-capacity-settings-page-s-server-timi → main 2 commits fork 1 thread · 1 unresolved

  • Merged. scout/3506-the-capacity-settings-page-s-server-timi was merged into main as fe927d8.

Review

Sign in to review this change.
2 files changed +80 −4
src/web/pages/capacity.ts +15 −4
@@ -9,6 +9,7 @@
99 import { changeCapacity, listCapacity, type CapacityRow } from "../../actions/capacity.ts";
1010 import { CAPACITY, type CapacityKey } from "../../core/capacity.ts";
1111 import type { Ctx } from "../../http/context.ts";
12 import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts";
1213 import { notFoundPage, signInFirst } from "../chrome.ts";
1314 import { field, readForm } from "../form.ts";
1415 import { html, when } from "../html.ts";
@@ -66,12 +67,22 @@ function input(key: CapacityKey, value: string) {
6667
6768 // GET /settings/capacity
6869 export function capacityPage(ctx: Ctx): Response {
70 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
6971 const denied = requireAdmin(ctx);
7072 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 );
7276 if (!listed.ok) return notFoundPage(ctx);
7377 const { settings, changes } = listed.value;
78 const errs = timePhase(ctx.request, "errors", () =>
outdated

rendering starts here, but the errors and refused reads further down run inside this window, so render includes the time already reported as errors and refused. Server-Timing phases should be disjoint, or the header misleads whoever reads it. Move the clock to just before the rows build and take the signal reads before it, or hoist errs and refused above this line. Then add a test that would fail on the overlap.

79 ctx.app.signals.errors.recent(10),
80 );
81 const refused = timePhase(ctx.request, "refused", () =>
82 ctx.app.signals.refused.recent(),
83 );
7484 const now = Date.now();
85 const rendering = performance.now();
7586 const rows = settings.map((s) =>
7687 row({
7788 lead: icon("activity"),
@@ -119,8 +130,6 @@ export function capacityPage(ctx: Ctx): Response {
119130 const reported = settings.some(
120131 (s) => s.key === "cells.egress" && s.value === "report",
121132 );
122 const errs = ctx.app.signals.errors.recent(10);
123 const refused = ctx.app.signals.refused.recent();
124133 const happened = card(
125134 html`<div class="card-body">
126135 <p class="muted small" id="errors">
@@ -187,7 +196,9 @@ export function capacityPage(ctx: Ctx): Response {
187196 pad: false,
188197 },
189198 )}`;
190 return usageShell(ctx, "capacity", body);
199 const response = usageShell(ctx, "capacity", body);
200 notePhase(ctx.request, "render", performance.now() - rendering);
201 return response;
191202 }
192203
193204 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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs