Skip to content

Commit 3f2ab7e

Browse files
authored
Merge pull request #401 from realpython/python-312-perf-profiler
Python 3.12 Preview: perf Profiler - Initial Commit (Materials)
2 parents beff3a7 + 9215415 commit 3f2ab7e

File tree

10 files changed

+224
-3
lines changed

10 files changed

+224
-3
lines changed

python-311/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ This section only contains brief instructions on how you can run the examples. S
5050

5151
### Improved Error Messages
5252

53-
Run [`inverse.py](inverse.py) for a quick look at the improved tracebacks:
53+
Run [`inverse.py`](inverse.py) for a quick look at the improved tracebacks:
5454

5555
```console
5656
(venv) $ python inverse.py

python-312/README.md

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ You need Python 3.12 installed to run these examples. See the following tutorial
88

99
- [How Can You Install a Pre-Release Version of Python](https://realpython.com/python-pre-release/)
1010

11+
Note that for the `perf` support, you'll need to build Python from source code with additional compiler flags enabled, as [explained below](#support-for-the-linux-perf-profiler).
12+
1113
You can learn more about Python 3.12's new features in the following Real Python tutorials:
1214

1315
- [Python 3.12 Preview: Ever Better Error Messages](https://realpython.com/python312-error-messages/)
16+
- [Python 3.12 Preview: Support For the Linux `perf` Profiler](https://realpython.com/python312-perf-profiler/)
1417

1518
You'll find examples from all these tutorials in this repository.
1619

@@ -20,7 +23,7 @@ This section only contains brief instructions on how you can run the examples. S
2023

2124
### Improved Error Messages
2225

23-
Run [`encoder.py](encoder.py) to create an encoded message like the one shown in the tutorial. You can decode the message using [`decoder.py](decoder.py).
26+
Run [`encoder.py`](error-messages/encoder.py) to create an encoded message like the one shown in the tutorial. You can decode the message using [`decoder.py`](error-messages/decoder.py).
2427

2528
You can swap the import statement to `import d from this` in either of the files to encounter the improved error message:
2629

@@ -32,14 +35,93 @@ You can swap the import statement to `import d from this` in either of the files
3235
SyntaxError: Did you mean to use 'from ... import ...' instead?
3336
```
3437

35-
In [`local_self.py`](local_self.py), you can see a naive reproduction of another improved error message. Pick apart the example code to learn more about how this was implemented in Python 3.12.
38+
In [`local_self.py`](error-messages/local_self.py), you can see a naive reproduction of another improved error message. Pick apart the example code to learn more about how this was implemented in Python 3.12.
3639

3740
See [Ever Better Error Messages in Python 3.12](https://realpython.com/python312-error-messages/) for more information.
3841

42+
### Support For the Linux `perf` Profiler
43+
44+
#### Setting Up
45+
46+
You'll need to download, build, and install Python 3.12 from the source code with the frame pointer optimizations disabled:
47+
48+
```shell
49+
$ git clone --branch v3.12.0b2 https://github.com/python/cpython.git
50+
$ cd cpython/
51+
$ export CFLAGS='-fno-omit-frame-pointer -mno-omit-leaf-frame-pointer'
52+
$ ./configure --prefix="$HOME/python-custom-build"
53+
$ make -j $(nproc)
54+
$ make install
55+
```
56+
57+
Create and activate a new virtual environment based on Python 3.12:
58+
59+
```shell
60+
$ "$HOME/python-custom-build/bin/python3" -m venv venv/ --prompt 'py3.12-custom'
61+
$ source venv/bin/activate
62+
```
63+
64+
Install dependencies from the `requirements.txt` file into your virtual environment:
65+
66+
```shell
67+
(py3.12-custom) $ python -m pip install -r requirements.txt
68+
```
69+
70+
#### Using `perf` With Python
71+
72+
Record Samples:
73+
74+
```shell
75+
$ cd perf-profiler/
76+
$ sudo perf record -g -F max ../venv/bin/python -X perf benchmark.py
77+
```
78+
79+
Display reports:
80+
81+
```shell
82+
$ cd perf-profiler/
83+
$ sudo perf report
84+
$ sudo perf report --stdio -g
85+
$ sudo perf report --hierarchy --verbose --call-graph fractal --sort sample,dso
86+
```
87+
88+
#### Rendering Flame Graphs
89+
90+
Download Perl scripts and add them to your `$PATH` environment variable:
91+
92+
```shell
93+
$ git clone [email protected]:brendangregg/FlameGraph.git
94+
$ export PATH="$(pwd)/FlameGraph:$PATH"
95+
```
96+
97+
Generate the flame graph and save it to a local file:
98+
99+
```shell
100+
$ cd perf-profiler/
101+
$ sudo perf script | stackcollapse-perf.pl | flamegraph.pl > flamegraph.svg
102+
```
103+
104+
Open the flame graph in your default SVG viewer: (Use your web browser for the interactive features.)
105+
106+
```shell
107+
$ xdg-open flamegraph.svg
108+
```
109+
110+
Produce a pure-Python flame graph by filtering and processing the collapsed stack traces:
111+
112+
```shell
113+
$ sudo perf script | stackcollapse-perf.pl > traces.txt
114+
$ cat traces.txt | python censor.py -m benchmark,PIL > traces_censored.txt
115+
$ cat traces_censored.txt | flamegraph.pl --minwidth 10 > flamegraph.svg
116+
```
117+
118+
See [Support For the Linux `perf` Profiler in Python 3.12](https://realpython.com/python312-perf-profiler/) for more information.
119+
39120
## Authors
40121

41122
- **Martin Breuss**, E-mail: [[email protected]]([email protected])
42123
- **Bartosz Zaczyński**, E-mail: [[email protected]]([email protected])
124+
- **Leodanis Pozo Ramos**, E-mail: [[email protected]]([email protected])
43125
- **Geir Arne Hjelle**, E-mail: [[email protected]]([email protected])
44126

45127
## License
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()

python-312/perf-profiler/censor.py

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())

python-312/perf-profiler/image.jpg

630 KB
Loading

python-312/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Pillow==9.5.0

0 commit comments

Comments
 (0)