Skip to content

Commit 97ff9d0

Browse files
Merge pull request #1509 from corbitsdev/cl-10810-add-a-perf-regression-gate-and-frozen-baseline-for-the
ci(session): gate session-store fs.promises counts against a baseline
2 parents b5ff6b2 + 4337ea1 commit 97ff9d0

5 files changed

Lines changed: 596 additions & 1 deletion

File tree

‎.github/workflows/ci.yml‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,42 @@ jobs:
197197
- name: Test
198198
run: bun run check:projects-dir-guard
199199

200+
# scripts/context-store-perf.ts and scripts/turn-persistence-census.ts
201+
# both patch fs.promises only, so synchronous fs is invisible. The job
202+
# fails when a counted op moves past the tolerance in
203+
# scripts/check-session-store-perf-baseline.ts. It does not see sync fs,
204+
# callback fs, or Bun.file. .git mkdir and writeFile have slack for
205+
# object-id fanout; every other counted op is exact. Wall time is not gated.
206+
session-store-perf:
207+
name: session-store perf baseline
208+
runs-on: ubuntu-latest
209+
steps:
210+
- name: Checkout
211+
uses: actions/checkout@v4
212+
213+
- name: Setup Bun
214+
uses: oven-sh/setup-bun@v2
215+
with:
216+
bun-version: "1.3.14"
217+
218+
- name: Cache dependencies
219+
uses: actions/cache@v4
220+
with:
221+
path: node_modules
222+
key: bun-${{ hashFiles('bun.lock') }}
223+
224+
- name: Install dependencies
225+
run: bun install --frozen-lockfile
226+
227+
- name: Compare to the frozen baseline
228+
run: |
229+
bun scripts/context-store-perf.ts 200 --json > "$RUNNER_TEMP/context-store-perf.json"
230+
bun scripts/turn-persistence-census.ts 200 --flow=coalesced --json > "$RUNNER_TEMP/turn-persistence-census.json"
231+
bun scripts/check-session-store-perf-baseline.ts \
232+
scripts/results/session-store-perf-baseline.json \
233+
"$RUNNER_TEMP/context-store-perf.json" \
234+
"$RUNNER_TEMP/turn-persistence-census.json"
235+
200236
# protect-main still requires the pre-restructure check names. These jobs
201237
# exist only to publish those contexts after the real work succeeds.
202238
prettier:
@@ -218,7 +254,7 @@ jobs:
218254
- run: "true"
219255

220256
build-and-test:
221-
needs: [build, test]
257+
needs: [build, test, session-store-perf]
222258
runs-on: ubuntu-latest
223259
steps:
224260
- run: "true"
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { readFileSync } from "node:fs";
2+
import { join } from "node:path";
3+
import { describe, expect, test } from "bun:test";
4+
import {
5+
COUNTED_OP_TOLERANCE,
6+
GIT_FANOUT_TOLERANCE,
7+
type BaselineFile,
8+
type CensusMeasurement,
9+
type PerfMeasurement,
10+
gateSessionStoreBaseline,
11+
gateSessionStoreBaselineJson,
12+
} from "./check-session-store-perf-baseline.js";
13+
14+
const repoRoot = join(import.meta.dir, "..");
15+
const baselinePath = join(
16+
repoRoot,
17+
"scripts",
18+
"results",
19+
"session-store-perf-baseline.json",
20+
);
21+
const baselineRaw = readFileSync(baselinePath, "utf8");
22+
const ci = readFileSync(
23+
join(repoRoot, ".github", "workflows", "ci.yml"),
24+
"utf8",
25+
);
26+
27+
function parsedBaseline(): {
28+
baseline: BaselineFile;
29+
perf: PerfMeasurement;
30+
census: CensusMeasurement;
31+
} {
32+
const failures = gateSessionStoreBaselineJson(
33+
baselineRaw,
34+
JSON.stringify(JSON.parse(baselineRaw).contextStorePerf),
35+
JSON.stringify(JSON.parse(baselineRaw).turnPersistenceCensus),
36+
);
37+
expect(failures).toEqual([]);
38+
const baseline = JSON.parse(baselineRaw) as BaselineFile;
39+
return {
40+
baseline,
41+
perf: baseline.contextStorePerf,
42+
census: baseline.turnPersistenceCensus,
43+
};
44+
}
45+
46+
describe("session-store perf baseline", () => {
47+
test("checked-in snapshot matches the gate, ignoring wall time", () => {
48+
expect(COUNTED_OP_TOLERANCE).toBe(0);
49+
expect(GIT_FANOUT_TOLERANCE).toBe(64);
50+
const { baseline, perf, census } = parsedBaseline();
51+
const noisy = {
52+
...perf,
53+
wallMs: 1,
54+
perTurn: { wallMs: 99, fsOps: 0 },
55+
};
56+
expect(gateSessionStoreBaseline(baseline, noisy, census)).toEqual([]);
57+
});
58+
59+
test("a per-turn access outside .git fails; git fanout within slack passes", () => {
60+
const { baseline, perf, census } = parsedBaseline();
61+
const extraAccess: PerfMeasurement = {
62+
...perf,
63+
fsOps: { ...perf.fsOps, access: (perf.fsOps.access ?? 0) + 1 },
64+
};
65+
expect(gateSessionStoreBaseline(baseline, extraAccess, census)).not.toEqual(
66+
[],
67+
);
68+
69+
const fanout = GIT_FANOUT_TOLERANCE;
70+
const within: PerfMeasurement = {
71+
...perf,
72+
fsOps: {
73+
...perf.fsOps,
74+
mkdir: (perf.fsOps.mkdir ?? 0) + fanout,
75+
writeFile: (perf.fsOps.writeFile ?? 0) + fanout,
76+
},
77+
gitPathFsOpsByName: {
78+
...perf.gitPathFsOpsByName,
79+
mkdir: (perf.gitPathFsOpsByName.mkdir ?? 0) + fanout,
80+
writeFile: (perf.gitPathFsOpsByName.writeFile ?? 0) + fanout,
81+
},
82+
};
83+
expect(gateSessionStoreBaseline(baseline, within, census)).toEqual([]);
84+
85+
const past: PerfMeasurement = {
86+
...perf,
87+
fsOps: {
88+
...perf.fsOps,
89+
mkdir: (perf.fsOps.mkdir ?? 0) + fanout + 1,
90+
},
91+
gitPathFsOpsByName: {
92+
...perf.gitPathFsOpsByName,
93+
mkdir: (perf.gitPathFsOpsByName.mkdir ?? 0) + fanout + 1,
94+
},
95+
};
96+
const failures = gateSessionStoreBaseline(baseline, past, census);
97+
expect(failures.some((line) => line.includes(".git mkdir"))).toBe(true);
98+
});
99+
100+
test("CI runs both scripts and states the fs.promises-only blind spot", () => {
101+
expect(ci).toContain("bun scripts/context-store-perf.ts 200 --json");
102+
expect(ci).toContain(
103+
"bun scripts/turn-persistence-census.ts 200 --flow=coalesced --json",
104+
);
105+
expect(ci).toContain(
106+
"both patch fs.promises only, so synchronous fs is invisible",
107+
);
108+
expect(ci).not.toContain("synchronous fs is counted");
109+
expect(ci).not.toContain("every filesystem");
110+
});
111+
});

0 commit comments

Comments
 (0)