Public: anyone can read this; the crew can change it.
A client's first screen's Server-Timing names its phases: session, each read, render #3296
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3283-a-client-s-first-screen-s-server-timing → main 1 commit fork 1 thread · 1 unresolved
2 files
changed +64
−2
src/web/studio/client_home.ts +7 −2
| @@ -6,6 +6,7 @@ | ||
| 6 | 6 | * stay off it; `test/client-home.test.ts` holds the list. |
| 7 | 7 | */ |
| 8 | 8 | |
| 9 | import { notePhase, takeSessionTime, timePhaseAsync } from "../../core/route_timing.ts"; | |
| 9 | 10 | import { clientAppRows } from "../../core/client-apps.ts"; |
| 10 | 11 | import type { User } from "../../core/users.ts"; |
| 11 | 12 | import type { Ctx } from "../../http/context.ts"; |
| @@ -25,7 +26,9 @@ export function agentsLine(working: number, queued: number): string { | ||
| 25 | 26 | |
| 26 | 27 | /** The card of apps; an empty one says they will appear. The caller decides who is a client. */ |
| 27 | 28 | export async function clientApps(ctx: Ctx, user: User): Promise<Html> { |
| 28 | const apps = await clientAppRows(ctx.app, user); | |
| 29 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 30 | const apps = await timePhaseAsync(ctx.request, "apps", () => clientAppRows(ctx.app, user)); | |
| 31 | const rendering = performance.now(); | |
| 29 | 32 | const rows = apps.map(({ repo, working, queued }) => { |
| 30 | 33 | const about = repo.description !== "" ? repo.description : "An app of yours."; |
| 31 | 34 | return html`<li class="client-app"> |
| @@ -36,7 +39,7 @@ export async function clientApps(ctx: Ctx, user: User): Promise<Html> { | ||
| 36 | 39 | > |
| 37 | 40 | </li>`; |
| 38 | 41 | }); |
| 39 | return card( | |
| 42 | const shown = card( | |
| 40 | 43 | rows.length === 0 |
| 41 | 44 | ? html`<p class="muted">Your apps will appear here.</p>` |
| 42 | 45 | : html`<ul class="list client-apps"> |
| @@ -44,4 +47,6 @@ export async function clientApps(ctx: Ctx, user: User): Promise<Html> { | ||
| 44 | 47 | </ul>`, |
| 45 | 48 | { id: "client-apps", head: html`<h2>Your apps</h2>` }, |
| 46 | 49 | ); |
| 50 | notePhase(ctx.request, "render", performance.now() - rendering); | |
| 51 | return shown; | |
| 47 | 52 | } |
test/client-home-timing.test.ts added +57
| @@ -0,0 +1,57 @@ | ||
| 1 | /** A client's first screen names its phases in Server-Timing: session, apps, render (#3283). */ | |
| 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 { createOrg } from "../src/core/orgs.ts"; | |
| 8 | import { createUser } from "../src/core/users.ts"; | |
| 9 | import { createToken } from "../src/core/tokens.ts"; | |
| 10 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 11 | ||
| 12 | let dir = ""; | |
| 13 | let app: App; | |
| 14 | let servers: Servers; | |
| 15 | let bearer = ""; | |
| 16 | ||
| 17 | beforeAll(async () => { | |
| 18 | dir = await mkdtemp(join(tmpdir(), "forge-client-home-timing-")); | |
| 19 | app = createApp({ dataDir: dir, port: 0 }); | |
| 20 | servers = await serve(app); | |
| 21 | const made = await createUser(app.db, { | |
| 22 | origin: "seed", trusted: true, username: "maria", email: "[email protected]", | |
| 23 | password: "a-good-passphrase", adminIfFirst: true, | |
| 24 | }); | |
| 25 | if (!made.ok) throw new Error(made.error); | |
| 26 | // A forge made for a client: parent.json names the org its owner's apps live in. | |
| 27 | const org = createOrg(app.db, "marias", made.value.id); | |
| 28 | if (!org.ok) throw new Error(org.error); | |
| 29 | await Bun.write(join(dir, "parent.json"), JSON.stringify({ org: "marias" })); | |
| 30 | bearer = createToken(app.db, made.value.id, "t", "admin").token; | |
| 31 | }); | |
| 32 | ||
| 33 | afterAll(async () => { | |
| 34 | servers.stop(); | |
| 35 | app.close(); | |
| 36 | await rm(dir, { recursive: true, force: true }); | |
| 37 | }); | |
| 38 | ||
| 39 | const get = (path: string) => fetch(servers.url + path, { headers: { authorization: `Bearer ${bearer}` } }); | |
| 40 | const timing = (res: Response): string[] => (res.headers.get("server-timing") ?? "").split(", "); | |
| 41 | ||
| 42 | test("the client home names session, apps and render once each", async () => { | |
| 43 | const res = await get("/"); | |
| 44 | expect(res.status).toBe(200); | |
| 45 | expect(await res.text()).toContain('id="client-apps"'); | |
| 46 | const parts = timing(res); | |
| 47 | for (const n of ["session", "apps", "render"]) { | |
| 48 | expect(parts.filter((p) => p.startsWith(`${n};`)).length).toBe(1); | |
| 49 | expect(parts.find((p) => p.startsWith(`${n};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/); | |
| 50 | } | |
| 51 | }); | |
| 52 | ||
| 53 | test("the full home does not name the client's phases", async () => { | |
| 54 | const res = await get("/?all=1"); | |
| 55 | await res.arrayBuffer(); | |
| 56 | expect(timing(res).some((p) => p.startsWith("apps;"))).toBe(false); | |
| 57 | }); |
new above
offline
Check the issue number in this header against the pull (#3296).