compareGasReports.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const chalk = require('chalk');
  4. const { argv } = require('yargs')
  5. .env()
  6. .options({
  7. style: {
  8. type: 'string',
  9. choices: [ 'shell', 'markdown' ],
  10. default: 'shell',
  11. },
  12. });
  13. // Deduce base tx cost from the percentage denominator
  14. const BASE_TX_COST = 21000;
  15. // Utilities
  16. function sum (...args) {
  17. return args.reduce((a, b) => a + b, 0);
  18. }
  19. function average (...args) {
  20. return sum(...args) / args.length;
  21. }
  22. function variation (current, previous, offset = 0) {
  23. return {
  24. value: current,
  25. delta: current - previous,
  26. prcnt: 100 * (current - previous) / (previous - offset),
  27. };
  28. }
  29. // Report class
  30. class Report {
  31. // Read report file
  32. static load (filepath) {
  33. return JSON.parse(fs.readFileSync(filepath, 'utf8'));
  34. }
  35. // Compare two reports
  36. static compare (update, ref, opts = { hideEqual: true }) {
  37. if (JSON.stringify(update.config.metadata) !== JSON.stringify(ref.config.metadata)) {
  38. throw new Error('Reports produced with non matching metadata');
  39. }
  40. const deployments = update.info.deployments
  41. .map(contract => Object.assign(
  42. contract,
  43. { previousVersion: ref.info.deployments.find(({ name }) => name === contract.name) },
  44. ))
  45. .filter(contract => contract.gasData?.length && contract.previousVersion?.gasData?.length)
  46. .flatMap(contract => [{
  47. contract: contract.name,
  48. method: '[bytecode length]',
  49. avg: variation(contract.bytecode.length / 2 - 1, contract.previousVersion.bytecode.length / 2 - 1),
  50. }, {
  51. contract: contract.name,
  52. method: '[construction cost]',
  53. avg: variation(
  54. ...[contract.gasData, contract.previousVersion.gasData].map(x => Math.round(average(...x))),
  55. BASE_TX_COST),
  56. }])
  57. .sort((a, b) => `${a.contract}:${a.method}`.localeCompare(`${b.contract}:${b.method}`));
  58. const methods = Object.keys(update.info.methods)
  59. .filter(key => ref.info.methods[key])
  60. .filter(key => update.info.methods[key].numberOfCalls > 0)
  61. .filter(key => update.info.methods[key].numberOfCalls === ref.info.methods[key].numberOfCalls)
  62. .map(key => ({
  63. contract: ref.info.methods[key].contract,
  64. method: ref.info.methods[key].fnSig,
  65. min: variation(...[update, ref].map(x => Math.min(...x.info.methods[key].gasData)), BASE_TX_COST),
  66. max: variation(...[update, ref].map(x => Math.max(...x.info.methods[key].gasData)), BASE_TX_COST),
  67. avg: variation(...[update, ref].map(x => Math.round(average(...x.info.methods[key].gasData))), BASE_TX_COST),
  68. }))
  69. .sort((a, b) => `${a.contract}:${a.method}`.localeCompare(`${b.contract}:${b.method}`));
  70. return [].concat(deployments, methods)
  71. .filter(row => !opts.hideEqual || row.min?.delta || row.max?.delta || row.avg?.delta);
  72. }
  73. }
  74. // Display
  75. function center (text, length) {
  76. return text.padStart((text.length + length) / 2).padEnd(length);
  77. }
  78. function plusSign (num) {
  79. return num > 0 ? '+' : '';
  80. }
  81. function formatCellShell (cell) {
  82. const format = chalk[cell?.delta > 0 ? 'red' : cell?.delta < 0 ? 'green' : 'reset'];
  83. return [
  84. format((!isFinite(cell?.value) ? '-' : cell.value.toString()).padStart(8)),
  85. format((!isFinite(cell?.delta) ? '-' : plusSign(cell.delta) + cell.delta.toString()).padStart(8)),
  86. format((!isFinite(cell?.prcnt) ? '-' : plusSign(cell.prcnt) + cell.prcnt.toFixed(2) + '%').padStart(8)),
  87. ];
  88. }
  89. function formatCmpShell (rows) {
  90. const contractLength = Math.max(8, ...rows.map(({ contract }) => contract.length));
  91. const methodLength = Math.max(7, ...rows.map(({ method }) => method.length));
  92. const COLS = [
  93. { txt: '', length: 0 },
  94. { txt: 'Contract', length: contractLength },
  95. { txt: 'Method', length: methodLength },
  96. { txt: 'Min', length: 30 },
  97. { txt: 'Max', length: 30 },
  98. { txt: 'Avg', length: 30 },
  99. { txt: '', length: 0 },
  100. ];
  101. const HEADER = COLS.map(entry => chalk.bold(center(entry.txt, entry.length || 0))).join(' | ').trim();
  102. const SEPARATOR = COLS.map(({ length }) => length > 0 ? '-'.repeat(length + 2) : '').join('|').trim();
  103. return [
  104. '',
  105. HEADER,
  106. ...rows.map(entry => [
  107. '',
  108. chalk.grey(entry.contract.padEnd(contractLength)),
  109. entry.method.padEnd(methodLength),
  110. ...formatCellShell(entry.min),
  111. ...formatCellShell(entry.max),
  112. ...formatCellShell(entry.avg),
  113. '',
  114. ].join(' | ').trim()),
  115. '',
  116. ].join(`\n${SEPARATOR}\n`).trim();
  117. }
  118. function alignPattern (align) {
  119. switch (align) {
  120. case 'left':
  121. case undefined:
  122. return ':-';
  123. case 'right':
  124. return '-:';
  125. case 'center':
  126. return ':-:';
  127. }
  128. }
  129. function trend (value) {
  130. return value > 0
  131. ? ':x:'
  132. : value < 0
  133. ? ':heavy_check_mark:'
  134. : ':heavy_minus_sign:';
  135. }
  136. function formatCellMarkdown (cell) {
  137. return [
  138. (!isFinite(cell?.value) ? '-' : cell.value.toString()),
  139. (!isFinite(cell?.delta) ? '-' : plusSign(cell.delta) + cell.delta.toString()),
  140. (!isFinite(cell?.prcnt) ? '-' : plusSign(cell.prcnt) + cell.prcnt.toFixed(2) + '%' + trend(cell.delta)),
  141. ];
  142. }
  143. function formatCmpMarkdown (rows) {
  144. const COLS = [
  145. { txt: '' },
  146. { txt: 'Contract', align: 'left' },
  147. { txt: 'Method', align: 'left' },
  148. { txt: 'Min', align: 'right' },
  149. { txt: '(+/-)', align: 'right' },
  150. { txt: '%', align: 'right' },
  151. { txt: 'Max', align: 'right' },
  152. { txt: '(+/-)', align: 'right' },
  153. { txt: '%', align: 'right' },
  154. { txt: 'Avg', align: 'right' },
  155. { txt: '(+/-)', align: 'right' },
  156. { txt: '%', align: 'right' },
  157. { txt: '' },
  158. ];
  159. const HEADER = COLS.map(entry => entry.txt).join(' | ').trim();
  160. const SEPARATOR = COLS.map(entry => entry.txt ? alignPattern(entry.align) : '').join('|').trim();
  161. return [
  162. '# Changes to gas costs',
  163. '',
  164. HEADER,
  165. SEPARATOR,
  166. rows.map(entry => [
  167. '',
  168. entry.contract,
  169. entry.method,
  170. ...formatCellMarkdown(entry.min),
  171. ...formatCellMarkdown(entry.max),
  172. ...formatCellMarkdown(entry.avg),
  173. '',
  174. ].join(' | ').trim()).join('\n'),
  175. '',
  176. ].join('\n').trim();
  177. }
  178. // MAIN
  179. const report = Report.compare(Report.load(argv._[0]), Report.load(argv._[1]));
  180. switch (argv.style) {
  181. case 'markdown':
  182. console.log(formatCmpMarkdown(report));
  183. break;
  184. case 'shell':
  185. default:
  186. console.log(formatCmpShell(report));
  187. break;
  188. }