| """Compare performance of mypyc-compiled mypy between one or more commits/branches. | |
| Simple usage: | |
| python misc/perf_compare.py my-branch master ... | |
| What this does: | |
| * Create a temp clone of the mypy repo for each target commit to measure | |
| * Checkout a target commit in each of the clones | |
| * Compile mypyc in each of the clones *in parallel* | |
| * Create another temp clone of the mypy repo as the code to check | |
| * Self check with each of the compiled mypys N times | |
| * Report the average runtimes and relative performance | |
| * Remove the temp clones | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import glob | |
| import os | |
| import random | |
| import shutil | |
| import statistics | |
| import subprocess | |
| import sys | |
| import threading | |
| import time | |
| def heading(s: str) -> None: | |
| print() | |
| print(f"=== {s} ===") | |
| print() | |
| def build_mypy(target_dir: str) -> None: | |
| env = os.environ.copy() | |
| env["CC"] = "clang" | |
| env["MYPYC_OPT_LEVEL"] = "2" | |
| cmd = [sys.executable, "setup.py", "--use-mypyc", "build_ext", "--inplace"] | |
| subprocess.run(cmd, env=env, check=True, cwd=target_dir) | |
| def clone(target_dir: str, commit: str | None) -> None: | |
| heading(f"Cloning mypy to {target_dir}") | |
| repo_dir = os.getcwd() | |
| if os.path.isdir(target_dir): | |
| print(f"{target_dir} exists: deleting") | |
| shutil.rmtree(target_dir) | |
| subprocess.run(["git", "clone", repo_dir, target_dir], check=True) | |
| if commit: | |
| subprocess.run(["git", "checkout", commit], check=True, cwd=target_dir) | |
| def run_benchmark(compiled_dir: str, check_dir: str) -> float: | |
| cache_dir = os.path.join(compiled_dir, ".mypy_cache") | |
| if os.path.isdir(cache_dir): | |
| shutil.rmtree(cache_dir) | |
| env = os.environ.copy() | |
| env["PYTHONPATH"] = os.path.abspath(compiled_dir) | |
| abschk = os.path.abspath(check_dir) | |
| cmd = [ | |
| sys.executable, | |
| "-m", | |
| "mypy", | |
| "--config-file", | |
| os.path.join(abschk, "mypy_self_check.ini"), | |
| ] | |
| cmd += glob.glob(os.path.join(abschk, "mypy/*.py")) | |
| cmd += glob.glob(os.path.join(abschk, "mypy/*/*.py")) | |
| t0 = time.time() | |
| # Ignore errors, since some commits being measured may generate additional errors. | |
| subprocess.run(cmd, cwd=compiled_dir, env=env) | |
| return time.time() - t0 | |
| def main() -> None: | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("commit", nargs="+") | |
| args = parser.parse_args() | |
| commits = args.commit | |
| num_runs = 16 | |
| if not (os.path.isdir(".git") and os.path.isdir("mypyc")): | |
| sys.exit("error: Run this the mypy repo root") | |
| build_threads = [] | |
| target_dirs = [] | |
| for i, commit in enumerate(commits): | |
| target_dir = f"mypy.{i}.tmpdir" | |
| target_dirs.append(target_dir) | |
| clone(target_dir, commit) | |
| t = threading.Thread(target=lambda: build_mypy(target_dir)) | |
| t.start() | |
| build_threads.append(t) | |
| self_check_dir = "mypy.self.tmpdir" | |
| clone(self_check_dir, commits[0]) | |
| heading("Compiling mypy") | |
| print("(This will take a while...)") | |
| for t in build_threads: | |
| t.join() | |
| print(f"Finished compiling mypy ({len(commits)} builds)") | |
| heading("Performing measurements") | |
| results: dict[str, list[float]] = {} | |
| for n in range(num_runs): | |
| if n == 0: | |
| print("Warmup...") | |
| else: | |
| print(f"Run {n}/{num_runs - 1}...") | |
| items = list(enumerate(commits)) | |
| random.shuffle(items) | |
| for i, commit in items: | |
| tt = run_benchmark(target_dirs[i], self_check_dir) | |
| # Don't record the first warm-up run | |
| if n > 0: | |
| print(f"{commit}: t={tt:.3f}s") | |
| results.setdefault(commit, []).append(tt) | |
| print() | |
| heading("Results") | |
| first = -1.0 | |
| for commit in commits: | |
| tt = statistics.mean(results[commit]) | |
| if first < 0: | |
| delta = "0.0%" | |
| first = tt | |
| else: | |
| d = (tt / first) - 1 | |
| delta = f"{d:+.1%}" | |
| print(f"{commit:<25} {tt:.3f}s ({delta})") | |
| shutil.rmtree(self_check_dir) | |
| for target_dir in target_dirs: | |
| shutil.rmtree(target_dir) | |
| if __name__ == "__main__": | |
| main() |