-
Notifications
You must be signed in to change notification settings - Fork 6.1k
/
Copy pathparse_eth_gas_report.py
executable file
·269 lines (216 loc) · 8.75 KB
/
parse_eth_gas_report.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
#!/usr/bin/env python3
# coding=utf-8
from dataclasses import asdict, dataclass, field
from typing import Dict, Optional, Tuple
import json
import re
import sys
REPORT_HEADER_REGEX = re.compile(r'''
^[|\s]+ Solc[ ]version:\s*(?P<solc_version>[\w\d.]+)
[|\s]+ Optimizer[ ]enabled:\s*(?P<optimize>[\w]+)
[|\s]+ Runs:\s*(?P<runs>[\d]+)
[|\s]+ Block[ ]limit:\s*(?P<block_limit>[\d]+)\s*gas
[|\s]+$
''', re.VERBOSE)
METHOD_HEADER_REGEX = re.compile(r'^[|\s]+Methods[|\s]+$')
METHOD_COLUMN_HEADERS_REGEX = re.compile(r'''
^[|\s]+ Contract
[|\s]+ Method
[|\s]+ Min
[|\s]+ Max
[|\s]+ Avg
[|\s]+ \#[ ]calls
[|\s]+ \w+[ ]\(avg\)
[|\s]+$
''', re.VERBOSE)
METHOD_ROW_REGEX = re.compile(r'''
^[|\s]+ (?P<contract>[^|]+)
[|\s]+ (?P<method>[^|]+)
[|\s]+ (?P<min>[^|]+)
[|\s]+ (?P<max>[^|]+)
[|\s]+ (?P<avg>[^|]+)
[|\s]+ (?P<call_count>[^|]+)
[|\s]+ (?P<eur_avg>[^|]+)
[|\s]+$
''', re.VERBOSE)
FRAME_REGEX = re.compile(r'^[-|\s]+$')
DEPLOYMENT_HEADER_REGEX = re.compile(r'^[|\s]+Deployments[|\s]+% of limit[|\s]+$')
DEPLOYMENT_ROW_REGEX = re.compile(r'''
^[|\s]+ (?P<contract>[^|]+)
[|\s]+ (?P<min>[^|]+)
[|\s]+ (?P<max>[^|]+)
[|\s]+ (?P<avg>[^|]+)
[|\s]+ (?P<percent_of_limit>[^|]+)\s*%
[|\s]+ (?P<eur_avg>[^|]+)
[|\s]+$
''', re.VERBOSE)
class ReportError(Exception):
pass
class ReportValidationError(ReportError):
pass
class ReportParsingError(Exception):
def __init__(self, message: str, line: str, line_number: int):
# pylint: disable=useless-super-delegation # It's not useless, it adds type annotations.
super().__init__(message, line, line_number)
def __str__(self):
return f"Parsing error on line {self.args[2] + 1}: {self.args[0]}\n{self.args[1]}"
@dataclass(frozen=True)
class MethodGasReport:
min_gas: int
max_gas: int
avg_gas: int
call_count: int
total_gas: int = field(init=False)
def __post_init__(self):
object.__setattr__(self, 'total_gas', self.avg_gas * self.call_count)
@dataclass(frozen=True)
class ContractGasReport:
min_deployment_gas: Optional[int]
max_deployment_gas: Optional[int]
avg_deployment_gas: Optional[int]
methods: Optional[Dict[str, MethodGasReport]]
total_method_gas: int = field(init=False, default=0)
def __post_init__(self):
if self.methods is not None:
object.__setattr__(self, 'total_method_gas', sum(method.total_gas for method in self.methods.values()))
@dataclass(frozen=True)
class GasReport:
solc_version: str
optimize: bool
runs: int
block_limit: int
contracts: Dict[str, ContractGasReport]
total_method_gas: int = field(init=False)
total_deployment_gas: int = field(init=False)
def __post_init__(self):
object.__setattr__(self, 'total_method_gas', sum(
total_method_gas
for total_method_gas in (contract.total_method_gas for contract in self.contracts.values())
if total_method_gas is not None
))
object.__setattr__(self, 'total_deployment_gas', sum(
contract.avg_deployment_gas
for contract in self.contracts.values()
if contract.avg_deployment_gas is not None
))
def to_json(self):
return json.dumps(asdict(self), indent=4, sort_keys=True)
def parse_bool(input_string: str) -> bool:
if input_string == 'true':
return True
elif input_string == 'false':
return True
else:
raise ValueError(f"Invalid boolean value: '{input_string}'")
def parse_optional_int(input_string: str, default: Optional[int] = None) -> Optional[int]:
if input_string.strip() == '-':
return default
return int(input_string)
def parse_report_header(line: str) -> Optional[dict]:
match = REPORT_HEADER_REGEX.match(line)
if match is None:
return None
return {
'solc_version': match.group('solc_version'),
'optimize': parse_bool(match.group('optimize')),
'runs': int(match.group('runs')),
'block_limit': int(match.group('block_limit')),
}
def parse_method_row(line: str, line_number: int) -> Optional[Tuple[str, str, MethodGasReport]]:
match = METHOD_ROW_REGEX.match(line)
if match is None:
raise ReportParsingError("Expected a table row with method details.", line, line_number)
avg_gas = parse_optional_int(match['avg'])
call_count = int(match['call_count'])
if avg_gas is None and call_count == 0:
# No calls, no gas values. Uninteresting. Skip the row.
return None
return (
match['contract'].strip(),
match['method'].strip(),
MethodGasReport(
min_gas=parse_optional_int(match['min'], avg_gas),
max_gas=parse_optional_int(match['max'], avg_gas),
avg_gas=avg_gas,
call_count=call_count,
)
)
def parse_deployment_row(line: str, line_number: int) -> Tuple[str, int, int, int]:
match = DEPLOYMENT_ROW_REGEX.match(line)
if match is None:
raise ReportParsingError("Expected a table row with deployment details.", line, line_number)
return (
match['contract'].strip(),
parse_optional_int(match['min'].strip()),
parse_optional_int(match['max'].strip()),
int(match['avg'].strip()),
)
def preprocess_unicode_frames(input_string: str) -> str:
# The report has a mix of normal pipe chars and its unicode variant.
# Let's just replace all frame chars with normal pipes for easier parsing.
return input_string.replace('\u2502', '|').replace('·', '|')
def parse_report(rst_report: str) -> GasReport:
report_params = None
methods_by_contract = {}
deployment_costs = {}
expected_row_type = None
for line_number, line in enumerate(preprocess_unicode_frames(rst_report).splitlines()):
try:
if (
line.strip() == "" or
FRAME_REGEX.match(line) is not None or
METHOD_COLUMN_HEADERS_REGEX.match(line) is not None
):
continue
if METHOD_HEADER_REGEX.match(line) is not None:
expected_row_type = 'method'
continue
if DEPLOYMENT_HEADER_REGEX.match(line) is not None:
expected_row_type = 'deployment'
continue
new_report_params = parse_report_header(line)
if new_report_params is not None:
if report_params is not None:
raise ReportParsingError("Duplicate report header.", line, line_number)
report_params = new_report_params
continue
if expected_row_type == 'method':
parsed_row = parse_method_row(line, line_number)
if parsed_row is None:
continue
(contract, method, method_report) = parsed_row
if contract not in methods_by_contract:
methods_by_contract[contract] = {}
if method in methods_by_contract[contract]:
# Report must be generated with full signatures for method names to be unambiguous.
raise ReportParsingError(f"Duplicate method row for '{contract}.{method}'.", line, line_number)
methods_by_contract[contract][method] = method_report
elif expected_row_type == 'deployment':
(contract, min_gas, max_gas, avg_gas) = parse_deployment_row(line, line_number)
if contract in deployment_costs:
raise ReportParsingError(f"Duplicate contract deployment row for '{contract}'.", line, line_number)
deployment_costs[contract] = (min_gas, max_gas, avg_gas)
else:
assert expected_row_type is None
raise ReportParsingError("Found data row without a section header.", line, line_number)
except ValueError as error:
raise ReportParsingError(error.args[0], line, line_number) from error
if report_params is None:
raise ReportValidationError("Report header not found.")
report_params['contracts'] = {
contract: ContractGasReport(
min_deployment_gas=deployment_costs.get(contract, (None, None, None))[0],
max_deployment_gas=deployment_costs.get(contract, (None, None, None))[1],
avg_deployment_gas=deployment_costs.get(contract, (None, None, None))[2],
methods=methods_by_contract.get(contract),
)
for contract in methods_by_contract.keys() | deployment_costs.keys()
}
return GasReport(**report_params)
if __name__ == "__main__":
try:
report = parse_report(sys.stdin.read())
print(report.to_json())
except ReportError as exception:
print(f"{exception}", file=sys.stderr)
sys.exit(1)