Skip to content

Commit 5adea18

Browse files
committed
Python 3.12 Preview: perf Profiler - Initial Commit (Materials)
1 parent 375f6a2 commit 5adea18

File tree

10 files changed

+370
-0
lines changed

10 files changed

+370
-0
lines changed

python-312-perf-profiler/README.md

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# Python 3.12 Preview: `perf` Profiler
2+
3+
This folder contains sample code for the [Python 3.12 Preview: Support For the Linux `perf` Profiler](https://realpython.com/python312-perf-profiler/) tutorial hosted on Real Python.
4+
5+
## 🛠️ Setup
6+
7+
### Python 3.12
8+
9+
Download, build, and install Python 3.12 from the source code with the frame pointer optimizations disabled:
10+
11+
```shell
12+
$ git clone --branch v3.12.0b2 https://github.com/python/cpython.git
13+
$ cd cpython/
14+
$ export CFLAGS='-fno-omit-frame-pointer -mno-omit-leaf-frame-pointer'
15+
$ ./configure --prefix="$HOME/python-custom-build"
16+
$ make -j $(nproc)
17+
$ make install
18+
```
19+
20+
### Virtual Environment
21+
22+
Create and activate a new virtual environment based on Python 3.12:
23+
24+
```shell
25+
$ "$HOME/python-custom-build/bin/python3" -m venv venv/ --prompt 'py3.12-custom'
26+
$ source venv/bin/activate
27+
```
28+
29+
### Dependencies
30+
31+
Install dependencies from the `requirements.txt` file into your virtual environment:
32+
33+
```shell
34+
(py3.12-custom) $ python -m pip install -r requirements.txt
35+
```
36+
37+
## 🏃‍♂️ Run Existing Profilers
38+
39+
### Profiler 1: `time`
40+
41+
```shell
42+
(py3.12-custom) $ python profile_time.py
43+
sleeper()
44+
Real time: 1.75 seconds
45+
CPU time: 0.00 seconds
46+
47+
spinlock()
48+
Real time: 1.80 seconds
49+
CPU time: 1.80 seconds
50+
```
51+
52+
### Profiler 2: `timeit`
53+
54+
```shell
55+
(py3.12-custom) $ python profile_timeit.py
56+
Average time is 0.15 seconds
57+
```
58+
59+
### Profiler 3: `cProfile`
60+
61+
```shell
62+
(py3.12-custom) $ python profile_cprofile.py
63+
fib(35) = 9227465
64+
29860712 function calls (10 primitive calls) in 9.487 seconds
65+
66+
Ordered by: call count
67+
68+
ncalls tottime percall cumtime percall filename:lineno(function)
69+
29860703/1 9.487 0.000 9.487 9.487 profile_cprofile.py:5(fib)
70+
1 0.000 0.000 0.000 0.000 pstats.py:118(init)
71+
1 0.000 0.000 0.000 0.000 pstats.py:137(load_stats)
72+
1 0.000 0.000 0.000 0.000 pstats.py:108(__init__)
73+
1 0.000 0.000 0.000 0.000 cProfile.py:51(create_stats)
74+
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
75+
1 0.000 0.000 0.000 0.000 {built-in method builtins.hasattr}
76+
1 0.000 0.000 0.000 0.000 {built-in method builtins.isinstance}
77+
1 0.000 0.000 0.000 0.000 {built-in method builtins.len}
78+
1 0.000 0.000 0.000 0.000 {built-in method builtins.print}
79+
```
80+
81+
### Profiler 4: `pyinstrument`
82+
83+
```shell
84+
(py3.12-custom) $ python profile_pyinstrument.py
85+
n = 10 [2.8, 2.8, 2.8, 3.2, 2.4]
86+
n = 100 [3.04, 3.28, 3.24, 2.92, 3.24]
87+
n = 1,000 [3.176, 3.192, 3.188, 3.076, 3.132]
88+
n = 10,000 [3.1376, 3.1328, 3.172, 3.1292, 3.1376]
89+
n = 100,000 [3.14636, 3.13424, 3.15104, 3.14212, 3.14612]
90+
n = 1,000,000 [3.141736, 3.141708, 3.14168, 3.141828, 3.140708]
91+
n = 10,000,000 [3.14143, 3.1403824, 3.1411704, 3.1408808, 3.1420068]
92+
93+
_ ._ __/__ _ _ _ _ _/_ Recorded: 11:49:03 Samples: 243
94+
/_//_/// /_\ / //_// / //_'/ // Duration: 24.334 CPU time: 24.298
95+
/ _/ v4.5.0
96+
97+
Program: profile_pyinstrument.py
98+
99+
24.300 <module> profile_pyinstrument.py:1
100+
└─ 24.300 estimate_pi profile_pyinstrument.py:6
101+
├─ 23.200 <genexpr> profile_pyinstrument.py:7
102+
│ ├─ 15.200 point profile_pyinstrument.py:14
103+
│ │ ├─ 7.700 Random.uniform random.py:520
104+
│ │ │ [4 frames hidden] random, <built-in>
105+
│ │ │ 6.200 [self] None
106+
│ │ └─ 7.500 [self] None
107+
│ ├─ 4.600 [self] None
108+
│ └─ 3.400 hits profile_pyinstrument.py:10
109+
│ ├─ 2.400 [self] None
110+
│ └─ 1.000 abs None
111+
│ [2 frames hidden] <built-in>
112+
└─ 1.100 [self] None
113+
```
114+
115+
## ⏲️ Profile Python With `perf`
116+
117+
Record Samples:
118+
119+
```shell
120+
$ cd benchmark/
121+
$ sudo perf record -g -F max ../venv/bin/python -X perf benchmark.py
122+
```
123+
124+
Display reports:
125+
126+
```shell
127+
$ cd benchmark/
128+
$ sudo perf report
129+
$ sudo perf report --stdio -g
130+
$ sudo perf report --hierarchy --verbose --call-graph fractal --sort sample,dso
131+
```
132+
133+
## 🔥 Flame Graphs
134+
135+
Download Perl scripts and add them to your `$PATH` environment variable:
136+
137+
```shell
138+
$ git clone [email protected]:brendangregg/FlameGraph.git
139+
$ export PATH="$(pwd)/FlameGraph:$PATH"
140+
```
141+
142+
Generate the flame graph and save it to a local file:
143+
144+
```shell
145+
$ cd benchmark/
146+
$ sudo perf script | stackcollapse-perf.pl | flamegraph.pl > flamegraph.svg
147+
```
148+
149+
Open the flame graph in your default SVG viewer: (Use your web browser for the interactive features.)
150+
151+
```shell
152+
$ xdg-open flamegraph.svg
153+
```
154+
155+
Produce a pure-Python flame graph by filtering and processing the collapsed stack traces:
156+
157+
```shell
158+
$ sudo perf script | stackcollapse-perf.pl > traces.txt
159+
$ cat traces.txt | python censor.py -m benchmark,PIL > traces_censored.txt
160+
$ cat traces_censored.txt | flamegraph.pl --minwidth 10 > flamegraph.svg
161+
```
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import sys
2+
import sysconfig
3+
from math import exp, log
4+
from statistics import mean
5+
6+
from PIL import Image
7+
8+
9+
def check_perf_support():
10+
if sys.version_info < (3, 12):
11+
version = sysconfig.get_python_version()
12+
raise RuntimeError(f"This is Python {version}, not 3.12 or later")
13+
14+
if not sysconfig.get_config_var("PY_HAVE_PERF_TRAMPOLINE"):
15+
raise RuntimeError("Python doesn't support perf on this platform")
16+
17+
if not sys.is_stack_trampoline_active():
18+
raise RuntimeError("Did you forget the '-X perf' option?")
19+
20+
cflags = sysconfig.get_config_var("CONFIGURE_CFLAGS")
21+
if "-fno-omit-frame-pointer" not in cflags:
22+
print("Python compiled without the frame pointer", file=sys.stderr)
23+
24+
25+
def main():
26+
image = Image.open("image.jpg")
27+
print("luminance =", get_average_luminance(image.getdata()))
28+
image.show()
29+
30+
31+
def get_average_luminance(pixels):
32+
return exp(mean(log(luminance(pixel) + 1e-9) for pixel in pixels))
33+
34+
35+
def luminance(pixel):
36+
red, green, blue = tuple(linearize(c) for c in pixel)
37+
return 0.2126 * red + 0.7152 * green + 0.0722 * blue
38+
39+
40+
def linearize(channel, gamma=2.2):
41+
return (channel / 255) ** gamma
42+
43+
44+
if __name__ == "__main__":
45+
try:
46+
check_perf_support()
47+
except RuntimeError as error:
48+
print(error, file=sys.stderr)
49+
else:
50+
main()
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import sys
2+
import sysconfig
3+
from math import exp, log
4+
5+
from PIL import Image
6+
7+
8+
def check_perf_support():
9+
if sys.version_info < (3, 12):
10+
version = sysconfig.get_python_version()
11+
raise RuntimeError(f"This is Python {version}, not 3.12 or later")
12+
13+
if not sysconfig.get_config_var("PY_HAVE_PERF_TRAMPOLINE"):
14+
raise RuntimeError("Python doesn't support perf on this platform")
15+
16+
if not sys.is_stack_trampoline_active():
17+
raise RuntimeError("Did you forget the '-X perf' option?")
18+
19+
cflags = sysconfig.get_config_var("CONFIGURE_CFLAGS")
20+
if "-fno-omit-frame-pointer" not in cflags:
21+
print("Python compiled without the frame pointer", file=sys.stderr)
22+
23+
24+
def main():
25+
image = Image.open("image.jpg")
26+
print("luminance =", get_average_luminance(image.getdata()))
27+
image.show()
28+
29+
30+
def get_average_luminance(pixels):
31+
return exp(mean([log(luminance(pixel) + 1e-9) for pixel in pixels]))
32+
33+
34+
def mean(n):
35+
return sum(n) / len(n)
36+
37+
38+
def luminance(pixel):
39+
red, green, blue = tuple(linearize(c) for c in pixel)
40+
return 0.2126 * red + 0.7152 * green + 0.0722 * blue
41+
42+
43+
def linearize(channel, gamma=2.2):
44+
return (channel / 255) ** gamma
45+
46+
47+
if __name__ == "__main__":
48+
try:
49+
check_perf_support()
50+
except RuntimeError as error:
51+
print(error, file=sys.stderr)
52+
else:
53+
main()
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import sys
2+
from argparse import ArgumentParser
3+
4+
5+
def main(args):
6+
censorship = censor(args.modules)
7+
for line in sys.stdin:
8+
stack_trace, num_samples = line.rsplit(maxsplit=1)
9+
symbols = filter(censorship, stack_trace.split(";"))
10+
censored_stack_trace = ";".join(symbols)
11+
if censored_stack_trace:
12+
print(censored_stack_trace, num_samples)
13+
14+
15+
def parse_args():
16+
parser = ArgumentParser()
17+
parser.add_argument(
18+
"-m", "--modules", default=[], type=lambda s: s.split(",")
19+
)
20+
return parser.parse_args()
21+
22+
23+
def censor(modules):
24+
def is_valid(symbol):
25+
if not symbol.startswith("py::"):
26+
return False
27+
if modules:
28+
return any(module in symbol for module in modules)
29+
return True
30+
31+
return is_valid
32+
33+
34+
if __name__ == "__main__":
35+
main(parse_args())
630 KB
Loading
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from cProfile import Profile
2+
from pstats import SortKey, Stats
3+
4+
5+
def fib(n):
6+
return n if n < 2 else fib(n - 2) + fib(n - 1)
7+
8+
9+
with Profile() as profile:
10+
print(f"{fib(35) = }")
11+
Stats(profile).strip_dirs().sort_stats(SortKey.CALLS).print_stats()
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from random import uniform
2+
3+
from pyinstrument import Profiler
4+
5+
6+
def estimate_pi(n):
7+
return 4 * sum(hits(point()) for _ in range(n)) / n
8+
9+
10+
def hits(point):
11+
return abs(point) <= 1
12+
13+
14+
def point():
15+
return complex(uniform(0, 1), uniform(0, 1))
16+
17+
18+
for exponent in range(1, 8):
19+
n = 10**exponent
20+
estimates = [estimate_pi(n) for _ in range(5)]
21+
print("n =", format(n, "<10,"), estimates)
22+
23+
24+
with Profiler(interval=0.1) as profiler:
25+
estimate_pi(n=10_000_000)
26+
27+
profiler.print()
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import time
2+
3+
4+
def sleeper():
5+
time.sleep(1.75)
6+
7+
8+
def spinlock():
9+
for _ in range(int(1e8)):
10+
pass
11+
12+
13+
for function in sleeper, spinlock:
14+
t1 = time.perf_counter(), time.process_time()
15+
function()
16+
t2 = time.perf_counter(), time.process_time()
17+
print(f"{function.__name__}()")
18+
print(f" Real time: {t2[0] - t1[0]:.2f} seconds")
19+
print(f" CPU time: {t2[1] - t1[1]:.2f} seconds")
20+
print()
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import timeit
2+
3+
4+
def fib(n):
5+
return n if n < 2 else fib(n - 2) + fib(n - 1)
6+
7+
8+
iterations = 100
9+
total_time = timeit.timeit("fib(30)", number=iterations, globals=globals())
10+
11+
print(f"Average time is {total_time / iterations:.2f} seconds")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Pillow==9.5.0
2+
pyinstrument==4.5.0

0 commit comments

Comments
 (0)