compareGasReports.js 6.9 KB

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