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

/signin and /signup name their Server-Timing phases and answer under 10 ms #4116

Merged opened this

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

Merged

scout/forge:scout/4106-signin-and-signup-name-their-server-timi → main 1 commit fork 2 threads · 2 unresolved

  • Merged. scout/4106-signin-and-signup-name-their-server-timi was merged into main as f8e4276.

Review

Sign in to review this change.
3 files changed +58 −19
src/web/pages/site.ts +15 −17
@@ -268,7 +268,7 @@ function authForm(
268268 const action = query.size === 0 ? `/${kind}` : `/${kind}?${query.toString()}`;
269269 // "Is this the first account?" is a `LIMIT 1` existence check, not a count
270270 // of the whole user table — and the sign-in screen paints on every session.
271 const firstAccount = kind === "signup" && !hasAnyUser(ctx.app.db);
271 const firstAccount = kind === "signup" && !timePhase(ctx.request, "users", () => hasAnyUser(ctx.app.db));
272272
273273 // A newcomer the edge vouched for is already through it: no door to go
274274 // round again, and the name is theirs alone.
@@ -302,7 +302,7 @@ function authForm(
302302 <button type="button" id="passkey-signin" class="btn" data-variant="primary" data-size="lg">Sign in with a passkey</button>
303303 <p class="passkey-note small" id="passkey-signin-note" role="status"></p>
304304 </div>
305 ${when(SIGNUP_SENTENCES[getSignup(ctx.app.db)], (said) => html`<p class="muted small mb" id="signup-mode">${said}</p>`)}`,
305 ${when(SIGNUP_SENTENCES[timePhase(ctx.request, "settings", () => getSignup(ctx.app.db))], (said) => html`<p class="muted small mb" id="signup-mode">${said}</p>`)}`,
306306 );
307307
308308 const body = html`${unknownLink}
@@ -412,7 +412,7 @@ function authForm(
412412
413413 // A spawned forge nobody has claimed yet: its person may arrive here with
414414 // the code from their link but not the link itself (#730).
415 const unowned = !hasAdmin(ctx.app.db);
415 const unowned = !timePhase(ctx.request, "claim", () => hasAdmin(ctx.app.db));
416416 const claimable =
417417 existsSync(join(ctx.app.config.dataDir, "parent.json")) && unowned;
418418 return html`<div class="narrow">
@@ -437,12 +437,11 @@ function authForm(
437437 }
438438
439439 export function signinPage(ctx: Ctx): Response {
440 return page({
441 ctx,
442 title: "Sign in",
443 body: authForm(ctx, "signin"),
444 scriptJs: passkeyJs,
445 });
440 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
441 const body = authForm(ctx, "signin");
442 return timePhase(ctx.request, "render", () =>

authForm runs outside any timePhase, so its cost is unattributed. Wrap it in a named phase (e.g. "form") so a slow sign-in page shows where the time goes.

443 page({ ctx, title: "Sign in", body, scriptJs: passkeyJs }),
444 );
446445 }
447446
448447 /** The widget's public site key, when signup is gated on Turnstile. */
@@ -471,16 +470,15 @@ function signupClosedPage(ctx: Ctx): Response {
471470 }
472471
473472 export function signupPage(ctx: Ctx): Response {
474 if (signupClosed(ctx.app, inviteOf(ctx))) return signupClosedPage(ctx);
473 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
474 if (timePhase(ctx.request, "signup", () => signupClosed(ctx.app, inviteOf(ctx))))
475 return timePhase(ctx.request, "render", () => signupClosedPage(ctx));
475476 const first = throughEdgeFirst(ctx);
476477 if (first) return first;
477 return page({
478 ctx,
479 title: "Sign up",
480 body: authForm(ctx, "signup"),
481 turnstile: siteKey(ctx) !== undefined,
482 scriptJs: passkeyJs,
483 });
478 const body = authForm(ctx, "signup");
479 return timePhase(ctx.request, "render", () =>
480 page({ ctx, title: "Sign up", body, turnstile: siteKey(ctx) !== undefined, scriptJs: passkeyJs }),
481 );
484482 }
485483
486484 /**
test/server-timing-sweep.test.ts −2
@@ -52,10 +52,8 @@ const UNNAMED: Array<[path: string, issue: string]> = [
5252 ["/settings/password", "#3390"],
5353 ["/settings/tokens", "#3390"],
5454 ["/settings/usage", "#3279"],
55 ["/signin", "#3390"],
5655 ["/signin/parent", "#3390"],
5756 ["/signin/parent/callback", "#3390"],
58 ["/signup", "#3390"],
5957 ["/verify", "#3390"],
6058 ];
6159
test/signin-timing.test.ts added +43
@@ -0,0 +1,43 @@
1 /** `/signin` and `/signup` name session, each read and render in Server-Timing, and answer under 10 ms (#4106). */
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 { serve, type Servers } from "../src/http/server.ts";
8 import { quietLogs } from "./helpers/globals.ts";
9
10 quietLogs();
11
12 let root = "";
13 let app: App;
14 let servers: Servers;
15
16 beforeAll(async () => {
17 root = mkdtempSync(join(tmpdir(), "forge-signin-timing-"));
18 app = createApp({ dataDir: root, port: 0 });
19 servers = await serve(app);
20 });
21 afterAll(() => {
22 servers.stop();
23 app.close();
24 rmSync(root, { recursive: true, force: true });
25 });
26
27 const PAGES: [string, string[]][] = [
28 ["/signin", ["session", "settings", "claim", "render"]],
29 ["/signup", ["session", "signup", "users", "claim", "render"]],
30 ];
31
32 for (const [path, names] of PAGES)
33 test(`GET ${path} names its phases and answers under 10 ms`, async () => {
34 await fetch(`${servers.url}${path}`); // warm
35 const res = await fetch(`${servers.url}${path}`);
36 await res.text();
37 expect(res.status).toBe(200);
38 const parts = (res.headers.get("server-timing") ?? "").split(", ");
39 for (const name of ["app", ...names])
40 expect(parts.find((p) => p.startsWith(`${name};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/);
41 const app = parts.find((p) => p.startsWith("app;"))!;

This const app shadows the App declared at the top of the file. Rename it (e.g. appPart) to keep it readable.

42 expect(Number(app.slice("app;dur=".length))).toBeLessThan(10);
43 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs