|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +Run LQP parser/pretty-printer benchmarks comparing old (Lark) vs new (generated). |
| 4 | +
|
| 5 | +Uses `uv run --with` to create ephemeral environments for each version. |
| 6 | +""" |
| 7 | + |
| 8 | +import json |
| 9 | +import os |
| 10 | +import subprocess |
| 11 | +import sys |
| 12 | +from pathlib import Path |
| 13 | + |
| 14 | +REPO_ROOT = Path(__file__).resolve().parent.parent.parent |
| 15 | +BENCHMARK_SCRIPT = Path(__file__).resolve().parent / "benchmark.py" |
| 16 | +SDK_PYTHON = REPO_ROOT / "sdks" / "python" |
| 17 | +TESTS_DIR = REPO_ROOT / "tests" |
| 18 | +OLD_PACKAGE = "lqp==0.2.3" |
| 19 | + |
| 20 | + |
| 21 | +def run_benchmark(label: str, with_pkg: str, iterations: int): |
| 22 | + """Run benchmark.py in an ephemeral uv environment.""" |
| 23 | + env = { |
| 24 | + **os.environ, |
| 25 | + "BENCH_TESTS_DIR": str(TESTS_DIR), |
| 26 | + "BENCH_ITERATIONS": str(iterations), |
| 27 | + } |
| 28 | + cmd = [ |
| 29 | + "uv", |
| 30 | + "run", |
| 31 | + "--no-project", |
| 32 | + "--with", |
| 33 | + with_pkg, |
| 34 | + "python", |
| 35 | + str(BENCHMARK_SCRIPT), |
| 36 | + ] |
| 37 | + print(f"Running {label} benchmarks...", file=sys.stderr) |
| 38 | + result = subprocess.run(cmd, capture_output=True, text=True, env=env) |
| 39 | + if result.stderr: |
| 40 | + print(result.stderr, end="", file=sys.stderr) |
| 41 | + if result.returncode != 0: |
| 42 | + print(f"error: {label} benchmark failed", file=sys.stderr) |
| 43 | + sys.exit(1) |
| 44 | + return json.loads(result.stdout) |
| 45 | + |
| 46 | + |
| 47 | +def fmt_speedup(old_ms, new_ms): |
| 48 | + """Format a speedup ratio.""" |
| 49 | + if new_ms > 0: |
| 50 | + return f"{old_ms / new_ms:>7.2f}x" |
| 51 | + return f"{'inf':>7}" |
| 52 | + |
| 53 | + |
| 54 | +def print_parse_table(old_by_file, new_by_file, all_files): |
| 55 | + """Print parser comparison table.""" |
| 56 | + print() |
| 57 | + print("## Parser") |
| 58 | + print() |
| 59 | + hdr = f"{'file':<25} {'old parse':>10} {'old p+emit':>11} {'new parse':>10} {'speedup':>8}" |
| 60 | + print(hdr) |
| 61 | + print("-" * len(hdr)) |
| 62 | + |
| 63 | + total_old = 0.0 |
| 64 | + total_old_emit = 0.0 |
| 65 | + total_new = 0.0 |
| 66 | + skipped = 0 |
| 67 | + compared = 0 |
| 68 | + |
| 69 | + for f in all_files: |
| 70 | + o = old_by_file[f] |
| 71 | + n = new_by_file[f] |
| 72 | + |
| 73 | + po = o["parse_ms"] |
| 74 | + peo = o.get("parse_emit_ms") |
| 75 | + pn = n["parse_ms"] |
| 76 | + |
| 77 | + if po is None or pn is None: |
| 78 | + print(f"{f:<25} {'skip':>10} {'skip':>11} {'skip':>10} {'':>8}") |
| 79 | + skipped += 1 |
| 80 | + continue |
| 81 | + |
| 82 | + compared += 1 |
| 83 | + pe = peo if peo else po |
| 84 | + total_old += po |
| 85 | + total_old_emit += pe |
| 86 | + total_new += pn |
| 87 | + |
| 88 | + print(f"{f:<25} {po:>9.3f}ms {pe:>10.3f}ms {pn:>9.3f}ms {fmt_speedup(pe, pn)}") |
| 89 | + |
| 90 | + print("-" * len(hdr)) |
| 91 | + print( |
| 92 | + f"{'TOTAL':<25} {total_old:>9.3f}ms {total_old_emit:>10.3f}ms" |
| 93 | + f" {total_new:>9.3f}ms {fmt_speedup(total_old_emit, total_new)}" |
| 94 | + ) |
| 95 | + |
| 96 | + print() |
| 97 | + print("old parse = Lark parse to IR") |
| 98 | + print("old p+emit = Lark parse to IR + ir_to_proto") |
| 99 | + print("new parse = generated LL(k) parser to protobuf") |
| 100 | + print("speedup = old p+emit / new parse") |
| 101 | + |
| 102 | + return compared, skipped |
| 103 | + |
| 104 | + |
| 105 | +def print_pretty_table(old_by_file, new_by_file, all_files): |
| 106 | + """Print pretty-printer comparison table.""" |
| 107 | + print() |
| 108 | + print("## Pretty-printer") |
| 109 | + print() |
| 110 | + hdr = f"{'file':<25} {'old':>10} {'new':>10} {'speedup':>8}" |
| 111 | + print(hdr) |
| 112 | + print("-" * len(hdr)) |
| 113 | + |
| 114 | + total_old = 0.0 |
| 115 | + total_new = 0.0 |
| 116 | + |
| 117 | + for f in all_files: |
| 118 | + o = old_by_file[f] |
| 119 | + n = new_by_file[f] |
| 120 | + |
| 121 | + pro = o["pretty_ms"] |
| 122 | + prn = n["pretty_ms"] |
| 123 | + |
| 124 | + if pro is None or prn is None: |
| 125 | + print(f"{f:<25} {'skip':>10} {'skip':>10} {'':>8}") |
| 126 | + continue |
| 127 | + |
| 128 | + total_old += pro |
| 129 | + total_new += prn |
| 130 | + |
| 131 | + print(f"{f:<25} {pro:>9.3f}ms {prn:>9.3f}ms {fmt_speedup(pro, prn)}") |
| 132 | + |
| 133 | + print("-" * len(hdr)) |
| 134 | + print( |
| 135 | + f"{'TOTAL':<25} {total_old:>9.3f}ms {total_new:>9.3f}ms {fmt_speedup(total_old, total_new)}" |
| 136 | + ) |
| 137 | + |
| 138 | + print() |
| 139 | + print("old = IR to text") |
| 140 | + print("new = protobuf to text") |
| 141 | + |
| 142 | + |
| 143 | +def print_comparison(old_data, new_data): |
| 144 | + """Print formatted comparison tables.""" |
| 145 | + old_by_file = {r["file"]: r for r in old_data["results"]} |
| 146 | + new_by_file = {r["file"]: r for r in new_data["results"]} |
| 147 | + all_files = sorted(set(old_by_file) & set(new_by_file)) |
| 148 | + |
| 149 | + compared, skipped = print_parse_table(old_by_file, new_by_file, all_files) |
| 150 | + print_pretty_table(old_by_file, new_by_file, all_files) |
| 151 | + |
| 152 | + print() |
| 153 | + print(f"Iterations per file: {old_data['iterations']}") |
| 154 | + print(f"Files compared: {compared}") |
| 155 | + if skipped: |
| 156 | + print(f"Files skipped: {skipped} (unsupported by old parser)") |
| 157 | + |
| 158 | + |
| 159 | +def main(): |
| 160 | + iterations = int(sys.argv[1]) if len(sys.argv) > 1 else 20 |
| 161 | + |
| 162 | + old_data = run_benchmark("old (Lark)", OLD_PACKAGE, iterations) |
| 163 | + new_data = run_benchmark("new (generated)", str(SDK_PYTHON), iterations) |
| 164 | + print_comparison(old_data, new_data) |
| 165 | + |
| 166 | + |
| 167 | +if __name__ == "__main__": |
| 168 | + main() |
0 commit comments