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

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

Merged opened this

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

Merged

scout/forge:scout/3423-the-origins-page-s-server-timing-names-i → main 1 commit fork 2 threads · 2 unresolved

  • Merged. scout/3423-the-origins-page-s-server-timing-names-i was merged into main as ad3b57d.

Review

Sign in to review this change.
2 files changed +87 −11
src/web/pages/origins.ts +33 −11
@@ -48,6 +48,7 @@ import {
4848 } from "../../core/origins.ts";
4949 import { pinnedRepoIds } from "../../core/pins.ts";
5050 import { getRepo, getReposByIds } from "../../core/repos.ts";
51 import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts";
5152 import type { Ctx } from "../../http/context.ts";
5253 import { forgeBaseUrl, oauthCallbackUri } from "../../http/api.ts";
5354 import { qrEncode, qrSvg } from "../../design/qr.ts";
@@ -672,11 +673,20 @@ function trustedHostsCard(text: string, error?: string): Html {
672673
673674 // ---- the page ----
674675
675 function originsView(ctx: Ctx, values: ConnectValues): Html {
676 function originsView(
677 ctx: Ctx,
678 values: ConnectValues,
679 timing?: { rendering: number },
680 ): Html {
676681 const user = ctx.auth.user!;
677 const origins = listUserOrigins(ctx.app, user);
678 const mirrors = mirrorsByOrigin(ctx.app.db, user.id);
679 const apps = listOriginApps(ctx.app, user);
682 // Only the page itself names its reads; the error re-renders do not.
683 const read = <T>(name: string, work: () => T): T =>
684 timing === undefined ? work() : timePhase(ctx.request, name, work);
685 const origins = read("origins", () => listUserOrigins(ctx.app, user));
686 const mirrors = read("mirrors", () => mirrorsByOrigin(ctx.app.db, user.id));
687 const apps = read("apps", () => listOriginApps(ctx.app, user));
688 const frozen = read("frozen", () => readFrozen(ctx));
689 if (timing !== undefined) timing.rendering = performance.now();

Setting timing.rendering from inside the view is a hidden out-parameter. Consider timing originsView plus shell with one timePhase("render") around the body build, so the view needs no timing argument beyond the read wrapper.

680690 const githubConfigured = githubConnectable(ctx);
681691 const matched =
682692 values.host === ""
@@ -697,7 +707,7 @@ function originsView(ctx: Ctx, values: ConnectValues): Html {
697707 : html`<div class="stack">
698708 ${origins.map((o) => originCard(o, mirrors.get(o.id)?.count ?? 0))}
699709 </div>`}
700 ${frozenCard(ctx)}
710 ${frozenCard(frozen)}
701711 ${mayEditTrustedHosts(user)
702712 ? trustedHostsCard(
703713 values.trustedText ?? editableHosts().join("\n"),
@@ -743,15 +753,22 @@ function originsView(ctx: Ctx, values: ConnectValues): Html {
743753 * connecting an account on that host brings them back by itself, so this
744754 * card's only verb is the connect form beneath it.
745755 */
746 function frozenCard(ctx: Ctx): Html | null {
756 function readFrozen(ctx: Ctx) {
747757 const frozen = listDetachedMirrors(ctx.app.db, {
748758 ownerId: ctx.auth.user!.id,
749759 });
760 const repos =
761 frozen.length === 0
762 ? (new Map() as ReturnType<typeof getReposByIds>)
763 : getReposByIds(
764 ctx.app.db,
765 frozen.map((m) => m.repoId),

new Map() as ReturnType<typeof getReposByIds> is a cast. Typing the empty map via getReposByIds(db, []) or a typed new Map<number, Repo>() would avoid it.

766 );
767 return { frozen, repos };
768 }
769
770 function frozenCard({ frozen, repos }: ReturnType<typeof readFrozen>): Html | null {
750771 if (frozen.length === 0) return null;
751 const repos = getReposByIds(
752 ctx.app.db,
753 frozen.map((m) => m.repoId),
754 );
755772 return listCard(
756773 frozen.flatMap((m) => {
757774 const repo = repos.get(m.repoId);
@@ -800,6 +817,7 @@ function shell(ctx: Ctx, body: Html, status = 200): Response {
800817
801818 // GET /settings/origins
802819 export function originsPage(ctx: Ctx): Response {
820 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
803821 const denied = accountOnly(ctx);
804822 if (denied) return denied;
805823 const params = ctx.url.searchParams;
@@ -832,7 +850,11 @@ export function originsPage(ctx: Ctx): Response {
832850 </div>`
833851 : null;
834852
835 return shell(ctx, html`${banner}${originsView(ctx, values)}`);
853 const timing = { rendering: performance.now() };
854 const view = originsView(ctx, values, timing);
855 const response = shell(ctx, html`${banner}${view}`);
856 notePhase(ctx.request, "render", performance.now() - timing.rendering);
857 return response;
836858 }
837859
838860 // POST /settings/origins/token
test/origins-timing.test.ts added +54
@@ -0,0 +1,54 @@
1 /** `GET /settings/origins` names session, each read and render in Server-Timing (#3423). */
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-origins-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 /settings/origins 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}/settings/origins`, { headers: { cookie } });
48 const body = await res.text();
49 expect(res.status).toBe(200);
50 expect(body).toContain("<h1>Origins</h1>");
51 const parts = (res.headers.get("server-timing") ?? "").split(", ");
52 for (const name of ["app", "session", "origins", "mirrors", "apps", "frozen", "render"])
53 expect(parts.find((p) => p.startsWith(`${name};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/);
54 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs