compareGasReports.js 6.9 KB

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