"""A script to collect gas statistics and print it. Useful to summarize gas differences to semantic tests for a PR / branch. Dependencies: Parsec (https://pypi.org/project/parsec/) and Tabulate (https://pypi.org/project/tabulate/) pip install parsec tabulate Run from root project dir. python3 scripts/gas_diff_stats.py Note that the changes to semantic tests have to be committed. Assumes that there is a remote named ``origin`` pointing to the Solidity github repository. The changes are compared against ``origin/develop``. """ import subprocess from pathlib import Path from enum import Enum from parsec import generate, ParseError, regex, string from tabulate import tabulate class Kind(Enum): IrOptimized = 1 Legacy = 2 LegacyOptimized = 3 class Diff(Enum): Minus = 1 Plus = 2 minus = string("-").result(Diff.Minus) plus = string("+").result(Diff.Plus) space = string(" ") comment = string("//") colon = string(":") gas_ir_optimized = string("gas irOptimized").result(Kind.IrOptimized) gas_legacy_optimized = string("gas legacyOptimized").result(Kind.LegacyOptimized) gas_legacy = string("gas legacy").result(Kind.Legacy) def number() -> int: """Parse number.""" return regex(r"([0-9]*)").parsecmap(int) @generate def diff_string() -> (Kind, Diff, int): """Usage: diff_string.parse(string) Example string: -// gas irOptimized: 138070 """ diff_kind = yield minus | plus yield comment yield space codegen_kind = yield gas_ir_optimized ^ gas_legacy_optimized ^ gas_legacy yield colon yield space val = yield number() return (diff_kind, codegen_kind, val) def collect_statistics(lines) -> (int, int, int, int, int, int): """Returns (old_ir_optimized, old_legacy_optimized, old_legacy, new_ir_optimized, new_legacy_optimized, new_legacy) All the values in the same file (in the diff) are summed up. """ if not lines: raise RuntimeError("Empty list") def try_parse(line): try: return diff_string.parse(line) except ParseError: pass return None out = [parsed for line in lines if (parsed := try_parse(line)) is not None] diff_kinds = [Diff.Minus, Diff.Plus] codegen_kinds = [Kind.IrOptimized, Kind.LegacyOptimized, Kind.Legacy] return tuple( sum( val for (diff_kind, codegen_kind, val) in out if diff_kind == _diff_kind and codegen_kind == _codegen_kind ) for _diff_kind in diff_kinds for _codegen_kind in codegen_kinds ) def semantictest_statistics(): """Prints the tabulated statistics that can be pasted in github.""" def try_parse_git_diff(fname): try: diff_output = subprocess.check_output( "git diff --unified=0 origin/develop HEAD " + fname, shell=True, universal_newlines=True ).splitlines() if diff_output: return collect_statistics(diff_output) except subprocess.CalledProcessError as e: print("Error in the git diff:") print(e.output) return None def stat(old, new): return ((new - old) / old) * 100 if old else 0 table = [] for path in Path("test/libsolidity/semanticTests").rglob("*.sol"): fname = path.as_posix() parsed = try_parse_git_diff(fname) if parsed is None: continue ir_optimized = stat(parsed[0], parsed[3]) legacy_optimized = stat(parsed[1], parsed[4]) legacy = stat(parsed[2], parsed[5]) fname = fname.split('/', 3)[-1] table += [map(str, [fname, ir_optimized, legacy_optimized, legacy])] if table: print("
Click for a table of gas differences\n") table_header = ["File name", "IR-optimized (%)", "Legacy-Optimized (%)", "Legacy (%)"] print(tabulate(table, headers=table_header, tablefmt="github")) print("
") else: print("No differences found.") if __name__ == "__main__": semantictest_statistics()