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

Merged opened this

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

Merged

scout/forge:scout/3283-a-client-s-first-screen-s-server-timing → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/3283-a-client-s-first-screen-s-server-timing was merged into main as 2c4b112.

Review

Sign in to review this change.
2 files changed +64 −2
src/web/studio/client_home.ts +7 −2
@@ -6,6 +6,7 @@
66 * stay off it; `test/client-home.test.ts` holds the list.
77 */
88
9 import { notePhase, takeSessionTime, timePhaseAsync } from "../../core/route_timing.ts";
910 import { clientAppRows } from "../../core/client-apps.ts";
1011 import type { User } from "../../core/users.ts";
1112 import type { Ctx } from "../../http/context.ts";
@@ -25,7 +26,9 @@ export function agentsLine(working: number, queued: number): string {
2526
2627 /** The card of apps; an empty one says they will appear. The caller decides who is a client. */
2728 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();
2932 const rows = apps.map(({ repo, working, queued }) => {
3033 const about = repo.description !== "" ? repo.description : "An app of yours.";
3134 return html`<li class="client-app">
@@ -36,7 +39,7 @@ export async function clientApps(ctx: Ctx, user: User): Promise<Html> {
3639 >
3740 </li>`;
3841 });
39 return card(
42 const shown = card(
4043 rows.length === 0
4144 ? html`<p class="muted">Your apps will appear here.</p>`
4245 : html`<ul class="list client-apps">
@@ -44,4 +47,6 @@ export async function clientApps(ctx: Ctx, user: User): Promise<Html> {
4447 </ul>`,
4548 { id: "client-apps", head: html`<h2>Your apps</h2>` },
4649 );
50 notePhase(ctx.request, "render", performance.now() - rendering);
51 return shown;
4752 }
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). */

Check the issue number in this header against the pull (#3296).

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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs