Skip to content

Commit 7ade889

Browse files
committed
Update the performance regression framework to support proof verification
1 parent 5821ac3 commit 7ade889

File tree

1 file changed

+121
-66
lines changed

1 file changed

+121
-66
lines changed

src/lib/testing/perf-regression.ts

Lines changed: 121 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Regression testing framework for individual ZkProgram examples.
33
*
4-
* Stores and compares metadata such as compile & proving times.
4+
* Stores and compares metadata such as compile, proving, and verifying times.
55
* Can run in two modes:
66
* - **Dump**: write baseline results into
77
* {@link tests/perf-regression/perf-regression.json}
@@ -34,6 +34,7 @@ type MethodsInfo = Record<
3434
rows: number;
3535
digest: string;
3636
proveTime?: number;
37+
verifyTime?: number;
3738
}
3839
>;
3940

@@ -45,10 +46,10 @@ type PerfRegressionEntry = {
4546

4647
type PerfStack = {
4748
start: number;
48-
label?: 'compile' | 'prove' | string;
49+
label?: 'compile' | 'prove' | 'verify' | string;
4950
programName?: string;
5051
methodsSummary?: Record<string, ConstraintSystemSummary>;
51-
methodName?: string; // required for prove; optional for compile
52+
methodName?: string; // required for prove/verify; optional for compile
5253
};
5354

5455
const argv = minimist(process.argv.slice(2), {
@@ -72,10 +73,10 @@ if (DUMP && CHECK) {
7273
}
7374

7475
const FILE_PATH = path.isAbsolute(argv.file ?? '')
75-
? argv.file
76+
? (argv.file as string)
7677
: path.join(
7778
process.cwd(),
78-
argv.file ? argv.file : './tests/perf-regression/perf-regression.json'
79+
argv.file ? (argv.file as string) : './tests/perf-regression/perf-regression.json'
7980
);
8081

8182
// Create directory & file if missing (only on dump)
@@ -89,7 +90,7 @@ if (DUMP) {
8990
* Create a new performance tracking session for a program.
9091
*
9192
* @param programName Name of the program (key in perf-regression.json)
92-
* @param methodsSummary Optional methods analysis (required for prove checks)
93+
* @param methodsSummary Optional methods analysis (required for prove/verify checks)
9394
* @param log Optional boolean (default: true). If `--silent` is passed via CLI,
9495
* it overrides this and disables all logs.
9596
* @returns An object with `start()` and `end()` methods
@@ -106,10 +107,10 @@ function createPerformanceSession(
106107
/**
107108
* Start measuring performance for a given phase.
108109
*
109-
* @param label The phase label: `'compile' | 'prove' | string`
110-
* @param methodName Method name (required for `prove`)
110+
* @param label The phase label: `'compile' | 'prove' | 'verify' | string`
111+
* @param methodName Method name (required for `prove` and `verify`)
111112
*/
112-
start(label?: 'compile' | 'prove' | string, methodName?: string) {
113+
start(label?: 'compile' | 'prove' | 'verify' | string, methodName?: string) {
113114
perfStack.push({
114115
label,
115116
start: performance.now(),
@@ -134,75 +135,56 @@ function createPerformanceSession(
134135
const time = (performance.now() - start) / 1000;
135136

136137
// Base logging — only if log is enabled
137-
// — shows contract.method for prove
138+
// — shows contract.method for prove/verify
138139
if (shouldLog && label) {
139140
console.log(
140141
`${label} ${programName ?? ''}${
141-
label === 'prove' && methodName ? '.' + methodName : ''
142+
(label === 'prove' || label === 'verify') && methodName ? '.' + methodName : ''
142143
}... ${time.toFixed(3)} sec`
143144
);
144145
}
145146

146-
// If neither --dump nor --check, just optionally log
147+
// If neither --dump nor --check, we’re done.
147148
if (!DUMP && !CHECK) return;
148149

149-
// Only act for compile/prove with required context
150-
if (!programName || (label !== 'compile' && label !== 'prove')) return;
150+
// Only act for compile/prove/verify with required context
151+
if (!programName || (label !== 'compile' && label !== 'prove' && label !== 'verify')) return;
151152

152153
// Load the baseline JSON used for both DUMP and CHECK modes.
153-
// - In DUMP mode: merge new data with existing entries so multiple methods remain grouped.
154-
// - In CHECK mode: compare current results against stored baselines.
155154
const raw = fs.readFileSync(FILE_PATH, 'utf8');
156155
const perfRegressionJson: Record<string, PerfRegressionEntry> = JSON.parse(raw);
157156

157+
// --- compile ---
158158
if (label === 'compile') {
159-
// DUMP: update only contract-level compileTime
160159
if (DUMP) {
161160
dumpCompile(perfRegressionJson, programName, time);
162161
return;
163162
}
164-
165-
// CHECK: validate against baseline (no writes)
166163
if (CHECK) {
167164
checkCompile(perfRegressionJson, programName, time);
168165
return;
169166
}
170167
}
171168

172-
if (label === 'prove') {
173-
// Require analyzed methods summary when proving
174-
if (!cs) {
175-
throw new Error(
176-
'methodsSummary is required for "prove". Pass it to Performance.create(programName, methodsSummary).'
177-
);
178-
}
179-
180-
// Require the specific method name
181-
if (!methodName) {
182-
throw new Error(
183-
'Please provide the method name you are proving (start("prove", methodName)).'
184-
);
185-
}
169+
// --- prove / verify (shared validation + separate actions) ---
170+
if (label === 'prove' || label === 'verify') {
171+
const info = validateMethodContext(label, cs, methodName, programName);
186172

187-
// Look up the method; error if missing (also covers empty methodsSummary)
188-
const info = cs[methodName as keyof typeof cs];
189-
if (!info) {
190-
const available = Object.keys(cs);
191-
throw new Error(
192-
`The method "${methodName}" does not exist in the analyzed constraint system for "${programName}". ` +
193-
`Available: ${available.length ? available.join(', ') : '(none)'}`
194-
);
195-
}
196-
197-
// DUMP: update per-method rows/digest and proveTime; leave compileTime untouched
198173
if (DUMP) {
199-
dumpProve(perfRegressionJson, programName, methodName, info, time);
174+
if (label === 'prove') {
175+
dumpProve(perfRegressionJson, programName, methodName!, info, time);
176+
} else {
177+
dumpVerify(perfRegressionJson, programName, methodName!, info, time);
178+
}
200179
return;
201180
}
202181

203-
// CHECK: validate only, no writes
204182
if (CHECK) {
205-
checkProve(perfRegressionJson, programName, methodName, info.digest, time);
183+
if (label === 'prove') {
184+
checkProve(perfRegressionJson, programName, methodName!, info.digest, time);
185+
} else {
186+
checkVerify(perfRegressionJson, programName, methodName!, info.digest, time);
187+
}
206188
return;
207189
}
208190
}
@@ -215,17 +197,16 @@ const Performance = {
215197
* Initialize a new performance session.
216198
*
217199
* @param programName Optional identifier for the program or label.
218-
* - When provided with a ZkProgram name and its `methodsSummary`, the session
219-
* benchmarks compile and prove phases, storing or checking results against
200+
* - With a ZkProgram name and its `methodsSummary`, the session benchmarks
201+
* compile, prove, and verify phases, storing or checking results against
220202
* `perf-regression.json`.
221-
* - When used without a ZkProgram, `programName` acts as a freeform label and
222-
* the session can be used like `console.time` / `console.timeEnd` to log
223-
* timestamps for arbitrary phases.
203+
* - Without a ZkProgram, `programName` acts as a freeform label and the session
204+
* can be used like `console.time` / `console.timeEnd` to log timestamps.
224205
* @param methodsSummary Optional analysis of ZkProgram methods, required when
225-
* measuring prove performance.
206+
* measuring prove/verify performance.
226207
* @param log Optional boolean flag (default: `true`).
227208
* - When set to `false`, disables all console output for both general labels
228-
* and compile/prove phase logs.
209+
* and compile/prove/verify phase logs.
229210
* - When the `--silent` flag is provided, it overrides this setting and disables
230211
* all logging regardless of the `log` value.
231212
*/
@@ -238,7 +219,7 @@ const Performance = {
238219
},
239220
};
240221

241-
// HELPERS
222+
/// HELPERS (dump/check)
242223

243224
function dumpCompile(
244225
perfRegressionJson: Record<string, PerfRegressionEntry>,
@@ -269,13 +250,39 @@ function dumpProve(
269250
merged.methods[methodName] = {
270251
rows: info.rows,
271252
digest: info.digest,
253+
// keep any existing verifyTime if present
254+
verifyTime: merged.methods[methodName]?.verifyTime,
272255
proveTime: time,
273256
};
274257

275258
perfRegressionJson[programName] = merged;
276259
fs.writeFileSync(FILE_PATH, JSON.stringify(perfRegressionJson, null, 2));
277260
}
278261

262+
function dumpVerify(
263+
perfRegressionJson: Record<string, PerfRegressionEntry>,
264+
programName: string,
265+
methodName: string,
266+
info: ConstraintSystemSummary,
267+
time: number
268+
) {
269+
const prev = perfRegressionJson[programName];
270+
const merged: PerfRegressionEntry = prev
271+
? { ...prev, methods: { ...prev.methods } }
272+
: { methods: {} };
273+
274+
merged.methods[methodName] = {
275+
rows: info.rows,
276+
digest: info.digest,
277+
// keep any existing proveTime if present
278+
proveTime: merged.methods[methodName]?.proveTime,
279+
verifyTime: time,
280+
};
281+
282+
perfRegressionJson[programName] = merged;
283+
fs.writeFileSync(FILE_PATH, JSON.stringify(perfRegressionJson, null, 2));
284+
}
285+
279286
function checkCompile(
280287
perfRegressionJson: Record<string, PerfRegressionEntry>,
281288
programName: string,
@@ -284,7 +291,7 @@ function checkCompile(
284291
checkAgainstBaseline({
285292
perfRegressionJson,
286293
programName,
287-
label: 'compile', // compile checks don't use method/digest; pass empty strings
294+
label: 'compile',
288295
methodName: '',
289296
digest: '',
290297
actualTime,
@@ -308,14 +315,60 @@ function checkProve(
308315
});
309316
}
310317

318+
function checkVerify(
319+
perfRegressionJson: Record<string, PerfRegressionEntry>,
320+
programName: string,
321+
methodName: string,
322+
digest: string,
323+
actualTime: number
324+
) {
325+
checkAgainstBaseline({
326+
perfRegressionJson,
327+
programName,
328+
label: 'verify',
329+
methodName,
330+
digest,
331+
actualTime,
332+
});
333+
}
334+
335+
// -------------------------
336+
// HELPERS (validation + baselines)
337+
// -------------------------
338+
339+
function validateMethodContext(
340+
label: string,
341+
cs: Record<string, ConstraintSystemSummary> | undefined,
342+
methodName: string | undefined,
343+
programName?: string
344+
): ConstraintSystemSummary {
345+
if (!cs || typeof cs !== 'object') {
346+
throw new Error(
347+
`methodsSummary is required for this label: ${label}. Pass it to Performance.create(programName, methodsSummary).`
348+
);
349+
}
350+
if (!methodName) {
351+
throw new Error(`Please provide the method name (start(${label}, methodName)).`);
352+
}
353+
const info = cs[methodName];
354+
if (!info) {
355+
const available = Object.keys(cs);
356+
throw new Error(
357+
`The method "${methodName}" does not exist in the analyzed constraint system for "${programName}". ` +
358+
`Available: ${available.length ? available.join(', ') : '(none)'}`
359+
);
360+
}
361+
return info;
362+
}
363+
311364
/**
312365
* Compare a measured time/digest against stored baselines.
313366
* Throws an error if regression exceeds tolerance.
314367
*/
315368
function checkAgainstBaseline(params: {
316369
perfRegressionJson: Record<string, PerfRegressionEntry>;
317370
programName: string;
318-
label: 'compile' | 'prove';
371+
label: 'compile' | 'prove' | 'verify';
319372
methodName: string;
320373
digest: string;
321374
actualTime: number;
@@ -327,11 +380,11 @@ function checkAgainstBaseline(params: {
327380
throw new Error(`No baseline for "${programName}". Seed it with --dump first.`);
328381
}
329382

330-
// tolerances (same as other file)
383+
// tolerances
331384
const compileTol = 1.05; // 5%
332385
const compileTiny = 1.08; // for near-zero baselines
333-
const proveTolDefault = 1.1; // 10%
334-
const proveTolSmall = 1.25; // 25% for very small times (<0.2s)
386+
const timeTolDefault = 1.1; // 10% for prove/verify
387+
const timeTolSmall = 1.25; // 25% for very small times (<0.2s)
335388

336389
if (label === 'compile') {
337390
const expected = baseline.compileTime;
@@ -354,11 +407,11 @@ function checkAgainstBaseline(params: {
354407
return;
355408
}
356409

357-
// prove checks
410+
// prove/verify checks
358411
const baseMethod = baseline.methods?.[methodName];
359412
if (!baseMethod) {
360413
throw new Error(
361-
`No baseline method entry for ${programName}.${methodName}. Run --dump (prove) to add it.`
414+
`No baseline method entry for ${programName}.${methodName}. Run --dump (${label}) to add it.`
362415
);
363416
}
364417
if (baseMethod.digest !== digest) {
@@ -368,19 +421,21 @@ function checkAgainstBaseline(params: {
368421
` Expected: ${baseMethod.digest}\n`
369422
);
370423
}
371-
const expected = baseMethod.proveTime;
424+
425+
const expected = label === 'prove' ? baseMethod.proveTime : baseMethod.verifyTime;
426+
const labelPretty = label === 'prove' ? 'Prove' : 'Verify';
372427
if (expected == null) {
373428
throw new Error(
374-
`No baseline proveTime for ${programName}.${methodName}. Run --dump (prove) to set it.`
429+
`No baseline ${label}Time for ${programName}.${methodName}. Run --dump (${label}) to set it.`
375430
);
376431
}
377-
const tol = expected < 0.2 ? proveTolSmall : proveTolDefault;
432+
const tol = expected < 0.2 ? timeTolSmall : timeTolDefault;
378433
const allowedPct = (tol - 1) * 100;
379434

380435
if (actualTime > expected * tol) {
381436
const regressionPct = ((actualTime - expected) / expected) * 100;
382437
throw new Error(
383-
`Prove regression for ${programName}.${methodName}\n` +
438+
`${labelPretty} regression for ${programName}.${methodName}\n` +
384439
` Actual: ${actualTime.toFixed(3)}s\n` +
385440
` Regression: +${regressionPct.toFixed(2)}% (allowed +${allowedPct.toFixed(0)}%)`
386441
);

0 commit comments

Comments
 (0)