Public: anyone can read this; the crew can change it.
The shelf's stretches show in Server-Timing on /, /apps and a person's page #6264
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6178-the-shelf-s-stretches-show-in-server-tim → main 3 commits fork 2 threads · 2 unresolved
6 files
changed +83
−10
src/web/pages/org.ts +1 −1
| @@ -309,7 +309,7 @@ export async function ownerProfile(ctx: Ctx): Promise<Response> { | ||
| 309 | 309 | if (!owner || hiddenFrom(owner, ctx.auth.user)) return notFoundPage(ctx); |
| 310 | 310 | |
| 311 | 311 | const isOrg = owner.kind === "org"; |
| 312 | const access = timePhase(ctx.request, "owner", () => | |
| 312 | const access = timePhase(ctx.request, "access", () => | |
| 313 | 313 | isOrg ? orgAccess(ctx.app.db, owner.id, ctx.auth.user) : "none", |
| 314 | 314 | ); |
| 315 | 315 | const canManage = isOrg && accessAllows(access, "admin"); |
src/web/studio/home.ts +3 −4
| @@ -504,10 +504,9 @@ function frontControls(ctx: Ctx, front: FrontPage): Html { | ||
| 504 | 504 | export function studioDoor(ctx: Ctx): Response { |
| 505 | 505 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); |
| 506 | 506 | let tiles: Html[] = []; |
| 507 | const front = timePhase(ctx.request, "shelf", () => { | |
| 508 | const found = frontPage(ctx.app); | |
| 509 | tiles = found.shelf.map(tile); | |
| 510 | return found; | |
| 507 | const front = timePhase(ctx.request, "shelf", () => frontPage(ctx.app)); | |
| 508 | timePhase(ctx.request, "tiles", () => { | |
| 509 | tiles = front.shelf.map(tile); | |
| 511 | 510 | }); |
| 512 | 511 | // Render is what is left of the page once the shelf read is taken out. |
| 513 | 512 | const rendering = performance.now(); |
src/web/studio/shelf.ts +1 −1
| @@ -182,7 +182,7 @@ export async function shelfRows( | ||
| 182 | 182 | let mark = performance.now(); |
| 183 | 183 | const lap = (name: string): void => { |
| 184 | 184 | const now = performance.now(); |
| 185 | notePhase(ctx.request, name, now - mark); | |
| 185 | notePhase(ctx.request, `shelf-${name}`, now - mark); | |
Good: a hyphen prefix, because |
||
| 186 | 186 | mark = now; |
| 187 | 187 | }; |
| 188 | 188 | const records = appsForRepos(db, repoIds); |
test/front-timing.test.ts +3 −3
| @@ -33,15 +33,15 @@ async function read(): Promise<string[]> { | ||
| 33 | 33 | return (res.headers.get("server-timing") ?? "").split(", "); |
| 34 | 34 | } |
| 35 | 35 | |
| 36 | test("GET / signed out names session, shelf, render in order and covers the handler's share of app", async () => { | |
| 36 | test("GET / signed out names session, shelf, tiles, render in order and covers the handler's share of app", async () => { | |
| 37 | 37 | for (let i = 0; i < 5; i++) await read(); // warm |
| 38 | 38 | const shares: number[] = []; |
| 39 | 39 | for (let i = 0; i < 15; i++) { |
| 40 | 40 | const parts = await read(); |
| 41 | 41 | const names = parts.map((p) => p.split(";")[0]); |
| 42 | expect(names.filter((n) => n !== "app" && n !== "db")).toEqual(["session", "shelf", "render"]); | |
| 42 | expect(names.filter((n) => n !== "app" && n !== "db")).toEqual(["session", "shelf", "tiles", "render"]); | |
| 43 | 43 | const total = dur(parts.find((p) => p.startsWith("app;"))!); |
| 44 | const named = parts.filter((p) => /^(session|shelf|render);/.test(p)).reduce((s, p) => s + dur(p), 0); | |
| 44 | const named = parts.filter((p) => /^(session|shelf|tiles|render);/.test(p)).reduce((s, p) => s + dur(p), 0); | |
| 45 | 45 | shares.push(total === 0 ? 1 : named / total); |
| 46 | 46 | } |
| 47 | 47 | // The handler's own phases are all of the handler; what `app` also holds |
test/shelf-laps.test.ts added +74
| @@ -0,0 +1,74 @@ | ||
| 1 | /** The shelf's laps reach Server-Timing on /, /apps and /:owner (#6178). */ | |
| 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 { createApp as insertApp } from "../src/core/apps.ts"; | |
| 9 | import { createSession } from "../src/core/sessions.ts"; | |
| 10 | import { createUser } from "../src/core/users.ts"; | |
| 11 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 12 | import { promote } from "./helpers/members.ts"; | |
| 13 | import { quietLogs } from "./helpers/globals.ts"; | |
| 14 | ||
| 15 | quietLogs(); | |
| 16 | ||
| 17 | let root = ""; | |
| 18 | let app: App; | |
| 19 | let servers: Servers; | |
| 20 | let cookie = ""; | |
| 21 | ||
| 22 | beforeAll(async () => { | |
| 23 | root = mkdtempSync(join(tmpdir(), "forge-shelf-laps-")); | |
| 24 | app = createApp({ dataDir: root, port: 0 }); | |
| 25 | servers = await serve(app); | |
| 26 | const u = await createUser(app.db, { | |
| 27 | origin: "seed", | |
| 28 | trusted: true, | |
| 29 | username: "keeper", | |
| 30 | email: "[email protected]", | |
| 31 | password: "a-good-passphrase", | |
| 32 | adminIfFirst: true, | |
| 33 | }); | |
| 34 | if (!u.ok) throw new Error(u.error); | |
| 35 | promote(app.db, u.value); | |
| 36 | cookie = `forge_session=${createSession(app.db, u.value.id).token}; forge_mode=studio`; | |
| 37 | for (const name of ["alpha", "beta"]) { | |
| 38 | const made = await createRepo(app, u.value, { name, visibility: "public" }); | |
| 39 | if (!made.ok) throw new Error(String(made.error)); | |
| 40 | insertApp(app.db, { repoId: made.value.id, slug: name, access: "public" }); | |
| 41 | } | |
| 42 | }, 30_000); | |
| 43 | afterAll(() => { | |
| 44 | servers.stop(); | |
| 45 | app.close(); | |
| 46 | rmSync(root, { recursive: true, force: true }); | |
| 47 | }); | |
| 48 | ||
| 49 | async function names(path: string): Promise<string[]> { | |
| 50 | const res = await fetch(`${servers.url}${path}`, { | |
| 51 | headers: path === "/" ? {} : { cookie }, | |
| 52 | }); | |
| 53 | expect(res.status).toBe(200); | |
| 54 | const header = res.headers.get("server-timing") ?? ""; | |
| 55 | return header.split(",").map((p) => p.trim().split(";")[0]!); | |
| 56 | } | |
| 57 | ||
| 58 | for (const path of ["/apps", "/keeper"]) { | |
| 59 | test(`GET ${path} names the shelf's laps once each`, async () => { | |
| 60 | const got = await names(path); | |
| 61 | for (const lap of ["records", "runs", "production", "pulls", "rows"]) | |
| 62 | expect(got).toContain(`shelf-${lap}`); | |
| 63 | expect(new Set(got).size).toBe(got.length); | |
| 64 | }); | |
| 65 | } | |
| 66 | ||
| 67 | // The front door's shelf is the cached front page, not `shelfRows`: its two | |
| 68 | // stretches are reading the page and drawing the tiles. | |
| 69 | test("GET / names the front shelf's read and its tiles once each", async () => { | |
| 70 | const got = await names("/"); | |
| 71 | expect(got).toContain("shelf"); | |
| 72 | expect(got).toContain("tiles"); | |
| 73 | expect(new Set(got).size).toBe(got.length); | |
| 74 | }); |
test/shelf-speed.test.ts +1 −1
| @@ -84,7 +84,7 @@ test("40 apps are drawn within budget, in the phases Server-Timing names", async | ||
| 84 | 84 | best = Math.min(best, performance.now() - started); |
| 85 | 85 | phases = takePhases(c.request).map((p) => p.name); |
| 86 | 86 | } |
| 87 | expect(phases).toEqual(["records", "runs", "production", "pulls", "rows"]); | |
| 87 | expect(phases).toEqual(["shelf-records", "shelf-runs", "shelf-production", "shelf-pulls", "shelf-rows"]); | |
| 88 | 88 | // The yardstick is one batched read of the same shelf, taken in this run. |
| 89 | 89 | let against = Infinity; |
| 90 | 90 | for (let i = 0; i < 5; i++) { |
new above
offline
This fixes the duplicate
ownername in one header. The new test'sSetsize check pins it.