@alejoamiras/aztec-benchmark 0.0.0-canary.gb8d1485
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +340 -0
- package/action/action.yml +44 -0
- package/action/comparison.cjs +408 -0
- package/action/dist/index.cjs +3 -0
- package/action/index.cjs +93 -0
- package/bin/aztec-benchmark +21 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +145 -0
- package/dist/feeWrappedInteraction.d.ts +35 -0
- package/dist/feeWrappedInteraction.js +62 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +8 -0
- package/dist/profiler.d.ts +32 -0
- package/dist/profiler.js +197 -0
- package/dist/systemInfo.d.ts +15 -0
- package/dist/systemInfo.js +61 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.js +3 -0
- package/package.json +54 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Formats system info as a markdown table.
|
|
6
|
+
* @param {object} systemInfo - The system info object from benchmark JSON.
|
|
7
|
+
* @returns {string} Markdown table showing system hardware info.
|
|
8
|
+
*/
|
|
9
|
+
function formatSystemInfoTable(systemInfo) {
|
|
10
|
+
if (!systemInfo) {
|
|
11
|
+
return '| CPU | Cores | RAM | Arch |\n|-----|-------|-----|------|\n| N/A | N/A | N/A | N/A |\n';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const cpu = systemInfo.cpuModel || 'N/A';
|
|
15
|
+
const cores = systemInfo.cpuCores || 'N/A';
|
|
16
|
+
const ram = systemInfo.totalMemoryGiB ? `${systemInfo.totalMemoryGiB} GiB` : 'N/A';
|
|
17
|
+
const arch = systemInfo.arch || 'N/A';
|
|
18
|
+
|
|
19
|
+
return [
|
|
20
|
+
'| CPU | Cores | RAM | Arch |',
|
|
21
|
+
'|-----|-------|-----|------|',
|
|
22
|
+
`| ${cpu} | ${cores} | ${ram} | ${arch} |`,
|
|
23
|
+
'',
|
|
24
|
+
].join('\n');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extracts DA (Data Availability) gas from a benchmark result.
|
|
29
|
+
* @param {object} result - The benchmark result object.
|
|
30
|
+
* @returns {number} The DA gas value, or 0 if not found.
|
|
31
|
+
*/
|
|
32
|
+
const getDaGas = (result) => result?.gas?.gasLimits?.daGas ?? 0;
|
|
33
|
+
/**
|
|
34
|
+
* Extracts L2 gas from a benchmark result.
|
|
35
|
+
* @param {object} result - The benchmark result object.
|
|
36
|
+
* @returns {number} The L2 gas value, or 0 if not found.
|
|
37
|
+
*/
|
|
38
|
+
const getL2Gas = (result) => result?.gas?.gasLimits?.l2Gas ?? 0;
|
|
39
|
+
/**
|
|
40
|
+
* Extracts proving time from a benchmark result.
|
|
41
|
+
* @param {object} result - The benchmark result object.
|
|
42
|
+
* @returns {number} The proving time in milliseconds, or 0 if not found.
|
|
43
|
+
*/
|
|
44
|
+
const getProvingTime = (result) => result?.provingTime ?? 0;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Formats the difference between two numbers as a string, including percentage change.
|
|
48
|
+
* Handles cases like zero main value (infinite increase) or zero pr value (100% decrease).
|
|
49
|
+
* @param {number} main - The base value.
|
|
50
|
+
* @param {number} pr - The new value (from Pull Request).
|
|
51
|
+
* @returns {string} A formatted string representing the difference, or an empty string if no significant change.
|
|
52
|
+
*/
|
|
53
|
+
const formatDiff = (main, pr) => {
|
|
54
|
+
if (main === 0 && pr === 0) return ''; // Use empty string for no change if both zero
|
|
55
|
+
if (main === 0) return '+Inf%'; // Handle infinite increase
|
|
56
|
+
if (pr === 0) return '-100%'; // Handle 100% decrease
|
|
57
|
+
|
|
58
|
+
const diff = pr - main;
|
|
59
|
+
if (diff === 0) return ''; // Use empty string for no change
|
|
60
|
+
|
|
61
|
+
const pct = (diff / main) * 100;
|
|
62
|
+
const sign = diff > 0 ? '+' : '';
|
|
63
|
+
|
|
64
|
+
if (Math.abs(pct) < 0.01 && Math.abs(diff) < 1) return ''; // Threshold for small changes
|
|
65
|
+
// Format with commas and show percentage
|
|
66
|
+
return `${sign}${diff.toLocaleString()} (${sign}${pct.toFixed(1)}%)`;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Determines an emoji status based on benchmark metric changes and a threshold.
|
|
71
|
+
* @param {object} metrics - An object containing main and pr values for gates, daGas, and l2Gas.
|
|
72
|
+
* @param {number} threshold - The percentage threshold for significant change.
|
|
73
|
+
* @returns {string} An emoji: '🚮' for removed, '🆕' for new, '🔴' for regression, '🟢' for improvement, '⚪' for no significant change.
|
|
74
|
+
*/
|
|
75
|
+
const getStatusEmoji = (metrics, threshold) => {
|
|
76
|
+
const isRemoved =
|
|
77
|
+
metrics.gates.pr === 0 &&
|
|
78
|
+
metrics.daGas.pr === 0 &&
|
|
79
|
+
metrics.l2Gas.pr === 0 &&
|
|
80
|
+
(metrics.gates.main > 0 || metrics.daGas.main > 0 || metrics.l2Gas.main > 0);
|
|
81
|
+
const isNew =
|
|
82
|
+
metrics.gates.main === 0 &&
|
|
83
|
+
metrics.daGas.main === 0 &&
|
|
84
|
+
metrics.l2Gas.main === 0 &&
|
|
85
|
+
(metrics.gates.pr > 0 || metrics.daGas.pr > 0 || metrics.l2Gas.pr > 0);
|
|
86
|
+
|
|
87
|
+
if (isRemoved) return '🚮';
|
|
88
|
+
if (isNew) return '🆕';
|
|
89
|
+
|
|
90
|
+
// Avoid division by zero, handle infinite increases
|
|
91
|
+
const gateDiffPct =
|
|
92
|
+
metrics.gates.main === 0
|
|
93
|
+
? metrics.gates.pr > 0
|
|
94
|
+
? Infinity
|
|
95
|
+
: 0
|
|
96
|
+
: (metrics.gates.pr - metrics.gates.main) / metrics.gates.main;
|
|
97
|
+
const daGasDiffPct =
|
|
98
|
+
metrics.daGas.main === 0
|
|
99
|
+
? metrics.daGas.pr > 0
|
|
100
|
+
? Infinity
|
|
101
|
+
: 0
|
|
102
|
+
: (metrics.daGas.pr - metrics.daGas.main) / metrics.daGas.main;
|
|
103
|
+
const l2GasDiffPct =
|
|
104
|
+
metrics.l2Gas.main === 0
|
|
105
|
+
? metrics.l2Gas.pr > 0
|
|
106
|
+
? Infinity
|
|
107
|
+
: 0
|
|
108
|
+
: (metrics.l2Gas.pr - metrics.l2Gas.main) / metrics.l2Gas.main;
|
|
109
|
+
|
|
110
|
+
const metricsDiffs = [gateDiffPct, daGasDiffPct, l2GasDiffPct].filter((m) => isFinite(m));
|
|
111
|
+
const hasInfiniteIncrease = [gateDiffPct, daGasDiffPct, l2GasDiffPct].some((m) => m === Infinity);
|
|
112
|
+
|
|
113
|
+
// Use threshold percentage directly
|
|
114
|
+
const thresholdDecimal = threshold / 100.0;
|
|
115
|
+
|
|
116
|
+
const hasRegression = hasInfiniteIncrease || metricsDiffs.some((m) => m > thresholdDecimal);
|
|
117
|
+
const hasImprovement = metricsDiffs.some((m) => m < -thresholdDecimal);
|
|
118
|
+
|
|
119
|
+
if (hasRegression) return '🔴'; // Regression
|
|
120
|
+
if (hasImprovement) return '🟢'; // Improvement
|
|
121
|
+
return '⚪'; // No significant change / within threshold
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Finds pairs of benchmark report files (base and PR/latest) in a directory.
|
|
126
|
+
* Now includes new contracts that only have PR reports (no corresponding base report).
|
|
127
|
+
* @param {string} reportsDir - The directory containing benchmark reports.
|
|
128
|
+
* @param {string} baseSuffix - The suffix for base report filenames (e.g., '_base').
|
|
129
|
+
* @param {string} prSuffix - The suffix for PR/latest report filenames (e.g., '_latest').
|
|
130
|
+
* @returns {Array<object>} An array of pairs, each with contractName, baseJsonPath (or null), and prJsonPath.
|
|
131
|
+
*/
|
|
132
|
+
function findBenchmarkPairs(reportsDir, baseSuffix, prSuffix) {
|
|
133
|
+
const pairs = [];
|
|
134
|
+
const prSuffixPattern = `${prSuffix}.benchmark.json`;
|
|
135
|
+
const baseSuffixPattern = `${baseSuffix}.benchmark.json`;
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const files = fs.readdirSync(reportsDir);
|
|
139
|
+
for (const file of files) {
|
|
140
|
+
if (file.endsWith(prSuffixPattern)) {
|
|
141
|
+
// Extract contract name from PR filename
|
|
142
|
+
const contractName = file.substring(0, file.length - prSuffixPattern.length);
|
|
143
|
+
// Construct expected baseline filename
|
|
144
|
+
const baseFilename = `${contractName}${baseSuffixPattern}`;
|
|
145
|
+
const baseJsonPath = path.join(reportsDir, baseFilename);
|
|
146
|
+
const prJsonPath = path.join(reportsDir, file);
|
|
147
|
+
|
|
148
|
+
// Include all PR reports, whether or not they have a corresponding base report
|
|
149
|
+
pairs.push({
|
|
150
|
+
contractName,
|
|
151
|
+
baseJsonPath: fs.existsSync(baseJsonPath) ? baseJsonPath : null,
|
|
152
|
+
prJsonPath,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
// Handle cases where the directory doesn't exist
|
|
158
|
+
if (error.code === 'ENOENT') {
|
|
159
|
+
console.warn(`Reports directory not found: ${reportsDir}`);
|
|
160
|
+
} else {
|
|
161
|
+
console.error(`Error reading reports directory ${reportsDir}:`, error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return pairs;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Generates an expandable circuit breakdown section for all functions in a contract.
|
|
169
|
+
* Uses <details>/<summary> HTML for a collapsible view placed below the summary table.
|
|
170
|
+
* @param {object} comparison - The comparison object keyed by function name.
|
|
171
|
+
* @param {string[]} sortedNames - Sorted function names.
|
|
172
|
+
* @returns {string} HTML string with the expandable circuit breakdown, or empty string if no data.
|
|
173
|
+
*/
|
|
174
|
+
function generateCircuitBreakdownSection(comparison, sortedNames, contractName) {
|
|
175
|
+
const hasAnyCircuitData = sortedNames.some((name) => {
|
|
176
|
+
const gc = comparison[name]?.gateCounts;
|
|
177
|
+
return gc && gc.pr.length > 0;
|
|
178
|
+
});
|
|
179
|
+
if (!hasAnyCircuitData) return '';
|
|
180
|
+
|
|
181
|
+
const lines = ['<details>', `<summary>🔎 ${contractName} circuit details</summary>`, ''];
|
|
182
|
+
|
|
183
|
+
for (const funcName of sortedNames) {
|
|
184
|
+
const gc = comparison[funcName]?.gateCounts;
|
|
185
|
+
if (!gc || gc.pr.length === 0) continue;
|
|
186
|
+
|
|
187
|
+
lines.push(`#### \`${funcName}\``, '', '| Circuit | Gates |', '|---------|---:|');
|
|
188
|
+
|
|
189
|
+
for (const entry of gc.pr) {
|
|
190
|
+
lines.push(`| \`${entry.circuitName}\` | ${entry.gateCount.toLocaleString()} |`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
lines.push('');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
lines.push('</details>');
|
|
197
|
+
return lines.join('\n');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Generates an HTML table comparing benchmark results for a single contract.
|
|
202
|
+
* Handles new contracts where baseJsonPath may be null (no base report exists).
|
|
203
|
+
* @param {object} pair - An object containing contractName, baseJsonPath (or null), and prJsonPath.
|
|
204
|
+
* @param {number} threshold - The percentage threshold for highlighting regressions.
|
|
205
|
+
* @returns {string} An HTML string representing the comparison table, or an error message.
|
|
206
|
+
*/
|
|
207
|
+
function generateContractComparisonTable(pair, threshold, { circuitDetails = false } = {}) {
|
|
208
|
+
const { contractName, baseJsonPath, prJsonPath } = pair;
|
|
209
|
+
const isNewContract = baseJsonPath === null;
|
|
210
|
+
|
|
211
|
+
if (isNewContract) {
|
|
212
|
+
console.log(` Generating report for new contract: ${contractName} in ${prJsonPath}`);
|
|
213
|
+
} else {
|
|
214
|
+
console.log(` Comparing: ${baseJsonPath} vs ${prJsonPath}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check that PR report exists
|
|
218
|
+
if (!fs.existsSync(prJsonPath)) {
|
|
219
|
+
return `*Error: PR report file missing for ${contractName}: ${prJsonPath}*`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Check that base report exists (if not a new contract)
|
|
223
|
+
if (!isNewContract && !fs.existsSync(baseJsonPath)) {
|
|
224
|
+
return `*Error: Base report file missing for ${contractName}: ${baseJsonPath}*`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let mainData, prData;
|
|
228
|
+
try {
|
|
229
|
+
// For new contracts, use empty data structure for base
|
|
230
|
+
if (isNewContract) {
|
|
231
|
+
mainData = { results: [] };
|
|
232
|
+
} else {
|
|
233
|
+
mainData = JSON.parse(fs.readFileSync(baseJsonPath, 'utf-8'));
|
|
234
|
+
}
|
|
235
|
+
prData = JSON.parse(fs.readFileSync(prJsonPath, 'utf-8'));
|
|
236
|
+
} catch (e) {
|
|
237
|
+
return `*Error parsing benchmark JSON for ${contractName}: ${e.message}*`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!mainData || !mainData.results || !prData || !prData.results) {
|
|
241
|
+
return `*Skipping ${contractName}: Invalid JSON structure (missing results array).*`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const comparison = {};
|
|
245
|
+
const allFunctionNames = new Set([...mainData.results.map((r) => r.name), ...prData.results.map((r) => r.name)]);
|
|
246
|
+
|
|
247
|
+
for (const name of allFunctionNames) {
|
|
248
|
+
if (
|
|
249
|
+
!name ||
|
|
250
|
+
name.startsWith('unknown_function') ||
|
|
251
|
+
name.includes('(FAILED)') ||
|
|
252
|
+
name === 'BENCHMARK_RUNNER_ERROR'
|
|
253
|
+
) {
|
|
254
|
+
console.log(` Skipping comparison for malformed/failed entry: ${name}`);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const mainResult = mainData.results.find((r) => r.name === name);
|
|
258
|
+
const prResult = prData.results.find((r) => r.name === name);
|
|
259
|
+
|
|
260
|
+
comparison[name] = {
|
|
261
|
+
gates: { main: mainResult?.totalGateCount ?? 0, pr: prResult?.totalGateCount ?? 0 },
|
|
262
|
+
daGas: { main: getDaGas(mainResult), pr: getDaGas(prResult) },
|
|
263
|
+
l2Gas: { main: getL2Gas(mainResult), pr: getL2Gas(prResult) },
|
|
264
|
+
provingTime: { main: getProvingTime(mainResult), pr: getProvingTime(prResult) },
|
|
265
|
+
gateCounts: { main: mainResult?.gateCounts ?? [], pr: prResult?.gateCounts ?? [] },
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const output = [
|
|
270
|
+
'<table>',
|
|
271
|
+
'<thead>',
|
|
272
|
+
'<tr>',
|
|
273
|
+
'<th>🚦</th>',
|
|
274
|
+
'<th>Function</th>',
|
|
275
|
+
'<th colspan="3">Gates</th>',
|
|
276
|
+
'<th colspan="3">DA Gas</th>',
|
|
277
|
+
'<th colspan="3">L2 Gas</th>',
|
|
278
|
+
'<th colspan="3">Proving Time (ms)</th>',
|
|
279
|
+
'</tr>',
|
|
280
|
+
'<tr>',
|
|
281
|
+
'<th></th>',
|
|
282
|
+
'<th></th>',
|
|
283
|
+
'<th>Base</th>',
|
|
284
|
+
'<th>PR</th>',
|
|
285
|
+
'<th>Diff</th>',
|
|
286
|
+
'<th>Base</th>',
|
|
287
|
+
'<th>PR</th>',
|
|
288
|
+
'<th>Diff</th>',
|
|
289
|
+
'<th>Base</th>',
|
|
290
|
+
'<th>PR</th>',
|
|
291
|
+
'<th>Diff</th>',
|
|
292
|
+
'<th>Base</th>',
|
|
293
|
+
'<th>PR</th>',
|
|
294
|
+
'<th>Diff</th>',
|
|
295
|
+
'</tr>',
|
|
296
|
+
'</thead>',
|
|
297
|
+
'<tbody>',
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
const sortedNames = Object.keys(comparison).sort();
|
|
301
|
+
|
|
302
|
+
if (sortedNames.length === 0) {
|
|
303
|
+
return '*No comparable functions found between reports.*';
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
for (const funcName of sortedNames) {
|
|
307
|
+
const metrics = comparison[funcName];
|
|
308
|
+
if (!metrics) continue;
|
|
309
|
+
|
|
310
|
+
const statusEmoji = getStatusEmoji(metrics, threshold);
|
|
311
|
+
const ptMain = metrics.provingTime.main > 0 ? Math.round(metrics.provingTime.main).toLocaleString() : 'N/A';
|
|
312
|
+
const ptPr = metrics.provingTime.pr > 0 ? Math.round(metrics.provingTime.pr).toLocaleString() : 'N/A';
|
|
313
|
+
const ptDiff = formatDiff(Math.round(metrics.provingTime.main), Math.round(metrics.provingTime.pr));
|
|
314
|
+
output.push(
|
|
315
|
+
'<tr>',
|
|
316
|
+
`<td align="center">${statusEmoji}</td>`,
|
|
317
|
+
`<td><code>${funcName}</code></td>`,
|
|
318
|
+
// Gates
|
|
319
|
+
`<td align="right">${metrics.gates.main.toLocaleString()}</td>`,
|
|
320
|
+
`<td align="right">${metrics.gates.pr.toLocaleString()}</td>`,
|
|
321
|
+
`<td align="right">${formatDiff(metrics.gates.main, metrics.gates.pr)}</td>`,
|
|
322
|
+
// DA Gas
|
|
323
|
+
`<td align="right">${metrics.daGas.main.toLocaleString()}</td>`,
|
|
324
|
+
`<td align="right">${metrics.daGas.pr.toLocaleString()}</td>`,
|
|
325
|
+
`<td align="right">${formatDiff(metrics.daGas.main, metrics.daGas.pr)}</td>`,
|
|
326
|
+
// L2 Gas
|
|
327
|
+
`<td align="right">${metrics.l2Gas.main.toLocaleString()}</td>`,
|
|
328
|
+
`<td align="right">${metrics.l2Gas.pr.toLocaleString()}</td>`,
|
|
329
|
+
`<td align="right">${formatDiff(metrics.l2Gas.main, metrics.l2Gas.pr)}</td>`,
|
|
330
|
+
// Proving Time
|
|
331
|
+
`<td align="right">${ptMain}</td>`,
|
|
332
|
+
`<td align="right">${ptPr}</td>`,
|
|
333
|
+
`<td align="right">${ptDiff}</td>`,
|
|
334
|
+
'</tr>',
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
output.push('</tbody>', '</table>');
|
|
339
|
+
|
|
340
|
+
// Add expandable circuit breakdown section below the summary table
|
|
341
|
+
if (circuitDetails) {
|
|
342
|
+
const circuitSection = generateCircuitBreakdownSection(comparison, sortedNames, contractName);
|
|
343
|
+
if (circuitSection) {
|
|
344
|
+
output.push('', circuitSection);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return output.join('\n');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Main function to run the benchmark comparison.
|
|
353
|
+
* It finds benchmark report pairs, generates comparison tables for each, and combines them into a single markdown output.
|
|
354
|
+
* @param {object} inputs - The input parameters for the comparison.
|
|
355
|
+
* @param {string} inputs.reportsDir - Directory where benchmark reports are stored.
|
|
356
|
+
* @param {string} inputs.baseSuffix - Suffix for baseline report files.
|
|
357
|
+
* @param {string} inputs.prSuffix - Suffix for PR/current report files.
|
|
358
|
+
* @param {number} inputs.threshold - Percentage threshold for regressions.
|
|
359
|
+
* @returns {string} A markdown string containing the full comparison report.
|
|
360
|
+
*/
|
|
361
|
+
function runComparison(inputs) {
|
|
362
|
+
const { reportsDir, baseSuffix, prSuffix, threshold, circuitDetails = false } = inputs;
|
|
363
|
+
console.log('Comparison script starting...');
|
|
364
|
+
console.log(` Reports Dir: ${reportsDir} (expected ./benchmarks)`);
|
|
365
|
+
console.log(` Base Suffix: '${baseSuffix}' (expected _base)`);
|
|
366
|
+
console.log(` PR Suffix: '${prSuffix}' (expected _latest)`);
|
|
367
|
+
console.log(` Threshold: ${threshold}%`);
|
|
368
|
+
|
|
369
|
+
// Find pairs by scanning the directory
|
|
370
|
+
const benchmarkPairs = findBenchmarkPairs(reportsDir, baseSuffix, prSuffix);
|
|
371
|
+
|
|
372
|
+
if (!benchmarkPairs.length) {
|
|
373
|
+
console.log('No matching benchmark report pairs found in the directory.');
|
|
374
|
+
return '# Benchmark Comparison\n\nNo matching benchmark report pairs found to compare.\n';
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const markdownOutput = ['<!-- benchmark-diff -->\n', '# Benchmark Comparison\n'];
|
|
378
|
+
|
|
379
|
+
// Sort pairs by contract name for consistent report order
|
|
380
|
+
benchmarkPairs.sort((a, b) => a.contractName.localeCompare(b.contractName));
|
|
381
|
+
|
|
382
|
+
// Read system info from benchmark report (all reports have the same info since they run on the same machine)
|
|
383
|
+
let systemInfo = null;
|
|
384
|
+
if (benchmarkPairs.length > 0) {
|
|
385
|
+
try {
|
|
386
|
+
const firstReport = JSON.parse(fs.readFileSync(benchmarkPairs[0].prJsonPath, 'utf-8'));
|
|
387
|
+
systemInfo = firstReport.systemInfo;
|
|
388
|
+
} catch (e) {
|
|
389
|
+
console.warn('Could not read system info from benchmark report:', e.message);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Add system info table (displayed once at the top)
|
|
394
|
+
markdownOutput.push(formatSystemInfoTable(systemInfo));
|
|
395
|
+
|
|
396
|
+
for (const pair of benchmarkPairs) {
|
|
397
|
+
console.log(`\nProcessing contract: ${pair.contractName}...`);
|
|
398
|
+
const tableMarkdown = generateContractComparisonTable(pair, threshold, { circuitDetails });
|
|
399
|
+
markdownOutput.push(`## Contract: ${pair.contractName}\n`);
|
|
400
|
+
markdownOutput.push(tableMarkdown);
|
|
401
|
+
markdownOutput.push('\n');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
console.log(`\nComparison report generated for ${benchmarkPairs.length} contract pair(s).`);
|
|
405
|
+
return markdownOutput.join('\n');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
module.exports = { runComparison };
|