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

A repository's following page's Server-Timing names its phases: session, each read, render #3502

Merged opened this

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

Merged

scout/forge:scout/3461-a-repository-s-following-page-s-server-t → main 1 commit fork 2 threads · 2 unresolved

  • Merged. scout/3461-a-repository-s-following-page-s-server-t was merged into main as be86d04.

Review

Sign in to review this change.
2 files changed +86 −6
src/web/pages/chain.ts +15 −6
@@ -8,6 +8,7 @@
88 import { FOLLOW_MODE_REFUSALS, setFollowMode, severForge, takeForgeCode, takeForgeWords } from "../../actions/follow.ts";
99 import { forgeProposals, proposeToParentForge, type ForgeProposal } from "../../actions/upstream.ts";
1010 import { canAdmin, canWrite } from "../../core/repos.ts";
11 import { notePhase, takeSessionTime, timePhase, timePhaseAsync } from "../../core/route_timing.ts";
1112 import type { Ctx } from "../../http/context.ts";
1213 import { chainView, FOLLOW_MODES, forgeLinkOf, type ChainLine, type FollowMode } from "../../spawn/chain.ts";
1314 import { checkUpstream, upstreamState, upstreamWords } from "../../spawn/upstream.ts";
@@ -107,18 +108,24 @@ function proposalCard(base: string, proposals: readonly ForgeProposal[], writer:
107108
108109 // GET /:owner/:name/following
109110 export async function chainPage(ctx: Ctx): Promise<Response> {
110 const repo = loadRepo(ctx);
111 const at = ctx.request;
112 notePhase(at, "session", takeSessionTime(at));
113 const repo = timePhase(at, "repo", () => loadRepo(ctx));
111114 if (!repo) return notFoundPage(ctx);
112 const view = (await carriesChain(ctx)) ? await chainView(ctx.app) : undefined;
115 const view = await timePhaseAsync(at, "chain", async () => ((await carriesChain(ctx)) ? await chainView(ctx.app) : undefined));
113116 // Any other repository's following is the card on its home.
114117 if (view === undefined) return redirect(`${repoBase(repo)}#following`);
115118 const base = repoBase(repo);
116119 const owner = canAdmin(ctx.app.db, repo, ctx.auth.user);
117 const proposing = view.severedAt === null && (await parentCredential(ctx.app)) !== undefined;
118 const proposals = proposing ? await forgeProposals(ctx.app, repo, Date.now(), ctx.auth.user !== undefined) : [];
120 const proposing =
121 view.severedAt === null && (await timePhaseAsync(at, "parent", () => parentCredential(ctx.app))) !== undefined;
122 const proposals = proposing
123 ? await timePhaseAsync(at, "proposals", () => forgeProposals(ctx.app, repo, Date.now(), ctx.auth.user !== undefined))
124 : [];
119125 const writer = canWrite(ctx.app.db, repo, ctx.auth.user);
120 const state = await upstreamState(ctx.app);
121 return page({
126 const state = await timePhaseAsync(at, "upstream", () => upstreamState(ctx.app));
127 const rendering = performance.now();
128 const response = page({
122129 ctx,
123130 title: "Following",
124131 body: html`<div class="narrow-wide">
@@ -175,6 +182,8 @@ export async function chainPage(ctx: Ctx): Promise<Response> {
175182 </div>
176183 </div>`,
177184 });
185 notePhase(at, "render", performance.now() - rendering);
186 return response;
178187 }
179188
180189 /** POST /:owner/:name/following/sever {confirm: sever}; the owner only. */
test/chain-timing.test.ts added +71
@@ -0,0 +1,71 @@
1 /** `/:owner/:name/following` names session, repo, chain, parent, upstream and render in Server-Timing (#3461). */
2 import { afterAll, beforeAll, describe, 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 { createRepo } from "../src/actions/repos.ts";
7 import { childDirOf, spawnForge } from "../src/actions/spawn.ts";
8 import { createApp, type App } from "../src/app.ts";
9 import { createSession } from "../src/core/sessions.ts";
10 import { createUser, getUserByName } from "../src/core/users.ts";
11 import { repoDir } from "../src/git/repos.ts";
12 import { commitFiles } from "../src/git/write.ts";
13 import { serve, type Servers } from "../src/http/server.ts";
14 import { hostedPlacement } from "../src/spawn/placement.ts";
15 import { quietLogs } from "./helpers/globals.ts";
16
17 quietLogs();
18
19 let dir = "";
20 let root: App;
21 let child: App;
22 let servers: Servers;
23 let cookie = "";
24
25 beforeAll(async () => {
26 dir = await mkdtemp(join(tmpdir(), "forge-chain-mode-"));
27 root = createApp({ dataDir: dir, url: "https://root.test", port: 0, forgeRepo: "alice/forge-code" });
28 const alice = await createUser(root.db, { origin: "seed", trusted: true, username: "alice", email: "[email protected]", isAdmin: true });
29 if (!alice.ok) throw new Error("user");
30 const made = await createRepo(root, alice.value, { name: "forge-code", visibility: "public" });
31 if (!made.ok) throw new Error("repo");
32 const c = await commitFiles(repoDir(root.reposRoot, "alice", "forge-code"), {
33 branch: "main",
34 message: "forge v1",
35 files: [{ path: "README.md", content: "# forge\n" }],
36 author: { name: "a", email: "[email protected]" },
37 });

Temp dir prefix is forge-chain-mode (copied); rename to forge-chain-timing.

38 if (!c.ok) throw new Error("commit");
39 const a = await spawnForge(
40 root,
41 alice.value,
42 { name: "kid", org: "kid", apps: [], carryBriefs: false, carrySubscriptions: false },
43 { catalog: [], briefs: [], subscriptions: [] },
44 {
45 placement: hostedPlacement({ host: "root.test" }),
46 request: new Request("https://root.test/x", { method: "POST", headers: { origin: "https://root.test" } }),
47 url: new URL("https://root.test/x"),
48 },
49 );
50 if (!a.ok) throw new Error(JSON.stringify(a.error));
51 child = createApp({ dataDir: childDirOf(root, a.value.bodyId), port: 0 });
52 servers = await serve(child);
53 const owner = getUserByName(child.db, "alice")!;
54 cookie = `forge_session=${createSession(child.db, owner.id).token}`;
55 }, 60_000);
56
57 afterAll(async () => {
58 servers.stop();
59 child.close();
60 root.close();
61 await rm(dir, { recursive: true, force: true });
62 });
63
64 test("GET /:owner/:name/following names its phases", async () => {
65 const res = await fetch(`${servers.url}/kid/forge/following`, { headers: { cookie } });
66 expect(res.status).toBe(200);
67 expect(await res.text()).toContain("Following");
68 const parts = (res.headers.get("server-timing") ?? "").split(", ");

Add a case that reaches the proposals read, or proposals is untested.

69 for (const name of ["app", "session", "repo", "chain", "parent", "upstream", "render"])
70 expect(parts.find((p) => p.startsWith(`${name};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/);
71 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs