compareGasReports.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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.config.metadata) !== JSON.stringify(ref.config.metadata)) {
  46. throw new Error('Reports produced with non matching metadata');
  47. }
  48. const deployments = update.info.deployments
  49. .map(contract =>
  50. Object.assign(contract, { previousVersion: ref.info.deployments.find(({ name }) => name === contract.name) }),
  51. )
  52. .filter(contract => contract.gasData?.length && contract.previousVersion?.gasData?.length)
  53. .flatMap(contract => [
  54. {
  55. contract: contract.name,
  56. method: '[bytecode length]',
  57. avg: variation(contract.bytecode.length / 2 - 1, contract.previousVersion.bytecode.length / 2 - 1),
  58. },
  59. {
  60. contract: contract.name,
  61. method: '[construction cost]',
  62. avg: variation(
  63. ...[contract.gasData, contract.previousVersion.gasData].map(x => Math.round(average(...x))),
  64. BASE_TX_COST,
  65. ),
  66. },
  67. ])
  68. .sort((a, b) => `${a.contract}:${a.method}`.localeCompare(`${b.contract}:${b.method}`));
  69. const methods = Object.keys(update.info.methods)
  70. .filter(key => ref.info.methods[key])
  71. .filter(key => update.info.methods[key].numberOfCalls > 0)
  72. .filter(
  73. key => !opts.strictTesting || update.info.methods[key].numberOfCalls === ref.info.methods[key].numberOfCalls,
  74. )
  75. .map(key => ({
  76. contract: ref.info.methods[key].contract,
  77. method: ref.info.methods[key].fnSig,
  78. min: variation(...[update, ref].map(x => Math.min(...x.info.methods[key].gasData)), BASE_TX_COST),
  79. max: variation(...[update, ref].map(x => Math.max(...x.info.methods[key].gasData)), BASE_TX_COST),
  80. avg: variation(...[update, ref].map(x => Math.round(average(...x.info.methods[key].gasData))), BASE_TX_COST),
  81. }))
  82. .sort((a, b) => `${a.contract}:${a.method}`.localeCompare(`${b.contract}:${b.method}`));
  83. return []
  84. .concat(deployments, methods)
  85. .filter(row => !opts.hideEqual || row.min?.delta || row.max?.delta || row.avg?.delta);
  86. }
  87. }
  88. // Display
  89. function center(text, length) {
  90. return text.padStart((text.length + length) / 2).padEnd(length);
  91. }
  92. function plusSign(num) {
  93. return num > 0 ? '+' : '';
  94. }
  95. function formatCellShell(cell) {
  96. const format = chalk[cell?.delta > 0 ? 'red' : cell?.delta < 0 ? 'green' : 'reset'];
  97. return [
  98. format((!isFinite(cell?.value) ? '-' : cell.value.toString()).padStart(8)),
  99. format((!isFinite(cell?.delta) ? '-' : plusSign(cell.delta) + cell.delta.toString()).padStart(8)),
  100. format((!isFinite(cell?.prcnt) ? '-' : plusSign(cell.prcnt) + cell.prcnt.toFixed(2) + '%').padStart(8)),
  101. ];
  102. }
  103. function formatCmpShell(rows) {
  104. const contractLength = Math.max(8, ...rows.map(({ contract }) => contract.length));
  105. const methodLength = Math.max(7, ...rows.map(({ method }) => method.length));
  106. const COLS = [
  107. { txt: '', length: 0 },
  108. { txt: 'Contract', length: contractLength },
  109. { txt: 'Method', length: methodLength },
  110. { txt: 'Min', length: 30 },
  111. { txt: 'Max', length: 30 },
  112. { txt: 'Avg', length: 30 },
  113. { txt: '', length: 0 },
  114. ];
  115. const HEADER = COLS.map(entry => chalk.bold(center(entry.txt, entry.length || 0)))
  116. .join(' | ')
  117. .trim();
  118. const SEPARATOR = COLS.map(({ length }) => (length > 0 ? '-'.repeat(length + 2) : ''))
  119. .join('|')
  120. .trim();
  121. return [
  122. '',
  123. HEADER,
  124. ...rows.map(entry =>
  125. [
  126. '',
  127. chalk.grey(entry.contract.padEnd(contractLength)),
  128. entry.method.padEnd(methodLength),
  129. ...formatCellShell(entry.min),
  130. ...formatCellShell(entry.max),
  131. ...formatCellShell(entry.avg),
  132. '',
  133. ]
  134. .join(' | ')
  135. .trim(),
  136. ),
  137. '',
  138. ]
  139. .join(`\n${SEPARATOR}\n`)
  140. .trim();
  141. }
  142. function alignPattern(align) {
  143. switch (align) {
  144. case 'left':
  145. case undefined:
  146. return ':-';
  147. case 'right':
  148. return '-:';
  149. case 'center':
  150. return ':-:';
  151. }
  152. }
  153. function trend(value) {
  154. return value > 0 ? ':x:' : value < 0 ? ':heavy_check_mark:' : ':heavy_minus_sign:';
  155. }
  156. function formatCellMarkdown(cell) {
  157. return [
  158. !isFinite(cell?.value) ? '-' : cell.value.toString(),
  159. !isFinite(cell?.delta) ? '-' : plusSign(cell.delta) + cell.delta.toString(),
  160. !isFinite(cell?.prcnt) ? '-' : plusSign(cell.prcnt) + cell.prcnt.toFixed(2) + '%' + trend(cell.delta),
  161. ];
  162. }
  163. function formatCmpMarkdown(rows) {
  164. const COLS = [
  165. { txt: '' },
  166. { txt: 'Contract', align: 'left' },
  167. { txt: 'Method', align: 'left' },
  168. { txt: 'Min', align: 'right' },
  169. { txt: '(+/-)', align: 'right' },
  170. { txt: '%', align: 'right' },
  171. { txt: 'Max', align: 'right' },
  172. { txt: '(+/-)', align: 'right' },
  173. { txt: '%', align: 'right' },
  174. { txt: 'Avg', align: 'right' },
  175. { txt: '(+/-)', align: 'right' },
  176. { txt: '%', align: 'right' },
  177. { txt: '' },
  178. ];
  179. const HEADER = COLS.map(entry => entry.txt)
  180. .join(' | ')
  181. .trim();
  182. const SEPARATOR = COLS.map(entry => (entry.txt ? alignPattern(entry.align) : ''))
  183. .join('|')
  184. .trim();
  185. return [
  186. '# Changes to gas costs',
  187. '',
  188. HEADER,
  189. SEPARATOR,
  190. rows
  191. .map(entry =>
  192. [
  193. '',
  194. entry.contract,
  195. entry.method,
  196. ...formatCellMarkdown(entry.min),
  197. ...formatCellMarkdown(entry.max),
  198. ...formatCellMarkdown(entry.avg),
  199. '',
  200. ]
  201. .join(' | ')
  202. .trim(),
  203. )
  204. .join('\n'),
  205. '',
  206. ]
  207. .join('\n')
  208. .trim();
  209. }
  210. // MAIN
  211. const report = Report.compare(Report.load(argv._[0]), Report.load(argv._[1]), argv);
  212. switch (argv.style) {
  213. case 'markdown':
  214. console.log(formatCmpMarkdown(report));
  215. break;
  216. case 'shell':
  217. default:
  218. console.log(formatCmpShell(report));
  219. break;
  220. }