Public: anyone can read this; the crew can change it.
The family page's Server-Timing names its phases: session, each read, render #3490
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3458-the-family-page-s-server-timing-names-it → main 1 commit fork 1 thread · 1 unresolved
2 files
changed +62
−2
src/web/pages/family.ts +8 −2
| @@ -7,6 +7,7 @@ | ||
| 7 | 7 | * reads "quiet": a fact about the clock, not a fault. |
| 8 | 8 | */ |
| 9 | 9 | |
| 10 | import { notePhase, takeSessionTime, timePhaseAsync } from "../../core/route_timing.ts"; | |
| 10 | 11 | import type { Ctx } from "../../http/context.ts"; |
| 11 | 12 | import type { ChildAppsSummary } from "../../spawn/child-apps.ts"; |
| 12 | 13 | import { familyView, followLine, type FamilyChild } from "../../spawn/family.ts"; |
| @@ -93,7 +94,10 @@ export async function familyPage( | ||
| 93 | 94 | ): Promise<Response> { |
| 94 | 95 | const app = ctx.app; |
| 95 | 96 | const owns = ctx.auth.user?.isAdmin === true; |
| 96 | const view = await familyView(app, owns, appsOf); | |
| 97 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 98 | // One read: the parent, and for an admin the children's standings. | |
| 99 | const view = await timePhaseAsync(ctx.request, "family", () => familyView(app, owns, appsOf)); | |
| 100 | const rendering = performance.now(); | |
| 97 | 101 | const parent = view.parent; |
| 98 | 102 | const kids = { items: view.children, more: view.more }; |
| 99 | 103 | |
| @@ -120,7 +124,7 @@ export async function familyPage( | ||
| 120 | 124 | }; |
| 121 | 125 | |
| 122 | 126 | const alone = parent === null && kids.items.length === 0; |
| 123 | return page({ | |
| 127 | const response = page({ | |
| 124 | 128 | ctx, |
| 125 | 129 | title: "Family", |
| 126 | 130 | body: html`<div class="narrow-wide"> |
| @@ -145,4 +149,6 @@ export async function familyPage( | ||
| 145 | 149 | )} |
| 146 | 150 | </div>`, |
| 147 | 151 | }); |
| 152 | notePhase(ctx.request, "render", performance.now() - rendering); | |
| 153 | return response; | |
| 148 | 154 | } |
test/family-timing.test.ts added +54
| @@ -0,0 +1,54 @@ | ||
| 1 | /** `/family` names session, family and render in Server-Timing (#3458). */ | |
| 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 3 | import { mkdtempSync, rmSync } from "node:fs"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import { createApp, type App } from "../src/app.ts"; | |
| 7 | import { createUser } from "../src/core/users.ts"; | |
| 8 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 9 | import { proveAll } from "./helpers/verified.ts"; | |
| 10 | import { quietLogs } from "./helpers/globals.ts"; | |
| 11 | ||
| 12 | quietLogs(); | |
| 13 | ||
| 14 | let root = ""; | |
| 15 | let app: App; | |
| 16 | let servers: Servers; | |
| 17 | ||
| 18 | beforeAll(async () => { | |
| 19 | root = mkdtempSync(join(tmpdir(), "forge-family-timing-")); | |
| 20 | app = createApp({ dataDir: root, port: 0, forgeRepo: "alice/forge" }); | |
| 21 | const u = await createUser(app.db, { | |
| 22 | origin: "seed", | |
| 23 | trusted: true, | |
| 24 | username: "alice", | |
| 25 | email: "[email protected]", | |
| 26 | password: "a-good-passphrase", | |
| 27 | adminIfFirst: true, | |
| 28 | }); | |
| 29 | if (!u.ok) throw new Error(u.error); | |
| 30 | proveAll(app); | |
| 31 | servers = await serve(app); | |
| 32 | }); | |
| 33 | afterAll(() => { | |
| 34 | servers.stop(); | |
| 35 | app.close(); | |
| 36 | rmSync(root, { recursive: true, force: true }); | |
| 37 | }); | |
| 38 | ||
| 39 | test("GET /family names its phases", async () => { | |
| 40 | const signin = await fetch(`${servers.url}/signin`, { | |
| 41 | method: "POST", | |
| 42 | headers: { origin: servers.url }, | |
| 43 | body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }), | |
| 44 | redirect: "manual", | |
| 45 | }); | |
| 46 | const cookie = (signin.headers.get("set-cookie") ?? "").split(";")[0]!; | |
| 47 | const res = await fetch(`${servers.url}/family`, { headers: { cookie } }); | |
| 48 | const body = await res.text(); | |
| 49 | expect(res.status).toBe(200); | |
| 50 | expect(body).toContain("Family"); | |
| 51 | const parts = (res.headers.get("server-timing") ?? "").split(", "); | |
| 52 | for (const name of ["app", "session", "family", "render"]) | |
| 53 | expect(parts.find((p) => p.startsWith(`${name};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/); | |
| 54 | }); |
new above
offline
A single phase named family is honest for one read. If the children standings are the slow part for an admin, this is the line to split once familyView exposes them separately.