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

askToLiveByPillar and listEvents hold the main thread #8064

Merged opened this in answer to

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

Merged

mason/forge:mason/8061-asktolivebypillar-and-listevents-hold-th → main 3 commits fork

  • Merged. mason/8061-asktolivebypillar-and-listevents-hold-th was merged into main as 9adec1a.

Review

Sign in to review this change.
5 files changed +78 −1
AGENTS.md +2
@@ -9,3 +9,5 @@ When you write an issue, end it with a `Files:` line naming the files the work t
99 A new `forge` command is a new file in `src/cli/commands/` (exporting `command`) plus one line in its `index.ts`.
1010
1111 A new terminal screen offered only to site admins exports its palette `entry` from its own module and adds one line to `ENTRIES` in `src/tui/screens/entries.ts`; `after` names the commands it follows. Entries offered to everyone, and entries gated by a helper (`mayCapacity`, `mayManageFlags`, `canSeeMemory`, `telemetryReader` and the like) rather than by `isAdmin`, stay in `forgeCommands` in `src/tui/forge.ts`.
12
13 A new read of a table that grows (`event`, `issue`, `run`, `comment`) names the index it walks, in the migration that adds it, and a test shows it from the query plan (`test/hot-reads-indexed.test.ts` is the pattern): no `SCAN`, no `TEMP B-TREE`. Judge by plan or row count, never the clock. A read that stays slow with the best index is a summary table, kept as the rows change, before it is a worker.
src/core/migration-pins/20261102150000-event-kind-seq-merged-pull-index.pin added +1
@@ -0,0 +1,1 @@
1 c5e822146bb40540f5f5a06facc5b2feebebc7b03ec473d953deea3d4e794c64
src/core/migrations/20261102150000-event-kind-seq-merged-pull-index.ts added +9
@@ -0,0 +1,9 @@
1 // An event of one kind, newest first, walked the repository's whole event
2 // history by `seq` dropping the other kinds (or sorted every event of the
3 // kind); `event_repo_kind_seq` and `event_kind_seq` hand them back already in
4 // order (#8061). The window of merged pulls uses the existing `pull_merged`.
5 // Additive.
6 export default `
7 CREATE INDEX IF NOT EXISTS event_repo_kind_seq ON event(repo_id, kind, seq);
8 CREATE INDEX IF NOT EXISTS event_kind_seq ON event(kind, seq);
9 `;
src/core/pillars.ts +1 −1
@@ -299,7 +299,7 @@ function mergedPulls(
299299 const rows = db
300300 .query<{ id: string; body: string; at: string; oid: string | null }, string[]>(
301301 `SELECT i.id, i.body, p.merged_at AS at, p.merged_oid AS oid
302 FROM pull p JOIN issue i ON i.id = p.issue_id
302 FROM pull p INDEXED BY pull_merged CROSS JOIN issue i ON i.id = p.issue_id
303303 WHERE i.repo_id = ?1 AND p.merged_at IS NOT NULL AND p.merged_at >= ?2
304304 AND ${sealedClause("i", scope, 3)}
305305 ORDER BY p.merged_at DESC LIMIT 1000`,
test/hot-reads-indexed.test.ts added +65
@@ -0,0 +1,65 @@
1 /** The two reads that held the main thread answer from an index, not a scan or a sort (#8061). Judged by the query plan of the SQL they actually run, never the clock. */
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 { listEvents } from "../src/core/events.ts";
8 import { askToLiveByPillar } from "../src/core/pillars.ts";
9
10 let root = "";
11 let app: App;
12
13 beforeAll(() => {
14 root = mkdtempSync(join(tmpdir(), "forge-hot-reads-"));
15 app = createApp({ dataDir: root, port: 0 });
16 });
17 afterAll(() => rmSync(root, { recursive: true, force: true }));
18
19 /** The plan lines of every statement `work` runs, with its parameters bound as NULLs. */
20 function plans(work: () => void): string[] {
21 const db = app.db;
22 const original = db.query.bind(db);
23 const sqls: string[] = [];
24 (db as unknown as { query: unknown }).query = (sql: string) => {
25 sqls.push(sql);
26 return original(sql);
27 };
28 try {
29 work();
30 } finally {
31 (db as unknown as { query: unknown }).query = original;
32 }
33 return sqls
34 .filter((sql) => /^\s*SELECT/i.test(sql))
35 .flatMap((sql) => {
36 const n = Math.max(0, ...[...sql.matchAll(/\?(\d+)/g)].map((m) => Number(m[1])));
37 const args = Array.from({ length: n }, () => "x");
38 return original(`EXPLAIN QUERY PLAN ${sql}`)
39 .all(...args)
40 .map((row) => (row as { detail: string }).detail);
41 });
42 }
43
44 const noScan = (lines: string[]) => {
45 for (const line of lines) {
46 expect(line).not.toMatch(/^SCAN (?!.*USING (COVERING )?INDEX)/);
47 expect(line).not.toContain("TEMP B-TREE");
48 }
49 };
50
51 test("an event of one kind, newest first, is read in index order", () => {
52 const byRepo = plans(() => void listEvents(app.db, { repoId: "r", kind: "pull.closed", limit: 5 }));
53 expect(byRepo.join("\n")).toContain("event_repo_kind_seq");
54 noScan(byRepo);
55 const byKind = plans(() => void listEvents(app.db, { kind: "clog.cleared", limit: 1 }));
56 expect(byKind.join("\n")).toContain("event_kind_seq");
57 noScan(byKind);
58 });
59
60 test("the window of merged pulls walks the merged ones, not the repository's issues", () => {
61 const set = { pillars: [] } as never;
62 const lines = plans(() => void askToLiveByPillar(app.db, "r", set, new Map(), "2026-01-01T00:00:00Z"));
63 expect(lines.join("\n")).toContain("pull_merged");
64 expect(lines.join("\n")).not.toContain("issue_repo_updated");
65 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs