You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
233 lines
6.5 KiB
233 lines
6.5 KiB
#!/usr/bin/env node
|
|
|
|
const fs = require('fs');
|
|
const chalk = require('chalk');
|
|
const { argv } = require('yargs')
|
|
.env()
|
|
.options({
|
|
style: {
|
|
type: 'string',
|
|
choices: ['shell', 'markdown'],
|
|
default: 'shell',
|
|
},
|
|
});
|
|
|
|
// Deduce base tx cost from the percentage denominator
|
|
const BASE_TX_COST = 21000;
|
|
|
|
// Utilities
|
|
function sum(...args) {
|
|
return args.reduce((a, b) => a + b, 0);
|
|
}
|
|
|
|
function average(...args) {
|
|
return sum(...args) / args.length;
|
|
}
|
|
|
|
function variation(current, previous, offset = 0) {
|
|
return {
|
|
value: current,
|
|
delta: current - previous,
|
|
prcnt: (100 * (current - previous)) / (previous - offset),
|
|
};
|
|
}
|
|
|
|
// Report class
|
|
class Report {
|
|
// Read report file
|
|
static load(filepath) {
|
|
return JSON.parse(fs.readFileSync(filepath, 'utf8'));
|
|
}
|
|
|
|
// Compare two reports
|
|
static compare(update, ref, opts = { hideEqual: true }) {
|
|
if (JSON.stringify(update.config.metadata) !== JSON.stringify(ref.config.metadata)) {
|
|
throw new Error('Reports produced with non matching metadata');
|
|
}
|
|
|
|
const deployments = update.info.deployments
|
|
.map(contract =>
|
|
Object.assign(contract, { previousVersion: ref.info.deployments.find(({ name }) => name === contract.name) }),
|
|
)
|
|
.filter(contract => contract.gasData?.length && contract.previousVersion?.gasData?.length)
|
|
.flatMap(contract => [
|
|
{
|
|
contract: contract.name,
|
|
method: '[bytecode length]',
|
|
avg: variation(contract.bytecode.length / 2 - 1, contract.previousVersion.bytecode.length / 2 - 1),
|
|
},
|
|
{
|
|
contract: contract.name,
|
|
method: '[construction cost]',
|
|
avg: variation(
|
|
...[contract.gasData, contract.previousVersion.gasData].map(x => Math.round(average(...x))),
|
|
BASE_TX_COST,
|
|
),
|
|
},
|
|
])
|
|
.sort((a, b) => `${a.contract}:${a.method}`.localeCompare(`${b.contract}:${b.method}`));
|
|
|
|
const methods = Object.keys(update.info.methods)
|
|
.filter(key => ref.info.methods[key])
|
|
.filter(key => update.info.methods[key].numberOfCalls > 0)
|
|
.filter(key => update.info.methods[key].numberOfCalls === ref.info.methods[key].numberOfCalls)
|
|
.map(key => ({
|
|
contract: ref.info.methods[key].contract,
|
|
method: ref.info.methods[key].fnSig,
|
|
min: variation(...[update, ref].map(x => Math.min(...x.info.methods[key].gasData)), BASE_TX_COST),
|
|
max: variation(...[update, ref].map(x => Math.max(...x.info.methods[key].gasData)), BASE_TX_COST),
|
|
avg: variation(...[update, ref].map(x => Math.round(average(...x.info.methods[key].gasData))), BASE_TX_COST),
|
|
}))
|
|
.sort((a, b) => `${a.contract}:${a.method}`.localeCompare(`${b.contract}:${b.method}`));
|
|
|
|
return []
|
|
.concat(deployments, methods)
|
|
.filter(row => !opts.hideEqual || row.min?.delta || row.max?.delta || row.avg?.delta);
|
|
}
|
|
}
|
|
|
|
// Display
|
|
function center(text, length) {
|
|
return text.padStart((text.length + length) / 2).padEnd(length);
|
|
}
|
|
|
|
function plusSign(num) {
|
|
return num > 0 ? '+' : '';
|
|
}
|
|
|
|
function formatCellShell(cell) {
|
|
const format = chalk[cell?.delta > 0 ? 'red' : cell?.delta < 0 ? 'green' : 'reset'];
|
|
return [
|
|
format((!isFinite(cell?.value) ? '-' : cell.value.toString()).padStart(8)),
|
|
format((!isFinite(cell?.delta) ? '-' : plusSign(cell.delta) + cell.delta.toString()).padStart(8)),
|
|
format((!isFinite(cell?.prcnt) ? '-' : plusSign(cell.prcnt) + cell.prcnt.toFixed(2) + '%').padStart(8)),
|
|
];
|
|
}
|
|
|
|
function formatCmpShell(rows) {
|
|
const contractLength = Math.max(8, ...rows.map(({ contract }) => contract.length));
|
|
const methodLength = Math.max(7, ...rows.map(({ method }) => method.length));
|
|
|
|
const COLS = [
|
|
{ txt: '', length: 0 },
|
|
{ txt: 'Contract', length: contractLength },
|
|
{ txt: 'Method', length: methodLength },
|
|
{ txt: 'Min', length: 30 },
|
|
{ txt: 'Max', length: 30 },
|
|
{ txt: 'Avg', length: 30 },
|
|
{ txt: '', length: 0 },
|
|
];
|
|
const HEADER = COLS.map(entry => chalk.bold(center(entry.txt, entry.length || 0)))
|
|
.join(' | ')
|
|
.trim();
|
|
const SEPARATOR = COLS.map(({ length }) => (length > 0 ? '-'.repeat(length + 2) : ''))
|
|
.join('|')
|
|
.trim();
|
|
|
|
return [
|
|
'',
|
|
HEADER,
|
|
...rows.map(entry =>
|
|
[
|
|
'',
|
|
chalk.grey(entry.contract.padEnd(contractLength)),
|
|
entry.method.padEnd(methodLength),
|
|
...formatCellShell(entry.min),
|
|
...formatCellShell(entry.max),
|
|
...formatCellShell(entry.avg),
|
|
'',
|
|
]
|
|
.join(' | ')
|
|
.trim(),
|
|
),
|
|
'',
|
|
]
|
|
.join(`\n${SEPARATOR}\n`)
|
|
.trim();
|
|
}
|
|
|
|
function alignPattern(align) {
|
|
switch (align) {
|
|
case 'left':
|
|
case undefined:
|
|
return ':-';
|
|
case 'right':
|
|
return '-:';
|
|
case 'center':
|
|
return ':-:';
|
|
}
|
|
}
|
|
|
|
function trend(value) {
|
|
return value > 0 ? ':x:' : value < 0 ? ':heavy_check_mark:' : ':heavy_minus_sign:';
|
|
}
|
|
|
|
function formatCellMarkdown(cell) {
|
|
return [
|
|
!isFinite(cell?.value) ? '-' : cell.value.toString(),
|
|
!isFinite(cell?.delta) ? '-' : plusSign(cell.delta) + cell.delta.toString(),
|
|
!isFinite(cell?.prcnt) ? '-' : plusSign(cell.prcnt) + cell.prcnt.toFixed(2) + '%' + trend(cell.delta),
|
|
];
|
|
}
|
|
|
|
function formatCmpMarkdown(rows) {
|
|
const COLS = [
|
|
{ txt: '' },
|
|
{ txt: 'Contract', align: 'left' },
|
|
{ txt: 'Method', align: 'left' },
|
|
{ txt: 'Min', align: 'right' },
|
|
{ txt: '(+/-)', align: 'right' },
|
|
{ txt: '%', align: 'right' },
|
|
{ txt: 'Max', align: 'right' },
|
|
{ txt: '(+/-)', align: 'right' },
|
|
{ txt: '%', align: 'right' },
|
|
{ txt: 'Avg', align: 'right' },
|
|
{ txt: '(+/-)', align: 'right' },
|
|
{ txt: '%', align: 'right' },
|
|
{ txt: '' },
|
|
];
|
|
const HEADER = COLS.map(entry => entry.txt)
|
|
.join(' | ')
|
|
.trim();
|
|
const SEPARATOR = COLS.map(entry => (entry.txt ? alignPattern(entry.align) : ''))
|
|
.join('|')
|
|
.trim();
|
|
|
|
return [
|
|
'# Changes to gas costs',
|
|
'',
|
|
HEADER,
|
|
SEPARATOR,
|
|
rows
|
|
.map(entry =>
|
|
[
|
|
'',
|
|
entry.contract,
|
|
entry.method,
|
|
...formatCellMarkdown(entry.min),
|
|
...formatCellMarkdown(entry.max),
|
|
...formatCellMarkdown(entry.avg),
|
|
'',
|
|
]
|
|
.join(' | ')
|
|
.trim(),
|
|
)
|
|
.join('\n'),
|
|
'',
|
|
]
|
|
.join('\n')
|
|
.trim();
|
|
}
|
|
|
|
// MAIN
|
|
const report = Report.compare(Report.load(argv._[0]), Report.load(argv._[1]));
|
|
|
|
switch (argv.style) {
|
|
case 'markdown':
|
|
console.log(formatCmpMarkdown(report));
|
|
break;
|
|
case 'shell':
|
|
default:
|
|
console.log(formatCmpShell(report));
|
|
break;
|
|
}
|
|
|