123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- #!/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',
- },
- hideEqual: {
- type: 'boolean',
- default: true,
- },
- strictTesting: {
- type: 'boolean',
- default: false,
- },
- });
- // 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, strictTesting: false }) {
- 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 => !opts.strictTesting || 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]), argv);
- switch (argv.style) {
- case 'markdown':
- console.log(formatCmpMarkdown(report));
- break;
- case 'shell':
- default:
- console.log(formatCmpShell(report));
- break;
- }
|