Files
PaperClipAI/scripts/benchmark-created-from-issues.ts
T
DottaandClaude Opus 5.5 8e4c5e81b9 perf(issues): read created-from tasks by index and inbox review waits by page (#20573)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The task page shows "Created from this task". The inbox shows
blocked-inbox attention for a page of tasks.
> - On a hosted database, the created-from list ran about 4,000 times in
one afternoon at about 499 ms each. The task page sends it each time a
task opens.
> - Its filter was an `OR` of three provenance paths over the list's own
rows. PostgreSQL could not use an index for it. It read every task of
the company and tested each one, with the list's other per-row
conditions (such as the chat conversation state subquery).
> - The blocked-inbox attention map also read external review waits for
every in-review task of the company, not only for the tasks of the page.
> - This pull request reads created-from tasks from three index probes,
and reads review waits only for the in-review tasks that can change a
finding of the page.
> - The benefit is less database work on each task open and each inbox
page. The responses stay the same.

## Linked Issues or Issue Description

Refs #20400 (shares review-wait reads between the attention maps of one
request).
Refs #20551 (authorizes the review waits of a page with one read; this
change narrows the waits that the page asks for, and adds a test that
pins the batched read).
Refs #13241 (adds the created-from list).

**What happened**

- `GET /api/companies/:id/issues?createdFromIssueId=…` used
`createdFromIssueCondition`. Its filter was `origin_issue_id = source OR
(origin_issue_id IS NULL AND origin_run_id IN source runs) OR (… AND
EXISTS creation activity of a source run)`. The `OR` stops index use, so
the statement read every task of the company. The list's other
conditions, which include the chat conversation state subquery, ran for
each task before the task was excluded.
- `listIssueBlockedInboxAttentionMap` read review waits for every
in-review task of the company graph. The caller uses the findings of the
page rows only.

**Expected behavior**

The created-from list reads only the created tasks. The blocked-inbox
map reads review waits only for in-review tasks that the liveness
classifier can read for a page row. The rows, the order, and the
attention values do not change.

**Steps to reproduce**

1. Use a company with tens of thousands of tasks, runs, and activity
rows.
2. Open a task page. Look at the created-from list statement: it reads
the full task set of the company.
3. Open an inbox page with blocked-inbox attention. Look at the
review-wait read: it gets the ID of every in-review task of the company.

## What Changed

- `server/src/services/issue-creation-origin.ts`:
`createdFromIssueCondition` is now `issues.id <> source AND issues.id IN
(three branches)`. Each branch starts from an index:
  - recorded origin task: `issues_company_origin_issue_idx`;
- legacy origin run: the source task's runs
(`heartbeat_runs_company_origin_issue_idx`), then the new partial index
on `issues (company_id, origin_run_id)`;
- creation activity: `activity_log_run_id_idx` for the source runs, then
the task by primary key.
The precedence does not change: a recorded origin task wins, the run
path applies only without one, and the activity path applies only
without both. The activity path matches only an `entity_id` that is the
canonical text of the task ID, as before (it also compares `id::text =
entity_id`).
- `packages/db`: add `issues_company_legacy_origin_run_idx` on `issues
(company_id, origin_run_id) WHERE origin_issue_id IS NULL AND
origin_run_id IS NOT NULL`. Migration `0461` creates it with `CREATE
INDEX CONCURRENTLY IF NOT EXISTS` and is registered with the other
concurrent migrations. `doc/DATABASE.md` documents the index and the
benchmark command.
- `server/src/services/issues.ts`: add `blockedInboxReviewWaitIssueIds`.
It returns the in-review tasks in the blocker closure of the page rows.
The blocked-inbox map reads review waits only for these tasks. The
liveness classifier reads a wait only for a row or for a blocker that it
reaches from a row, and the caller reads only the findings of the rows.
The classifier does not change.
- Tests:
- `issue-created-from-routes.test.ts`: new cases for precedence in both
directions, a source-run comment, an activity row whose task has another
origin run, a non-canonical and a non-UUID activity entity ID, the
source's own creation activity, and other-company rows.
- `blocked-inbox-review-wait-scope.test.ts`: the narrowed set for
chains, cycles, shared blockers and off-page tasks. The rows get the
same findings with the narrowed waits as with the waits of every
in-review task, and each narrowed wait is needed.
- `issue-list-review-wait-authorization.integration.test.ts`: on real
PostgreSQL with two authorized waits, `listExternalReviewWaits` and one
issue-list request with blocked-inbox attention each send one bound-run
message-link statement. With the per-wait authorization from before
#20551, both tests fail (2 statements).
- `scripts/benchmark-created-from-issues.ts`: local benchmark. It loads
the baseline `issues.ts` and `issue-creation-origin.ts` from a pinned
commit, asserts identical complete row payloads, and reports alternating
timings and `EXPLAIN (ANALYZE, BUFFERS)`.

## Verification

- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/issue-created-from-routes.test.ts
src/__tests__/blocked-inbox-review-wait-scope.test.ts
src/__tests__/issue-list-review-wait-authorization.integration.test.ts
src/__tests__/agent-task-hierarchy-depth.test.ts
src/__tests__/issue-blocker-attention.test.ts
src/__tests__/issue-list-assignee-filter-routes.test.ts
src/__tests__/issues-parent-id-alias.test.ts
src/__tests__/batched-chat-authorization.integration.test.ts
src/__tests__/issue-liveness.test.ts` passes (126 tests). One timing
test in `issue-list-assignee-filter-routes` failed one time under
machine load and passed on rerun.
- `pnpm --filter @paperclipai/server typecheck` and `pnpm --filter
@paperclipai/db check:migrations` pass.
- `pnpm verify` asks for broader verification because the change adds a
file under `scripts/`. Required CI supplies that coverage.
- Benchmark: `pnpm --filter @paperclipai/server exec tsx
../scripts/benchmark-created-from-issues.ts --baseline-ref=bd30ef8510`

Fixture (synthetic, local PostgreSQL 18.1): one company with 30,000
tasks (10,628 with a recorded origin task, 10,445 with only a legacy
origin run), 60,030 runs with wide context snapshots (145 MB), and
261,079 activity rows; a second company with 10,000 tasks and 20,000
runs. The busy source task has 63 created tasks through all three paths,
plus decoys. The quiet source task has 3. The UI's page: `sortField=id`,
`asc`, `limit=500`, routine executions included. Both versions return
identical complete payloads. 9 alternating pairs after 2 warm-up pairs:

| Case | List statement before | List statement after | List call
before, median (min to max) | List call after, median (min to max) |
| --- | --- | --- | --- | --- |
| Busy source (63 tasks) | 234.9 ms, 47,437 buffers | 0.90 ms, 437
buffers | 156.0 ms (114.5 to 480.7) | 31.6 ms (19.9 to 62.7) |
| Quiet source (3 tasks) | 90.7 ms, 47,413 buffers | 0.25 ms, 46 buffers
| 126.1 ms (90.6 to 300.4) | 10.5 ms (7.2 to 26.1) |

The list call time includes the enrichment reads (labels, runs,
attention), which do not change.

Inbox page (50 open tasks, blocked-inbox attention): the review-wait
read goes from 2 statements with 1,215 parameters (every in-review task
of the company) to 1 statement with 31 parameters. The page time does
not change in this fixture (median 801 ms before, 873 ms after, both
with a wide spread), because the fixture has no review waits and the
other graph reads of the map are most of the time. With real waits, the
server does an inherited GitHub review read for each wait it finds, so
the gain grows with the number of off-page waits.

Migration timing (local PostgreSQL 18.1): `0461` (measured as the same
statement under its earlier number) on 500,000 tasks (846 MB, 20
companies, one third with a legacy origin run) takes 1.6 s. The index is
12 MB.

## Risks

- The new index adds a small write cost to tasks with only an origin
run. New tasks with a source task record `origin_issue_id`, so the index
holds mostly older tasks. The migration does not lock writes.
- The created-from rewrite must keep the same rows. The new tests and
the benchmark payload check cover precedence, company scope, and ID text
forms. Visibility and read filters are the list's other conditions and
do not change.
- The review-wait narrowing depends on the liveness classifier reading
waits only for rows and their blocker chains. A unit test compares the
findings of the rows with full and narrowed waits. A future classifier
change that reads waits of other tasks must update
`blockedInboxReviewWaitIssueIds`. The function comment says this.
- The local timings are synthetic. Hosted acceptance needs a fresh
query-statistics window after deployment.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use (shell,
file edits, local PostgreSQL) in Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 12:38:51 -06:00

254 lines
18 KiB
TypeScript

// Local SQL benchmark for the task page "Created from this task" list
// (GET /api/companies/:id/issues?createdFromIssueId=…&includeRoutineExecutions=true,
// which the UI pages with sortField=id, sortDir=asc and limit=500).
//
// pnpm --filter @paperclipai/server exec tsx ../scripts/benchmark-created-from-issues.ts \
// --baseline-ref=<pre-fix commit> [--issues=30000] [--runs=60000] [--samples=9]
// [--evidence-dir=<dir>]
//
// Seeds one disposable embedded-PostgreSQL company with tens of thousands of
// tasks: about a third with a recorded origin task, a third with only a legacy
// origin run, and a third manual. Runs carry a realistic context snapshot and
// every agent-created task has creation activity. The source task has
// created tasks through each of the three provenance paths, plus decoys.
// The script runs the baseline (from --baseline-ref) and the working-tree
// `issueService.list` with the route's filters, for several source tasks,
// asserts identical complete row payloads, and reports alternating timings and
// EXPLAIN (ANALYZE, BUFFERS) of the list statement. An inbox case lists a page
// with blocked-inbox attention and explains its external review-wait read.
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { createDb, applyPendingMigrations, startEmbeddedPostgresTestDatabase, type Db } from "../packages/db/src/index.js";
import { issueService, type IssueFilters } from "../server/src/services/issues.js";
import { issueReadSqlCondition } from "../server/src/services/authorization.js";
const repositoryRoot = fileURLToPath(new URL("../", import.meta.url));
const git = (args: string[]) => execFileSync("git", args, { cwd: repositoryRoot, encoding: "utf8" });
const arg = (name: string) => process.argv.find((value) => value.startsWith(`--${name}=`))?.slice(name.length + 3);
const baselineRef = arg("baseline-ref");
assert.ok(baselineRef, "Pass a pinned pre-fix --baseline-ref=<commit>");
const baselineRevision = git(["rev-parse", `${baselineRef}^{commit}`]).trim();
const issueCount = Number(arg("issues") ?? 30000);
const runCount = Number(arg("runs") ?? 60000);
const samples = Number(arg("samples") ?? 9);
const evidenceDir = arg("evidence-dir");
// Baseline modules: the created-from condition and the list that embeds it.
// Every other dependency is held constant at the working tree.
const modulePaths = ["server/src/services/issues.ts", "server/src/services/issue-creation-origin.ts"];
type Captured = { sql: string; params: unknown[] };
let database: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | undefined;
let db: Db | undefined;
const keepAlive = setInterval(() => {}, 1000);
const baselineDir = await fs.mkdtemp(path.join(repositoryRoot, "server/scripts/.created-from-baseline-"));
// Everything after the copy directory exists runs inside the cleanup scope,
// so a baseline that cannot be read or loaded leaves no copied sources.
try {
for (const modulePath of modulePaths) {
const source = git(["show", `${baselineRevision}:${modulePath}`]).replace(/from "(\.{1,2}\/[^"]+)"/g, (_m, specifier: string) => {
const resolved = fileURLToPath(new URL(specifier, pathToFileURL(path.join(repositoryRoot, modulePath))));
return `from ${JSON.stringify(resolved.endsWith("/services/issue-creation-origin.js") ? path.join(baselineDir, "issue-creation-origin.ts") : resolved)}`;
});
await fs.writeFile(path.join(baselineDir, path.basename(modulePath)), source);
}
const baseline = await import(pathToFileURL(path.join(baselineDir, "issues.ts")).href) as typeof import("../server/src/services/issues.js");
database = await startEmbeddedPostgresTestDatabase("created-from-benchmark-");
await applyPendingMigrations(database.connectionString);
db = createDb(database.connectionString, { maxConnections: 8 });
const client = db.$client as any;
let capture: Captured[] | null = null;
const unsafe = client.unsafe.bind(client);
client.unsafe = (query: string, params?: unknown[], options?: unknown) => {
if (capture && /^\s*(select|with)\b/i.test(query)) capture.push({ sql: query, params: params ?? [] });
return unsafe(query, params, options);
};
await client.unsafe("set client_min_messages = warning");
const [settings] = await client.unsafe("select version(), current_setting('shared_buffers') shared_buffers, current_setting('work_mem') work_mem, current_setting('jit') jit");
const seedStart = performance.now();
const companyId = randomUUID(), otherCompanyId = randomUUID();
await client.unsafe(`insert into companies(id,name,issue_prefix) values($1,'Synthetic Origin','ORG'),($2,'Synthetic Other','OTH')`, [companyId, otherCompanyId]);
const agentIds = [randomUUID(), randomUUID(), randomUUID()];
for (const agentId of agentIds) await client.unsafe(`insert into agents(id,company_id,name,adapter_type,status) values($1,$2,$3,'process','active')`, [agentId, companyId, `Agent ${agentId.slice(0, 8)}`]);
const otherAgentId = randomUUID();
await client.unsafe(`insert into agents(id,company_id,name,adapter_type,status) values($1,$2,'Other','process','active')`, [otherAgentId, otherCompanyId]);
await client.unsafe(`create or replace function bench_h(n int, salt text) returns int language sql immutable as $$ select abs(hashtext(n::text || salt)) % 100 $$`);
// Tasks first (no origins), then runs on those tasks, then origins.
await client.unsafe(`insert into issues(id,company_id,title,description,status,identifier,issue_number,created_by_agent_id,origin_kind,created_at,updated_at)
select gen_random_uuid(),$1::uuid,'Synthetic '||n,repeat('Task description. ',40),
case when bench_h(n,'s')<55 then 'done' when bench_h(n,'s')<62 then 'cancelled' when bench_h(n,'s')<72 then 'backlog'
when bench_h(n,'s')<84 then 'todo' when bench_h(n,'s')<92 then 'in_progress' when bench_h(n,'s')<96 then 'in_review' else 'blocked' end,
'ORG-'||n,n,
case when bench_h(n,'o')<70 then ($2::uuid[])[1+n%3] else null end,
case when n%41=0 then 'routine_execution' else 'manual' end,
'2026-01-01'::timestamptz+n*interval '1 minute','2026-01-01'::timestamptz+n*interval '1 minute'
from generate_series(1,$3::int) n`, [companyId, agentIds, issueCount]);
await client.unsafe(`insert into issues(company_id,title,status,identifier,issue_number)
select $1::uuid,'Other '||n,'todo','OTH-'||n,n from generate_series(1,$2::int) n`, [otherCompanyId, Math.round(issueCount / 3)]);
await client.unsafe(`create table bench_fixture_issues as select id, issue_number n, identifier from issues where company_id=$1`, [companyId]);
await client.unsafe(`create unique index on bench_fixture_issues(n)`);
// Runs: legacy context snapshots (issueId / taskId / taskKey) and native runs,
// with a wide snapshot like real wakes.
await client.unsafe(`insert into heartbeat_runs(id,company_id,agent_id,status,runtime_mode,native_issue_id,context_snapshot,created_at)
select gen_random_uuid(),$1::uuid,($2::uuid[])[1+g%3],'succeeded',
case when g%4=0 then 'native' else 'legacy' end,
case when g%4=0 then f.id else null end,
jsonb_build_object(
case when g%7=0 then 'taskKey' when g%5=0 then 'taskId' else 'issueId' end,
case when g%7=0 then f.identifier else f.id::text end,
'wakeReason','issue_assigned','source','assignment','prompt',repeat('context ',120)),
'2026-01-01'::timestamptz+g*interval '30 seconds'
from generate_series(1,$3::int) g join bench_fixture_issues f on f.n = 1 + (g * 7919) % $4::int`, [companyId, agentIds, runCount, issueCount]);
await client.unsafe(`insert into heartbeat_runs(company_id,agent_id,status,context_snapshot)
select $1::uuid,$2::uuid,'succeeded',jsonb_build_object('issueId',gen_random_uuid()::text) from generate_series(1,$3::int)`, [otherCompanyId, otherAgentId, Math.round(runCount / 3)]);
// Origins for agent-created tasks: an earlier task (recorded) or a run (legacy).
await client.unsafe(`update issues i set
origin_issue_id = case when bench_h(f.n,'p')<50 then p.id else null end,
origin_run_id = (select r.id::text from heartbeat_runs r where r.company_id=$1 and r.created_at <= '2026-01-01'::timestamptz+f.n*interval '1 minute' order by r.created_at desc limit 1)
from bench_fixture_issues f join bench_fixture_issues p on p.n = greatest(1, f.n - 1 - (f.n * 31) % 500)
where i.id=f.id and i.created_by_agent_id is not null and f.n > 1`, [companyId]);
// Creation activity for every agent-created task, plus ordinary activity.
await client.unsafe(`insert into activity_log(company_id,entity_type,entity_id,actor_type,actor_id,action,run_id,details,created_at)
select $1::uuid,'issue',i.id::text,'agent',i.created_by_agent_id::text,
case when i.parent_id is null then 'issue.created' else 'issue.child_created' end,
case when i.origin_run_id is not null then i.origin_run_id::uuid else null end,'{}'::jsonb,i.created_at
from issues i where i.company_id=$1 and i.created_by_agent_id is not null`, [companyId]);
await client.unsafe(`insert into activity_log(company_id,entity_type,entity_id,actor_type,actor_id,action,details,created_at)
select $1::uuid,'issue',f.id::text,'agent',($2::uuid[])[1+k%3]::text,
case when k%2=0 then 'issue.updated' else 'issue.comment_added' end,'{"status":"todo"}'::jsonb,
'2026-01-20'::timestamptz+(f.n*8+k)*interval '1 second'
from bench_fixture_issues f cross join generate_series(0,7) k`, [companyId, agentIds]);
// The source tasks: a busy one with every provenance path, and a quiet one
// with only recorded origins.
const [busy] = await client.unsafe(`select id, identifier from bench_fixture_issues where n = $1`, [Math.floor(issueCount / 2)]);
const [quiet] = await client.unsafe(`select id from bench_fixture_issues where n = $1`, [Math.floor(issueCount / 3)]);
const sourceRuns: string[] = [];
for (let k = 0; k < 30; k++) {
const runId = randomUUID();
sourceRuns.push(runId);
await client.unsafe(`insert into heartbeat_runs(id,company_id,agent_id,status,runtime_mode,native_issue_id,context_snapshot)
values($1,$2,$3,'succeeded',$4,$5,$6::jsonb)`, [runId, companyId, agentIds[k % 3], k % 3 === 0 ? "native" : "legacy", k % 3 === 0 ? busy.id : null,
JSON.stringify(k % 3 === 1 ? { taskKey: busy.identifier, prompt: "x".repeat(800) } : { issueId: busy.id, prompt: "x".repeat(800) })]);
}
const pick = async (limit: number, offset: number) => (await client.unsafe(
`select id from bench_fixture_issues where n > 100 order by n limit $1 offset $2`, [limit, offset])).map((row: { id: string }) => row.id);
const recorded = await pick(40, 0);
await client.unsafe(`update issues set origin_issue_id=$1 where id = any($2::uuid[])`, [busy.id, recorded]);
const legacyRun = await pick(15, 40);
await client.unsafe(`update issues set origin_issue_id=null, origin_run_id=$1 where id = any($2::uuid[])`, [sourceRuns[1], legacyRun]);
const activityOnly = await pick(8, 55);
await client.unsafe(`update issues set origin_issue_id=null, origin_run_id=null where id = any($1::uuid[])`, [activityOnly]);
await client.unsafe(`insert into activity_log(company_id,entity_type,entity_id,actor_type,actor_id,action,run_id)
select $1::uuid,'issue',id::text,'agent','agent','issue.child_created',$2::uuid from unnest($3::uuid[]) id`, [companyId, sourceRuns[2], activityOnly]);
// Decoys: a recorded origin elsewhere wins over a source run; a source-run
// comment is not creation; another task's run.
const decoys = await pick(6, 70);
await client.unsafe(`update issues set origin_issue_id=$1, origin_run_id=$2 where id = any($3::uuid[])`, [quiet.id, sourceRuns[3], decoys.slice(0, 3)]);
await client.unsafe(`update issues set origin_issue_id=null, origin_run_id=null where id = any($1::uuid[])`, [decoys.slice(3)]);
await client.unsafe(`insert into activity_log(company_id,entity_type,entity_id,actor_type,actor_id,action,run_id)
select $1::uuid,'issue',id::text,'agent','agent','issue.comment_added',$2::uuid from unnest($3::uuid[]) id`, [companyId, sourceRuns[4], decoys.slice(3)]);
await client.unsafe("ANALYZE");
const [counts] = await client.unsafe(`select
(select count(*)::int from issues where company_id=$1) issues,
(select count(*)::int from issues where company_id=$1 and origin_issue_id is not null) recorded_origin,
(select count(*)::int from issues where company_id=$1 and origin_issue_id is null and origin_run_id is not null) legacy_run_origin,
(select count(*)::int from heartbeat_runs where company_id=$1) runs,
(select count(*)::int from activity_log where company_id=$1) activity,
(select pg_size_pretty(pg_total_relation_size('heartbeat_runs'))) runs_size,
(select pg_size_pretty(pg_total_relation_size('issues'))) issues_size`, [companyId]);
const fixtureMs = performance.now() - seedStart;
const readCondition = await issueReadSqlCondition(db, { type: "board", source: "session", userId: "board-user", isInstanceAdmin: true } as never);
// The UI's listAll page: route filters with sortField=id, asc, limit 500.
const filtersFor = (sourceIssueId: string): IssueFilters => ({
readCondition, createdFromIssueId: sourceIssueId, includeRoutineExecutions: true,
sortField: "id", sortDir: "asc", limit: 500,
});
// The inbox case is a page of issues with blocked-inbox attention: its
// review-wait read used to cover every in-review task of the company.
const cases: Record<string, { filters: IssueFilters; statement: RegExp }> = {
busy: { filters: filtersFor(busy.id), statement: /"creation_source_issue"/ },
quiet: { filters: filtersFor(quiet.id), statement: /"creation_source_issue"/ },
inbox: { filters: { readCondition, status: "todo,in_progress,in_review,blocked", includeBlockedInboxAttention: true,
sortField: "updated", sortDir: "desc", limit: 50 }, statement: /"status_decisions"."reason_code"/ },
};
const arms = { baseline: baseline.issueService(db), treatment: issueService(db) };
const results: Record<string, unknown> = {};
const stats = (values: number[]) => {
const sorted = [...values].sort((a, b) => a - b);
return { medianMs: Number(sorted[Math.floor(sorted.length / 2)]!.toFixed(2)), minMs: Number(sorted[0]!.toFixed(2)),
maxMs: Number(sorted[sorted.length - 1]!.toFixed(2)), samplesMs: values.map((value) => Number(value.toFixed(2))) };
};
for (const [name, { filters, statement }] of Object.entries(cases)) {
const times = { baseline: [] as number[], treatment: [] as number[] };
let reference: unknown;
for (let iteration = -2; iteration < samples; iteration++) {
for (const arm of (iteration % 2 === 0 ? ["baseline", "treatment"] : ["treatment", "baseline"]) as Array<keyof typeof arms>) {
const started = performance.now();
const rows = await arms[arm].list(companyId, filters);
const elapsed = performance.now() - started;
if (iteration >= 0) times[arm].push(elapsed);
const serialized = JSON.parse(JSON.stringify(rows));
if (reference === undefined) reference = serialized;
else assert.deepEqual(serialized, reference, `${name}/${arm}: complete row payloads must match`);
}
}
const resultIds = new Set((reference as Array<{ id: string }>).map((row) => row.id));
if (name === "busy") {
for (const id of [...recorded, ...legacyRun, ...activityOnly]) assert.ok(resultIds.has(id), `busy source must list ${id}`);
for (const id of decoys) assert.ok(!resultIds.has(id), `busy source must not list decoy ${id}`);
}
const plans: Record<string, unknown> = {};
for (const arm of ["baseline", "treatment"] as const) {
capture = [];
await arms[arm].list(companyId, filters);
const matching = capture.filter((query) => statement.test(query.sql));
capture = null;
assert.ok(matching.length > 0, `${arm}: list statement captured`);
let executionMs = 0, planningMs = 0, sharedBuffers = 0;
const statementPlans = [];
for (const query of matching) {
const [row] = await client.unsafe(`EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${query.sql}`, query.params);
const plan = row["QUERY PLAN"][0];
executionMs += plan["Execution Time"];
planningMs += plan["Planning Time"];
sharedBuffers += plan.Plan["Shared Hit Blocks"] + plan.Plan["Shared Read Blocks"];
statementPlans.push(plan);
}
plans[arm] = { statements: matching.length, params: matching.reduce((sum, query) => sum + query.params.length, 0),
executionMs, planningMs, sharedBuffers, plans: statementPlans };
}
results[name] = {
resultRows: resultIds.size,
baseline: stats(times.baseline), treatment: stats(times.treatment),
speedup: Number((stats(times.baseline).medianMs / stats(times.treatment).medianMs).toFixed(1)),
listStatement: Object.fromEntries(Object.entries(plans).map(([arm, value]) => {
const { statements, params, executionMs, planningMs, sharedBuffers } = value as
{ statements: number; params: number; executionMs: number; planningMs: number; sharedBuffers: number };
return [arm, { statements, params, executionMs: Number(executionMs.toFixed(3)), planningMs: Number(planningMs.toFixed(3)), sharedBuffers }];
})),
};
if (evidenceDir) {
await fs.mkdir(evidenceDir, { recursive: true });
await fs.writeFile(path.join(evidenceDir, `plans-${name}.json`), JSON.stringify(plans, null, 2));
}
}
const report = {
baselineRevision, treatmentRevision: git(["rev-parse", "HEAD"]).trim(), treatmentDirty: git(["status", "--porcelain", "--", ...modulePaths]).trim() !== "",
settings, fixture: { ...counts, fixtureMs: Math.round(fixtureMs) }, cases: results,
};
if (evidenceDir) await fs.writeFile(path.join(evidenceDir, "summary.json"), JSON.stringify(report, null, 2));
console.log(JSON.stringify(report, null, 2));
} finally {
await db?.$client.end({ timeout: 0 });
await database?.cleanup();
await fs.rm(baselineDir, { recursive: true, force: true });
clearInterval(keepAlive);
}