compareGasReports.js 6.5 KB

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