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

The migrate page and a migration job's page name their Server-Timing phases: session, each read, render #3519

Merged opened this

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

Merged

scout/forge:scout/3504-the-migrate-page-and-a-migration-job-s-p → main 1 commit fork

  • Merged. scout/3504-the-migrate-page-and-a-migration-job-s-p was merged into main as f5b1b62.

Review

Sign in to review this change.
2 files changed +109 −6
src/web/pages/migrate.ts +30 −6
@@ -34,6 +34,12 @@ import {
3434 type BranchTip,
3535 } from "../../origins/work-branch.ts";
3636 import { scopeAllows } from "../../core/tokens.ts";
37 import {
38 notePhase,
39 takeSessionTime,
40 timePhase,
41 timePhaseAsync,
42 } from "../../core/route_timing.ts";
3743 import type { Ctx } from "../../http/context.ts";
3844 import type { MirrorJob, MirrorStage } from "../../origins/jobs.ts";
3945 import { notFoundPage, pageHead, signInFirst } from "../chrome.ts";
@@ -530,9 +536,12 @@ async function show(
530536 // Asked for only: each draw is a listing and a commit per branch at the
531537 // origin (remembered a minute), so a plain visit costs none of it.
532538 const branches = values.showBranches
533 ? await branchChoices(ctx, origins, values.source, values.branch)
539 ? await timePhaseAsync(ctx.request, "branches", () =>
540 branchChoices(ctx, origins, values.source, values.branch),
541 )
534542 : undefined;
535 return page(
543 const rendering = performance.now();
544 const response = page(
536545 {
537546 ctx,
538547 title: "Migrate",
@@ -541,14 +550,22 @@ async function show(
541550 },
542551 status,
543552 );
553 notePhase(ctx.request, "render", performance.now() - rendering);
554 return response;
544555 }
545556
546557 // GET /migrate
547558 export async function migratePage(ctx: Ctx): Promise<Response> {
559 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
548560 if (!ctx.auth.user) return signInFirst(ctx);
549561 const q = ctx.url.searchParams;
550 const origins = await originChoices(ctx);
551 const agents = listAllAgents(ctx.app, ctx.auth.user);
562 const origins = await timePhaseAsync(ctx.request, "origins", () =>
563 originChoices(ctx),
564 );
565 const user = ctx.auth.user;
566 const agents = timePhase(ctx.request, "agents", () =>
567 listAllAgents(ctx.app, user),
568 );
552569 const names = agents.ok ? agents.value.map((a) => a.user.username) : [];
553570 const linked = prefill(
554571 { from: q.get("from"), crew: q.get("crew") },
@@ -718,8 +735,12 @@ const migrateJobJs = /* js */ `
718735 `;
719736
720737 export function migrationJobPage(ctx: Ctx): Response {
738 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
721739 if (!ctx.auth.user) return signInFirst(ctx);
722 const found = migrationJobFor(ctx.auth.user, ctx.params.job ?? "");
740 const user = ctx.auth.user;
741 const found = timePhase(ctx.request, "job", () =>
742 migrationJobFor(user, ctx.params.job ?? ""),
743 );
723744 if (!found.ok) return notFoundPage(ctx);
724745 const job = found.value;
725746 if (job.stage === "done" && job.repo !== undefined)
@@ -727,7 +748,8 @@ export function migrationJobPage(ctx: Ctx): Response {
727748 message: `${capitalise(job.detail)}.`,
728749 });
729750 const failed = job.stage === "failed";
730 return page({
751 const rendering = performance.now();
752 const response = page({
731753 ctx,
732754 title: `Migrating ${job.fullName}`,
733755 ...(failed ? {} : { scriptJs: migrateJobJs }),
@@ -759,6 +781,8 @@ export function migrationJobPage(ctx: Ctx): Response {
759781 </p>`}
760782 </div>`,
761783 });
784 notePhase(ctx.request, "render", performance.now() - rendering);
785 return response;
762786 }
763787
764788 const capitalise = (s: string): string =>
test/migrate-timing.test.ts added +79
@@ -0,0 +1,79 @@
1 /** `/migrate` and `/migrate/jobs/:job` name their phases in Server-Timing (#3504). */
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 { createJob } from "../src/origins/jobs.ts";
10 import { quietLogs } from "./helpers/globals.ts";
11
12 quietLogs();
13
14 let root = "";
15 let app: App;
16 let servers: Servers;
17 let cookie = "";
18 let userId = "";
19
20 beforeAll(async () => {
21 root = mkdtempSync(join(tmpdir(), "forge-migrate-timing-"));
22 app = createApp({ dataDir: root, port: 0, forgeRepo: "alice/forge" });
23 const u = await createUser(app.db, {
24 origin: "seed",
25 trusted: true,
26 username: "alice",
27 email: "[email protected]",
28 password: "a-good-passphrase",
29 adminIfFirst: true,
30 });
31 if (!u.ok) throw new Error(u.error);
32 userId = u.value.id;
33 servers = await serve(app);
34 const signin = await fetch(`${servers.url}/signin`, {
35 method: "POST",
36 headers: { origin: servers.url },
37 body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }),
38 redirect: "manual",
39 });
40 cookie = (signin.headers.get("set-cookie") ?? "").split(";")[0]!;
41 });
42 afterAll(() => {
43 servers.stop();
44 app.close();
45 rmSync(root, { recursive: true, force: true });
46 });
47
48 function phases(res: Response): string[] {
49 return (res.headers.get("server-timing") ?? "").split(", ");
50 }
51
52 function expectPhases(res: Response, names: string[]): void {
53 for (const name of names)
54 expect(phases(res).find((p) => p.startsWith(`${name};`))).toMatch(
55 /^[a-z]+;dur=\d+\.\d$/,
56 );
57 }
58
59 test("GET /migrate names its phases", async () => {
60 const res = await fetch(`${servers.url}/migrate`, { headers: { cookie } });
61 expect(res.status).toBe(200);
62 await res.text();
63 expectPhases(res, ["app", "session", "origins", "agents", "render"]);
64 });
65
66 test("GET /migrate with branches asked for names the branches read", async () => {
67 const res = await fetch(`${servers.url}/migrate?branches=1`, { headers: { cookie } });
68 expect(res.status).toBe(200);
69 await res.text();
70 expectPhases(res, ["session", "origins", "agents", "branches", "render"]);
71 });
72
73 test("GET /migrate/jobs/:job names its phases", async () => {
74 const job = createJob("", "octo/streamed", { kind: "migrate", actorId: userId });
75 const res = await fetch(`${servers.url}/migrate/jobs/${job.id}`, { headers: { cookie } });
76 expect(res.status).toBe(200);
77 await res.text();
78 expectPhases(res, ["app", "session", "job", "render"]);
79 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs