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

The subscriptions page's Server-Timing names its phases: session, each read, render #3466

Merged opened this

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

Merged

scout/forge:scout/3427-the-subscriptions-page-s-server-timing-n → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/3427-the-subscriptions-page-s-server-timing-n was merged into main as 043ed2b.

Review

Sign in to review this change.
2 files changed +67 −9
src/web/pages/subscriptions.ts +22 −9
@@ -34,6 +34,7 @@ import {
3434 } from "../../core/standing.ts";
3535 import { getUserById } from "../../core/users.ts";
3636 import type { Ctx } from "../../http/context.ts";
37 import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts";
3738 import { notFoundPage, settingsLayout, signInFirst } from "../chrome.ts";
3839 import { field, readForm, type Form } from "../form.ts";
3940 import { html, when, type Html } from "../html.ts";
@@ -127,24 +128,34 @@ function shell(ctx: Ctx, body: Html): Response {
127128
128129 // GET /settings/subscriptions
129130 export function subscriptionsPage(ctx: Ctx): Response {
131 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
130132 const denied = requireAdmin(ctx);
131133 if (denied) return denied;
132134 const now = new Date();
133 const pool = listCredentials(ctx.app.db);
134 const places = listGrantPlaces(ctx.app.db);
135 const scopes = scopesOf(ctx.app.db);
135 const pool = timePhase(ctx.request, "credentials", () =>
136 listCredentials(ctx.app.db),
137 );
138 const places = timePhase(ctx.request, "places", () =>
139 listGrantPlaces(ctx.app.db),
140 );
141 const scopes = timePhase(ctx.request, "scopes", () => scopesOf(ctx.app.db));
136142 const scopeText = (s: Subscription): string =>
137143 s.id === PLATFORM
138144 ? "the crew's: every app no subscription is granted to"
139145 : scopes.has(s.id)
140146 ? `for ${scopes.get(s.id)!.join(", ")}`
141147 : "not offered to the crew — granted nowhere";
142 const budgets = budgetsOf(ctx.app.db);
143 const standingOf = standings(
144 ctx.app.db,
145 pool.map((s) => s.id),
146 now,
148 const budgets = timePhase(ctx.request, "budgets", () =>
149 budgetsOf(ctx.app.db),
147150 );
151 const standingOf = timePhase(ctx.request, "standings", () =>
152 standings(
153 ctx.app.db,
154 pool.map((s) => s.id),
155 now,
156 ),
157 );
158 const rendering = performance.now();

render starts here and so also covers the per-row user lookups, which are reads. The scout said so in the pull. If you want those named too, wrap them in a users phase later.

148159
149160 const sealed = pool.filter((s) => s.id !== PLATFORM);
150161 const none =
@@ -368,7 +379,7 @@ export function subscriptionsPage(ctx: Ctx): Response {
368379 },
369380 );
370381
371 return shell(
382 const response = shell(
372383 ctx,
373384 html`${list} ${none === null ? null : html`<div class="mt">${none}</div>`}
374385 ${when(
@@ -387,6 +398,8 @@ export function subscriptionsPage(ctx: Ctx): Response {
387398 <div class="mt">${add}</div>
388399 ${used === null ? null : html`<div class="mt">${used}</div>`}`,
389400 );
401 notePhase(ctx.request, "render", performance.now() - rendering);
402 return response;
390403 }
391404
392405 // POST /settings/subscriptions
test/subscriptions-timing.test.ts added +45
@@ -0,0 +1,45 @@
1 /** The subscriptions page names its phases in Server-Timing (#3427). */
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-subtime-"));
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 subscriptions page names session, each read and render once each", async () => {
34 const res = await fetch(`${servers.url}/settings/subscriptions`, { headers: { authorization: `Bearer ${bearer}` } });
35 const page = await res.text();
36 expect(res.status).toBe(200);
37 expect(page).toContain("Subscriptions");
38 const parts = (res.headers.get("server-timing") ?? "").split(", ");
39 const names = parts.map((p) => p.split(";")[0]!);
40 for (const n of ["session", "credentials", "places", "scopes", "budgets", "standings", "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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs