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

The signed-out front page's shelf is read under 10 ms #6133

Merged opened this

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

Merged

scout/forge:scout/6126-the-signed-out-front-page-s-shelf-is-rea → main 2 commits fork 3 threads · 3 unresolved

  • Merged. scout/6126-the-signed-out-front-page-s-shelf-is-rea was merged into main as c54b333.

Review

Sign in to review this change.
2 files changed +136 −0
src/actions/front.ts +31
@@ -150,10 +150,41 @@ function scoresFor(app: App, repoIds: readonly string[], now: number): Scores {
150150 /** Forget the remembered ranking: the picks changed. */
151151 export function forgetFrontScores(app: App): void {
152152 remembered.delete(app);
153 built.delete(app);
154 }
155
156 /** How long a built page is kept when nothing in the database changed: an app's state is not in it. */
157 const PAGE_MS = 3_000;
158
159 /**
160 * The built page, kept for `PAGE_MS` while the database is untouched: any
161 * write on this connection, or by another, changes the key, so a repository
162 * made private, a pick, a picture or a suspension shows on the next request.
163 */
164 const built = new WeakMap<App, { key: string; at: number; page: FrontPage }>();
165
166 function dbKey(app: App): string {
167 const row = app.db
168 .query<{ c: number; v: number }, []>(
169 "SELECT total_changes() AS c, (SELECT data_version FROM pragma_data_version) AS v",
170 )
171 .get();
172 return `${row?.c}:${row?.v}`;
153173 }
154174
155175 /** The front page as a stranger is shown it. */
156176 export function frontPage(app: App, now = Date.now()): FrontPage {
177 const key = dbKey(app);
178 const kept = built.get(app);
179 const wall = performance.now();

Low: on a hit the page returned was built with an earlier now, so a caller passing its own now (as tests do) can get an answer that ignores it. Either include now in the condition (now - kept.now < PAGE_MS, dropping performance.now()) or note in the doc comment that now only counts on a rebuild.

180 if (kept !== undefined && kept.key === key && wall - kept.at < PAGE_MS)
181 return kept.page;
182 const page = buildFrontPage(app, now);
183 built.set(app, { key, at: wall, page });
184 return page;
185 }
186
187 function buildFrontPage(app: App, now: number): FrontPage {
157188 const db = app.db;
158189 const picks = frontPicks(db);
159190 const records = listPublicApps(db, READ);
test/front-speed.test.ts added +105
@@ -0,0 +1,105 @@
1 /** The signed-out front page is kept between requests, and any write or a forgotten ranking drops it (#6126). */
2 import { afterAll, beforeAll, expect, spyOn, test } from "bun:test";
3 import { mkdirSync, writeFileSync } from "node:fs";
4 import { mkdtemp, rm } from "node:fs/promises";
5 import { tmpdir } from "node:os";
6 import { join } from "node:path";
7 import { createApp, type App } from "../src/app.ts";
8 import { forgetFrontScores, frontPage } from "../src/actions/front.ts";
9 import { createRepo } from "../src/actions/repos.ts";
10 import { AppsRuntime, type EnvStatus } from "../src/apps/runtime.ts";
11 import { createApp as insertApp, type AppRecord } from "../src/core/apps.ts";
12 import { recordLook } from "../src/core/looks.ts";
13 import { encodePng } from "../src/design/image/png.ts";
14 import { createUser, type User } from "../src/core/users.ts";
15 import { quietLogs } from "./helpers/globals.ts";
16 import { promote } from "./helpers/members.ts";
17
18 quietLogs();
19
20 const HEAD = "a".repeat(40);
21 const PNG = Buffer.from(encodePng(1, 1, new Uint8Array([255, 255, 255, 255])));
22 const APPS = Number(process.env.N ?? 30);
23
24 let dataDir = "";
25 let app: App;
26 let runtime: AppsRuntime;
27 let restore: () => void;
28 const repoIds: string[] = [];
29
30 beforeAll(async () => {
31 dataDir = await mkdtemp(join(tmpdir(), "forge-front-speed-"));
32 app = createApp({ dataDir, port: 0 });
33 const made = await createUser(app.db, {
34 origin: "seed",
35 trusted: true,
36 username: "owner",
37 allowReserved: true,
38 email: "[email protected]",
39 password: "a-good-passphrase",
40 adminIfFirst: true,
41 });
42 if (!made.ok) throw new Error(made.error);
43 promote(app.db, made.value);
44 const owner: User = made.value;
45 runtime = new AppsRuntime(app, {
46 domain: "front.test",
47 scheme: "http",
48 publicPort: 0,
49 forgeUrl: "http://localhost",
50 });
51 const status = spyOn(AppsRuntime.prototype, "status").mockImplementation(
52 (): EnvStatus[] => [{ env: "production", state: "running", commitOid: HEAD } as EnvStatus],
53 );
54 restore = () => status.mockRestore();
55 for (let n = 0; n < APPS; n++) {
56 const name = `speed${n}`;
57 const repo = await createRepo(app, owner, { name, visibility: "public", description: name });
58 if (!repo.ok) throw new Error(String(repo.error));
59 const record = insertApp(app.db, { repoId: repo.value.id, slug: name, access: "public" });
60 if (!record.ok) throw new Error(record.error);
61 const id = (record.value as AppRecord).id;
62 mkdirSync(join(dataDir, "looks", id), { recursive: true });
63 writeFileSync(join(dataDir, "looks", id, "thumb.png"), PNG);
64 recordLook(app.db, {
65 appId: id,
66 env: "production",
67 commitOid: HEAD,
68 kind: "thumb",
69 path: `looks/${id}/thumb.png`,
70 });
71 repoIds.push(repo.value.id);
72 }
73 }, 60_000);
74
75 afterAll(async () => {
76 restore();
77 await runtime.stop();
78 app.close();
79 await rm(dataDir, { recursive: true, force: true });
80 });
81
82 test("a second request is the page already built, not a new one", () => {
83 const first = frontPage(app);
84 expect(first.total).toBe(APPS);

This passes with no cache at 30 apps. Assert identity of the returned page on the second call (toBe(first)), which fails without the cache. Remove the console.log and the timing threshold.

85 expect(first.shelf.length).toBeGreaterThan(0);
86 expect(frontPage(app)).toBe(first);
87 });
88
89 test("forgetting the front scores drops the kept page", () => {
90 const first = frontPage(app);
91 expect(frontPage(app)).toBe(first);
92 forgetFrontScores(app);
93 expect(frontPage(app)).not.toBe(first);
94 });
95
96 test("making one app private builds a new page without it on the next request", () => {
97 const before = frontPage(app);
outdated

This also passes without the cache. Pair it with: warm call returns the same object, then after the UPDATE a different object. That is the test that would fail if the key were wrong.

98 expect(frontPage(app)).toBe(before);
99 const gone = before.shelf[0]!.repo;
100 app.db.query("UPDATE repo SET visibility = 'private' WHERE id = ?1").run(gone.id);
101 const after = frontPage(app);
102 expect(after).not.toBe(before);
103 expect(after.total).toBe(APPS - 1);
104 expect([after.featured, ...after.shelf].some((e) => e?.repo.id === gone.id)).toBe(false);
105 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs