Skip to content

Commit 12a8138

Browse files
committed
Add script for profiling self check (Linux only)
The script compiles mypy and profiles self check using the 'perf' profiler. Example of how to use this: ``` $ python misc/profile_self_check.py ... [will take several minutes] CPU profile collected. You can now analyze the profile: perf report -i mypy.profile.tmpdir/perf.data ```
1 parent 34949c8 commit 12a8138

File tree

2 files changed

+129
-1
lines changed

2 files changed

+129
-1
lines changed

misc/perf_compare.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def heading(s: str) -> None:
3535
print()
3636

3737

38-
def build_mypy(target_dir: str, multi_file: bool) -> None:
38+
def build_mypy(target_dir: str, multi_file: bool, *, cflags: str | None = None) -> None:
3939
env = os.environ.copy()
4040
env["CC"] = "clang"
4141
env["MYPYC_OPT_LEVEL"] = "2"

misc/profile_self_check.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
"""Compile mypy using mypyc and profile self-check using perf.
2+
3+
Notes:
4+
- Only Linux is supported for now (TODO: add support for other profilers)
5+
- The profile is collected at C level
6+
- It includes C functions compiled by mypyc and CPython runtime functions
7+
- The names of mypy functions are mangled to C names, but usually it's clear what they mean
8+
- Generally CPyDef_ prefix for native functions and CPyPy_ prefix for wrapper functions
9+
- It's important to compile CPython using special flags (see below) to get good results
10+
- Generally use the latest Python feature release (or the most recent beta if supported by mypyc)
11+
- The tool prints a command that can be used to analyze the profile afterwards
12+
13+
You may need to adjust kernel parameters temporarily, e.g. this (note that this has security
14+
impliciations):
15+
16+
sudo sysctl kernel.perf_event_paranoid=-1
17+
18+
This is the recommended way to configure CPython for profiling:
19+
20+
./configure \
21+
--enable-optimizations \
22+
--with-lto \
23+
CFLAGS="-O2 -g -fno-omit-frame-pointer"
24+
"""
25+
26+
import argparse
27+
import glob
28+
import os
29+
import shutil
30+
import subprocess
31+
import sys
32+
import time
33+
34+
from perf_compare import build_mypy, clone
35+
36+
# Use these C compiler flags when compiling mypy (important). Note that it's strongly recommended
37+
# to also compile CPython using similar flags, but we don't enforce it in this script.
38+
CFLAGS = "-O2 -fno-omit-frame-pointer -g"
39+
40+
41+
def _profile_self_check(target_dir: str) -> None:
42+
cache_dir = os.path.join(target_dir, ".mypy_cache")
43+
if os.path.exists(cache_dir):
44+
shutil.rmtree(cache_dir)
45+
files = []
46+
for pat in "mypy/*.py", "mypy/*/*.py", "mypyc/*.py", "mypyc/test/*.py":
47+
files.extend(glob.glob(pat))
48+
self_check_cmd = ["python", "-m", "mypy", "--config-file", "mypy_self_check.ini"] + files
49+
cmdline = ["perf", "record", "-g"] + self_check_cmd
50+
t0 = time.time()
51+
subprocess.run(cmdline, cwd=target_dir, check=True)
52+
elapsed = time.time() - t0
53+
print(f"{elapsed:.2f}s elapsed")
54+
55+
56+
def profile_self_check(target_dir: str) -> None:
57+
try:
58+
_profile_self_check(target_dir)
59+
except subprocess.CalledProcessError:
60+
print("\nProfiling failed! You may missing some permissions.")
61+
print("\nThis may help (note that it has security implications):")
62+
print(" sudo sysctl kernel.perf_event_paranoid=-1")
63+
sys.exit(1)
64+
65+
66+
def check_requirements() -> None:
67+
if sys.platform != "linux":
68+
# TODO: How to make this work on other platforms?
69+
sys.exit("error: Only Linux is supported")
70+
71+
try:
72+
subprocess.run(["perf", "-h"], capture_output=True)
73+
except subprocess.CalledProcessError:
74+
print("error: The 'perf' profiler is not installed")
75+
sys.exit(1)
76+
77+
try:
78+
subprocess.run(["clang", "--version"], capture_output=True)
79+
except subprocess.CalledProcessError:
80+
print("error: The clang compiler is not installed")
81+
sys.exit(1)
82+
83+
if not os.path.isfile("mypy_self_check.ini"):
84+
print("error: Run this in the mypy repository root")
85+
sys.exit(1)
86+
87+
88+
def main() -> None:
89+
check_requirements()
90+
91+
parser = argparse.ArgumentParser(
92+
description="Compile mypy and profile self checking using 'perf'."
93+
)
94+
parser.add_argument(
95+
"--multi-file",
96+
action="store_true",
97+
help="compile mypy into one C file per module (to reduce RAM use during compilation)",
98+
)
99+
parser.add_argument(
100+
"--skip-compile", action="store_true", help="use compiled mypy from previous run"
101+
)
102+
args = parser.parse_args()
103+
multi_file: bool = args.multi_file
104+
skip_compile: bool = args.skip_compile
105+
106+
target_dir = "."
107+
108+
target_dir = "mypy.profile.tmpdir"
109+
110+
if not skip_compile:
111+
clone(target_dir, "HEAD")
112+
113+
print(f"Building mypy in {target_dir}...")
114+
build_mypy(target_dir, multi_file, cflags=CFLAGS)
115+
elif not os.path.isdir(target_dir):
116+
sys.exit("error: Can't find compile mypy from previous run -- can't use --skip-compile")
117+
118+
profile_self_check(target_dir)
119+
120+
print()
121+
print('NOTE: Compile CPython using CFLAGS="-O2 -g -fno-omit-frame-pointer" for good results')
122+
print()
123+
print("CPU profile collected. You can now analyze the profile:")
124+
print(f" perf report -i {target_dir}/perf.data ")
125+
126+
127+
if __name__ == "__main__":
128+
main()

0 commit comments

Comments
 (0)