Public: anyone can read this; the crew can change it.
A repository's home page's Server-Timing names its phases: session, each read, render #4177
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3325-a-repository-s-home-page-s-server-timing → main 3 commits fork 1 thread · 1 unresolved
2 files
changed +77
−7
src/web/pages/repo.ts +17 −7
| @@ -46,7 +46,7 @@ import { | ||
| 46 | 46 | } from "../chrome.ts"; |
| 47 | 47 | import { codeJs } from "../client.ts"; |
| 48 | 48 | import { highlightBlob } from "../code.ts"; |
| 49 | import { notePhase, takeSessionTime } from "../../core/route_timing.ts"; | |
| 49 | import { notePhase, takeSessionTime, timePhase, timePhaseAsync } from "../../core/route_timing.ts"; | |
| 50 | 50 | import { liveClientJs, liveMount } from "../live.ts"; |
| 51 | 51 | import { appStrip } from "./apps.ts"; |
| 52 | 52 | import { studioApp } from "../studio/app.ts"; |
| @@ -720,11 +720,13 @@ export async function renderCode( | ||
| 720 | 720 | entries: readonly TreeEntry[]; |
| 721 | 721 | extra?: Html; |
| 722 | 722 | rail?: Html; |
| 723 | /** The README, when the caller has already read it (and timed the read). */ | |
| 724 | readme?: Html; | |
| 723 | 725 | }, |
| 724 | 726 | ): Promise<Html> { |
| 725 | 727 | const base = scope.base; |
| 726 | 728 | const { ref, path } = o; |
| 727 | const readme = await readmeFor(scope.source, ref, o.entries, path); | |
| 729 | const readme = o.readme ?? (await readmeFor(scope.source, ref, o.entries, path)); | |
| 728 | 730 | const historyParams = new URLSearchParams({ ref }); |
| 729 | 731 | if (path !== "") historyParams.set("path", path); |
| 730 | 732 | const bar = |
| @@ -973,11 +975,12 @@ function archivedLine(ctx: Ctx, repo: Repo): Html | null { | ||
| 973 | 975 | /** The code at a ref: the tree, the README and the About rail — with, in |
| 974 | 976 | * the studio, the offer to make a repository that is not an app one. */ |
| 975 | 977 | async function codeHome(ctx: Ctx, repo: Repo): Promise<Response> { |
| 976 | const isApp = getAppByRepo(ctx.app.db, repo.id) !== undefined; | |
| 978 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 979 | const isApp = timePhase(ctx.request, "appof", () => getAppByRepo(ctx.app.db, repo.id) !== undefined); | |
| 977 | 980 | const noApp = isApp ? null : noAppCard(ctx, repo); |
| 978 | 981 | const scope = localScope(ctx, repo); |
| 979 | 982 | const cloneUrl = `${ctx.url.origin}/${repo.ownerName}/${repo.name}.git`; |
| 980 | const refs = await refSetFrom(scope.source); | |
| 983 | const refs = await timePhaseAsync(ctx.request, "refs", () => refSetFrom(scope.source)); | |
| 981 | 984 | |
| 982 | 985 | if (refs.branches.length === 0) { |
| 983 | 986 | const body = html`${scopeHeader(ctx, scope, "code")}${archivedLine(ctx, repo)}${noApp} |
| @@ -1032,12 +1035,16 @@ async function codeHome(ctx: Ctx, repo: Repo): Promise<Response> { | ||
| 1032 | 1035 | // the tree read, not after it, because neither waits on the other. |
| 1033 | 1036 | const parent = forkParent(ctx, repo); |
| 1034 | 1037 | const [entries, drift] = await Promise.all([ |
| 1035 | scope.source.tree(ref, ""), | |
| 1038 | timePhaseAsync(ctx.request, "tree", () => scope.source.tree(ref, "")), | |
| 1036 | 1039 | parent === undefined ? undefined : forkDrift(ctx.app, repo, parent), |
| 1037 | 1040 | ]); |
| 1038 | 1041 | const tree = entries.ok ? entries.value : []; |
| 1039 | 1042 | |
| 1040 | const following = await followingOf(ctx, repo); | |
| 1043 | const readme = await timePhaseAsync(ctx.request, "readme", () => | |
| 1044 | readmeFor(scope.source, ref, tree, ""), | |
| 1045 | ); | |
| 1046 | const following = await timePhaseAsync(ctx.request, "following", () => followingOf(ctx, repo)); | |
| 1047 | const rendering = performance.now(); | |
| 1041 | 1048 | const body = html`${scopeHeader(ctx, scope, "code", { |
| 1042 | 1049 | branchCount: refs.branches.length, |
| 1043 | 1050 | })}${archivedLine(ctx, repo)}${noApp} |
| @@ -1050,10 +1057,11 @@ async function codeHome(ctx: Ctx, repo: Repo): Promise<Response> { | ||
| 1050 | 1057 | ref, |
| 1051 | 1058 | path: "", |
| 1052 | 1059 | entries: tree, |
| 1060 | readme, | |
| 1053 | 1061 | rail: aboutCard(ctx, repo, refs, drift), |
| 1054 | 1062 | })} |
| 1055 | 1063 | ${codeMount(scope, ref)} ${finderDialog()} ${liveMount({ repo: scope.base })}`; |
| 1056 | return page({ | |
| 1064 | const response = page({ | |
| 1057 | 1065 | ctx, |
| 1058 | 1066 | title: repo.name, |
| 1059 | 1067 | description: repo.description, |
| @@ -1067,6 +1075,8 @@ async function codeHome(ctx: Ctx, repo: Repo): Promise<Response> { | ||
| 1067 | 1075 | body, |
| 1068 | 1076 | scriptJs: `${codeJs}\n${liveClientJs}`, |
| 1069 | 1077 | }); |
| 1078 | notePhase(ctx.request, "render", performance.now() - rendering); | |
| 1079 | return response; | |
| 1070 | 1080 | } |
| 1071 | 1081 | |
| 1072 | 1082 | function knownRef(refs: RefSet, name: string): boolean { |
test/repo-timing.test.ts added +60
| @@ -0,0 +1,60 @@ | ||
| 1 | /** A repository home's Server-Timing names its phases, in order (#3325). */ | |
| 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 { commitToBranch, createRepo } from "../src/actions/repos.ts"; | |
| 8 | import { createSession } from "../src/core/sessions.ts"; | |
| 9 | import { createUser } from "../src/core/users.ts"; | |
| 10 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 11 | import { quietLogs } from "./helpers/globals.ts"; | |
| 12 | ||
| 13 | quietLogs(); | |
| 14 | ||
| 15 | let dataDir: string; | |
| 16 | let app: App; | |
| 17 | let servers: Servers; | |
| 18 | let cookie = ""; | |
| 19 | const cells = process.env["FORGE_CELLS"]; | |
| 20 | ||
| 21 | beforeAll(async () => { | |
| 22 | process.env["FORGE_CELLS"] = "off"; | |
| 23 | dataDir = await mkdtemp(join(tmpdir(), "forge-repo-timing-")); | |
| 24 | app = createApp({ dataDir, port: 0 }); | |
| 25 | servers = await serve(app); | |
| 26 | const made = await createUser(app.db, { | |
| 27 | origin: "seed", | |
| 28 | trusted: true, | |
| 29 | username: "alice", | |
| 30 | email: "[email protected]", | |
| 31 | password: "a-good-passphrase", | |
| 32 | adminIfFirst: true, | |
| 33 | }); | |
| 34 | if (!made.ok) throw new Error(String(made.error)); | |
| 35 | const alice = made.value; | |
| 36 | cookie = `forge_session=${createSession(app.db, alice.id).token}`; | |
| 37 | const repo = await createRepo(app, alice, { name: "r", visibility: "public" }); | |
| 38 | if (!repo.ok) throw new Error(String(repo.error)); | |
| 39 | const done = await commitToBranch(app, alice, repo.value, { | |
| 40 | message: "change", | |
| 41 | files: [{ path: "README.md", content: "# hi" }], | |
| 42 | }); | |
| 43 | if (!done.ok) throw new Error(JSON.stringify(done.error)); | |
| 44 | }); | |
| 45 | ||
| 46 | afterAll(async () => { | |
| 47 | await servers.stop(); | |
| 48 | app.close(); | |
| 49 | if (cells === undefined) delete process.env["FORGE_CELLS"]; | |
| 50 | else process.env["FORGE_CELLS"] = cells; | |
| 51 | await rm(dataDir, { recursive: true, force: true }); | |
| 52 | }); | |
| 53 | ||
| 54 | test("a repository home names session, appof, refs, tree, readme, following and render in order", async () => { | |
| 55 | const res = await fetch(`${servers.url}/alice/r`, { headers: { cookie } }); | |
| 56 | expect(res.status).toBe(200); | |
| 57 | await res.arrayBuffer(); | |
| 58 | const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!); | |
| 59 | expect(names).toEqual(["app", "session", "appof", "refs", "tree", "readme", "following", "render"]); | |
| 60 | }); |
new above
offline
Render is noted only on this path; the empty-repo branch above returns without it. Add the same note there.