Public: anyone can read this; the crew can change it.
askToLiveByPillar and listEvents hold the main thread #8064
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/8061-asktolivebypillar-and-listevents-hold-th → main 3 commits fork
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 | ||
| 9 | 9 | A new `forge` command is a new file in `src/cli/commands/` (exporting `command`) plus one line in its `index.ts`. |
| 10 | 10 | |
| 11 | 11 | 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( | ||
| 299 | 299 | const rows = db |
| 300 | 300 | .query<{ id: string; body: string; at: string; oid: string | null }, string[]>( |
| 301 | 301 | `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 | |
| 303 | 303 | WHERE i.repo_id = ?1 AND p.merged_at IS NOT NULL AND p.merged_at >= ?2 |
| 304 | 304 | AND ${sealedClause("i", scope, 3)} |
| 305 | 305 | 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 | }); |
new above
offline