Last active
May 12, 2022 17:27
-
-
Save guidanoli/b7566f54c437e0b0f28c4554d151286f to your computer and use it in GitHub Desktop.
Calculate difference in gas costs from Hardhat Gas Reports
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
################################################################ | |
# Hardhat Gas Report Diff | |
# ============================================================== | |
# Steps: | |
# 1) Have `hardhat-gas-reporter` | |
# 2) Run `npx hardhat test` before and after applying | |
# some change to the smart contracts you're testing | |
# 3) Save the gas reports into separate files (e.g. | |
# `before.txt` and `after.txt`) | |
# 4) Run `python3 hardhat-gas-report-diff before.txt after.txt` | |
# 5) It should output a Markdown table showing how much each | |
# method call and deployment cost changed (also in %) | |
################################################################ | |
import argparse | |
from enum import Enum, auto | |
VERSION = '0.1.0' | |
METHOD_COLUMNS = ( | |
'contract', | |
'method', | |
'min', | |
'max', | |
'avg', | |
'calls', | |
'avgeur', | |
) | |
DEPLOYMENT_COLUMNS = ( | |
'contract', | |
'min', | |
'max', | |
'avg', | |
'pctg', | |
'avgeur', | |
) | |
class State(Enum): | |
""" | |
Finite machine states that relate to sections of gas reports | |
""" | |
HEADER = auto() | |
METHODS = auto() | |
DEPLOYMENTS = auto() | |
def flatten_line(line): | |
""" | |
Convert line into a nice clean list of columns | |
Example: '│ foo · bar · hello world │' ---> ['foo', 'bar', 'hello world'] | |
Argument: | |
line - str | |
Return: | |
list[str] | |
""" | |
return [col for column in line.split('·') | |
if (col := column.strip('│| \n'))] | |
def new_report_dict_from_file(filename): | |
""" | |
Create a report dictionary from file | |
Argument: | |
filename | |
Return: | |
report dictionary | |
{ | |
'methods': { | |
[method_name]: {see METHOD_COLUMNS} | |
}, | |
'deployments': { | |
[contract_name]: {see DEPLOYMENT_COLUMNS} | |
} | |
} | |
""" | |
methods = {} | |
deployments = {} | |
state = State.HEADER | |
next_state = state | |
with open(filename) as fp: | |
for line in fp: | |
if state == State.HEADER: | |
if 'Contract' in line: | |
next_state = State.METHODS | |
elif state == State.METHODS: | |
if 'Deployments' in line: | |
next_state = State.DEPLOYMENTS | |
else: | |
data = flatten_line(line) | |
if len(data) == len(METHOD_COLUMNS): | |
labeled_data = dict(zip(METHOD_COLUMNS, data)) | |
key = (labeled_data['contract'] + | |
'.' + | |
labeled_data['method']) | |
methods[key] = labeled_data | |
elif state == State.DEPLOYMENTS: | |
data = flatten_line(line) | |
if len(data) == len(DEPLOYMENT_COLUMNS): | |
labeled_data = dict(zip(DEPLOYMENT_COLUMNS, data)) | |
key = labeled_data['contract'] | |
deployments[key] = labeled_data | |
else: | |
raise RuntimeError('Unkown state ' + str(state)) | |
state = next_state | |
return dict(methods=methods, deployments=deployments) | |
MARKDOWN_TABLE_COLUMNS = ( | |
'Method call or Contract deployment', | |
'Before', | |
'After', | |
'After - Before', | |
'(After - Before) / Before', | |
) | |
MARKDOWN_TABLE_ALIGNMENTS = ( | |
':-', | |
':-:', | |
':-:', | |
':-:', | |
':-:', | |
) | |
def print_list_in_markdown_table(lst): | |
print('| {} |'.format(' | '.join(lst))) | |
def dicts_union(d1, d2): | |
return sorted(set(d1) | set(d2)) | |
def calculate_line(before, after): | |
before_int = int(before) | |
diff = int(after) - before_int | |
diff_pct = diff / before_int | |
return [before, after, | |
"{:+d}".format(diff), "{:+.2f}%".format(100*diff_pct)] | |
def print_table_line(before, after, key, args): | |
name = f'`{key}`' | |
if key in before and key in after: | |
before_avg = before[key]['avg'] | |
after_avg = after[key]['avg'] | |
if not (not args.keep_zeros and before_avg == after_avg): | |
print_list_in_markdown_table( | |
[name] + calculate_line(before_avg, after_avg)) | |
elif not args.both: | |
if key in before: | |
before_avg = before[key]['avg'] | |
print_list_in_markdown_table([ | |
name, before_avg, '-', '-', '-', | |
]) | |
elif key in after: | |
after_avg = after[key]['avg'] | |
print_list_in_markdown_table([ | |
name, '-', after_avg, '-', '-', | |
]) | |
def print_subdict_in_markdown_table(before, after, datakey, args): | |
before_data = before[datakey] | |
after_data = after[datakey] | |
all_keys = dicts_union(before_data, after_data) | |
for key in all_keys: | |
print_table_line(before_data, after_data, key, args) | |
def format_markdown(before, after, args): | |
print_list_in_markdown_table(MARKDOWN_TABLE_COLUMNS) | |
print_list_in_markdown_table(MARKDOWN_TABLE_ALIGNMENTS) | |
print_subdict_in_markdown_table(before, after, 'methods', args) | |
print_subdict_in_markdown_table(before, after, 'deployments', args) | |
if __name__ == '__main__': | |
about = ('Process hardhat gas reports and calculate the difference in ' + | |
'gas costs of methods calls (on average) and of deployments') | |
parser = argparse.ArgumentParser(description=about) | |
parser.add_argument('--version', action='version', version=VERSION) | |
parser.add_argument( | |
'before', | |
type=str, | |
help='filename of report before change') | |
parser.add_argument( | |
'after', | |
type=str, | |
help='filename of report after change') | |
parser.add_argument( | |
'-z', dest='keep_zeros', action='store_true', default=False, | |
help='print methods/deployments with zero average cost change') | |
parser.add_argument( | |
'-b', dest='both', action='store_true', default=False, | |
help='only print methods/deployments with data on both reports') | |
args = parser.parse_args() | |
before = new_report_dict_from_file(args.before) | |
after = new_report_dict_from_file(args.after) | |
format_markdown(before, after, args) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment