Public: anyone can read this; the crew can change it.
A repository's map page names its Server-Timing phases, and `/alice/forge/map` leaves the sweep's UNNAMED list #5785
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/5766-a-repository-s-map-page-names-its-server → main 1 commit fork
3 files
changed +82
−17
src/web/pages/map.ts +25 −16
| @@ -33,7 +33,7 @@ import { | ||
| 33 | 33 | type Ranked, |
| 34 | 34 | } from "../../core/dependencies.ts"; |
| 35 | 35 | import type { Issue } from "../../core/issues.ts"; |
| 36 | import { notePhase, takeSessionTime } from "../../core/route_timing.ts"; | |
| 36 | import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts"; | |
| 37 | 37 | import { cachedLane, cachedLayout, repoHeads, scopeKey } from "../map_cache.ts"; |
| 38 | 38 | import { holdsFor } from "../../core/holds.ts"; |
| 39 | 39 | import { |
| @@ -1351,24 +1351,31 @@ const studioEmpty = (base: string): MapEmpty => ({ | ||
| 1351 | 1351 | |
| 1352 | 1352 | /** GET /:owner/:name/map */ |
| 1353 | 1353 | export async function mapPage(ctx: Ctx): Promise<Response> { |
| 1354 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 1354 | 1355 | const repo = loadRepo(ctx); |
| 1355 | 1356 | if (!repo) return notFoundPage(ctx); |
| 1356 | 1357 | const db = ctx.app.db; |
| 1357 | 1358 | const studio = studioRecord(ctx, repo) !== undefined; |
| 1358 | const graph = repoGraph(db, repo.id, { | |
| 1359 | ...sealedScope(db, repo, ctx.auth.user), | |
| 1360 | }); | |
| 1361 | const ready = readyNow(graph); | |
| 1362 | const blocked = blockedOn(graph); | |
| 1363 | const critical = criticalPath(graph); | |
| 1364 | const placed = cachedLayout(graph, layout); | |
| 1365 | const marks = marksFor(db, repo.id, graph, listActiveRuns(db, [repo.id])); | |
| 1366 | const readySet = new Set(ready.map((r) => r.issue.id)); | |
| 1367 | const counts = countWords( | |
| 1368 | graph.issues.map((i) => wordFor(i, readySet, marks)), | |
| 1369 | ); | |
| 1370 | const pillars = pillarView(db, repo); | |
| 1371 | const ask = askToLiveFor(ctx.app, repo, pillars, ctx.auth.user); | |
| 1359 | // The read: graph, marks and pillars, one phase; `render` is the page's own. | |
| 1360 | const { graph, ready, blocked, critical, placed, marks, readySet, counts, pillars, ask } = | |
| 1361 | timePhase(ctx.request, "map", () => { | |
| 1362 | const graph = repoGraph(db, repo.id, { | |
| 1363 | ...sealedScope(db, repo, ctx.auth.user), | |
| 1364 | }); | |
| 1365 | const ready = readyNow(graph); | |
| 1366 | const blocked = blockedOn(graph); | |
| 1367 | const critical = criticalPath(graph); | |
| 1368 | const placed = cachedLayout(graph, layout); | |
| 1369 | const marks = marksFor(db, repo.id, graph, listActiveRuns(db, [repo.id])); | |
| 1370 | const readySet = new Set(ready.map((r) => r.issue.id)); | |
| 1371 | const counts = countWords( | |
| 1372 | graph.issues.map((i) => wordFor(i, readySet, marks)), | |
| 1373 | ); | |
| 1374 | const pillars = pillarView(db, repo); | |
| 1375 | const ask = askToLiveFor(ctx.app, repo, pillars, ctx.auth.user); | |
| 1376 | return { graph, ready, blocked, critical, placed, marks, readySet, counts, pillars, ask }; | |
| 1377 | }); | |
| 1378 | const rendering = performance.now(); | |
| 1372 | 1379 | const body = html`${repoHeader(ctx, repo, "map")} |
| 1373 | 1380 | <div class="page-head mt"> |
| 1374 | 1381 | <h1>Map</h1> |
| @@ -1423,7 +1430,7 @@ export async function mapPage(ctx: Ctx): Promise<Response> { | ||
| 1423 | 1430 | )} |
| 1424 | 1431 | </aside> |
| 1425 | 1432 | </div>`; |
| 1426 | return page({ | |
| 1433 | const response = page({ | |
| 1427 | 1434 | ctx, |
| 1428 | 1435 | title: `Map · ${repo.name}`, |
| 1429 | 1436 | crumbs: crumbsFor(repo), |
| @@ -1436,6 +1443,8 @@ export async function mapPage(ctx: Ctx): Promise<Response> { | ||
| 1436 | 1443 | styleCss: mapCss, |
| 1437 | 1444 | scriptJs: `${liveRegionsJs}\n${mapViewJs}`, |
| 1438 | 1445 | }); |
| 1446 | notePhase(ctx.request, "render", performance.now() - rendering); | |
| 1447 | return response; | |
| 1439 | 1448 | } |
| 1440 | 1449 | |
| 1441 | 1450 | /** |
test/map-page-timing.test.ts added +57
| @@ -0,0 +1,57 @@ | ||
| 1 | /** `/:owner/:name/map` names session, map and render in Server-Timing (#5766). */ | |
| 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 { createRepo } from "../src/actions/repos.ts"; | |
| 7 | import { createApp, type App } from "../src/app.ts"; | |
| 8 | import { createUser } from "../src/core/users.ts"; | |
| 9 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 10 | import { proveAll } from "./helpers/verified.ts"; | |
| 11 | import { quietLogs } from "./helpers/globals.ts"; | |
| 12 | ||
| 13 | quietLogs(); | |
| 14 | ||
| 15 | let root = ""; | |
| 16 | let app: App; | |
| 17 | let servers: Servers; | |
| 18 | ||
| 19 | beforeAll(async () => { | |
| 20 | root = mkdtempSync(join(tmpdir(), "forge-map-page-timing-")); | |
| 21 | app = createApp({ dataDir: root, port: 0, forgeRepo: "alice/forge" }); | |
| 22 | const u = await createUser(app.db, { | |
| 23 | origin: "seed", | |
| 24 | trusted: true, | |
| 25 | username: "alice", | |
| 26 | email: "[email protected]", | |
| 27 | password: "a-good-passphrase", | |
| 28 | adminIfFirst: true, | |
| 29 | }); | |
| 30 | if (!u.ok) throw new Error(u.error); | |
| 31 | proveAll(app); | |
| 32 | const made = await createRepo(app, { id: u.value.id, username: "alice" } as never, { name: "forge" }); | |
| 33 | if (!made.ok) throw new Error(made.error); | |
| 34 | servers = await serve(app); | |
| 35 | }); | |
| 36 | afterAll(() => { | |
| 37 | servers.stop(); | |
| 38 | app.close(); | |
| 39 | rmSync(root, { recursive: true, force: true }); | |
| 40 | }); | |
| 41 | ||
| 42 | test("GET /:owner/:name/map names its phases", async () => { | |
| 43 | const signin = await fetch(`${servers.url}/signin`, { | |
| 44 | method: "POST", | |
| 45 | headers: { origin: servers.url }, | |
| 46 | body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }), | |
| 47 | redirect: "manual", | |
| 48 | }); | |
| 49 | const cookie = (signin.headers.get("set-cookie") ?? "").split(";")[0]!; | |
| 50 | const res = await fetch(`${servers.url}/alice/forge/map`, { headers: { cookie } }); | |
| 51 | const body = await res.text(); | |
| 52 | expect(res.status).toBe(200); | |
| 53 | expect(body).toContain("<h1>Map</h1>"); | |
| 54 | const parts = (res.headers.get("server-timing") ?? "").split(", "); | |
| 55 | for (const name of ["app", "session", "map", "render"]) | |
| 56 | expect(parts.find((p) => p.startsWith(`${name};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/); | |
| 57 | }); |
test/server-timing-sweep.test.ts −1
| @@ -22,7 +22,6 @@ quietLogs(); | ||
| 22 | 22 | const UNNAMED: Array<[path: string, issue: string]> = [ |
| 23 | 23 | ["/-/confirm", "#3390"], |
| 24 | 24 | ["/alice/forge/changes", "#3323"], |
| 25 | ["/alice/forge/map", "#3320"], | |
| 26 | 25 | ["/alice/forge/tells", "#3322"], |
| 27 | 26 | ["/signin/parent", "#3390"], |
| 28 | 27 | ["/signin/parent/callback", "#3390"], |
new above
offline