Skip to content

Commit db22f07

Browse files
authored
Merge branch 'main' into disable-perf-old-macos
2 parents b4c70a4 + d18dbd5 commit db22f07

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2412
-153
lines changed

Doc/_static/profiling-sampling-visualization.css

Lines changed: 570 additions & 0 deletions
Large diffs are not rendered by default.

Doc/_static/profiling-sampling-visualization.js

Lines changed: 1163 additions & 0 deletions
Large diffs are not rendered by default.

Doc/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
'issue_role',
3434
'lexers',
3535
'misc_news',
36+
'profiling_trace',
3637
'pydoc_topics',
3738
'pyspecific',
3839
'sphinx.ext.coverage',

Doc/library/asyncio-eventloop.rst

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -297,8 +297,9 @@ clocks to track time.
297297
are called is undefined.
298298

299299
The optional positional *args* will be passed to the callback when
300-
it is called. If you want the callback to be called with keyword
301-
arguments use :func:`functools.partial`.
300+
it is called. Use :func:`functools.partial`
301+
:ref:`to pass keyword arguments <asyncio-pass-keywords>` to
302+
*callback*.
302303

303304
An optional keyword-only *context* argument allows specifying a
304305
custom :class:`contextvars.Context` for the *callback* to run in.
@@ -1034,8 +1035,8 @@ Watching file descriptors
10341035
.. method:: loop.add_writer(fd, callback, *args)
10351036

10361037
Start monitoring the *fd* file descriptor for write availability and
1037-
invoke *callback* with the specified arguments once *fd* is available for
1038-
writing.
1038+
invoke *callback* with the specified arguments *args* once *fd* is
1039+
available for writing.
10391040

10401041
Any preexisting callback registered for *fd* is cancelled and replaced by
10411042
*callback*.
@@ -1308,7 +1309,8 @@ Unix signals
13081309

13091310
.. method:: loop.add_signal_handler(signum, callback, *args)
13101311

1311-
Set *callback* as the handler for the *signum* signal.
1312+
Set *callback* as the handler for the *signum* signal,
1313+
passing *args* as positional arguments.
13121314

13131315
The callback will be invoked by *loop*, along with other queued callbacks
13141316
and runnable coroutines of that event loop. Unlike signal handlers
@@ -1343,7 +1345,8 @@ Executing code in thread or process pools
13431345

13441346
.. awaitablemethod:: loop.run_in_executor(executor, func, *args)
13451347

1346-
Arrange for *func* to be called in the specified executor.
1348+
Arrange for *func* to be called in the specified executor
1349+
passing *args* as positional arguments.
13471350

13481351
The *executor* argument should be an :class:`concurrent.futures.Executor`
13491352
instance. The default executor is used if *executor* is ``None``.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div id="sampling-profiler-viz" class="sampling-profiler-viz"></div>

Doc/library/profiling.rst

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ Choosing a profiler
6363

6464
For most performance analysis, use the statistical profiler
6565
(:mod:`profiling.sampling`). It has minimal overhead, works for both development
66-
and production, and provides rich visualization options including flamegraphs,
66+
and production, and provides rich visualization options including flame graphs,
6767
heatmaps, GIL analysis, and more.
6868

6969
Use the deterministic profiler (:mod:`profiling.tracing`) when you need **exact
@@ -81,7 +81,7 @@ The following table summarizes the key differences:
8181
+--------------------+------------------------------+------------------------------+
8282
| **Accuracy** | Statistical estimate | Exact call counts |
8383
+--------------------+------------------------------+------------------------------+
84-
| **Output formats** | pstats, flamegraph, heatmap, | pstats |
84+
| **Output formats** | pstats, flame graph, heatmap,| pstats |
8585
| | gecko, collapsed | |
8686
+--------------------+------------------------------+------------------------------+
8787
| **Profiling modes**| Wall-clock, CPU, GIL | Wall-clock |
@@ -103,7 +103,7 @@ performance analysis tasks. Use it the same way you would use
103103

104104
One of the main strengths of the sampling profiler is its variety of output
105105
formats. Beyond traditional pstats tables, it can generate interactive
106-
flamegraphs that visualize call hierarchies, line-level source heatmaps that
106+
flame graphs that visualize call hierarchies, line-level source heatmaps that
107107
show exactly where time is spent in your code, and Firefox Profiler output for
108108
timeline-based analysis.
109109

@@ -157,7 +157,7 @@ command::
157157
python -m profiling.sampling run -m mypackage.module
158158

159159
This runs the script under the profiler and prints a summary of where time was
160-
spent. For an interactive flamegraph::
160+
spent. For an interactive flame graph::
161161

162162
python -m profiling.sampling run --flamegraph script.py
163163

@@ -197,7 +197,7 @@ Understanding profile output
197197

198198
Both profilers collect function-level statistics, though they present them in
199199
different formats. The sampling profiler offers multiple visualizations
200-
(flamegraphs, heatmaps, Firefox Profiler, pstats tables), while the
200+
(flame graphs, heatmaps, Firefox Profiler, pstats tables), while the
201201
deterministic profiler produces pstats-compatible output. Regardless of format,
202202
the underlying concepts are the same.
203203

@@ -226,7 +226,7 @@ Key profiling concepts:
226226

227227
**Caller/Callee relationships**
228228
Which functions called a given function (callers) and which functions it
229-
called (callees). Flamegraphs visualize this as nested rectangles; pstats
229+
called (callees). Flame graphs visualize this as nested rectangles; pstats
230230
can display it via the :meth:`~pstats.Stats.print_callers` and
231231
:meth:`~pstats.Stats.print_callees` methods.
232232

@@ -248,7 +248,7 @@ continue to work without modification in all future Python versions.
248248
.. seealso::
249249

250250
:mod:`profiling.sampling`
251-
Statistical sampling profiler with flamegraphs, heatmaps, and GIL analysis.
251+
Statistical sampling profiler with flame graphs, heatmaps, and GIL analysis.
252252
Recommended for most users.
253253

254254
:mod:`profiling.tracing`

Doc/library/profiling.sampling.rst

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,23 @@ of samples over a profiling session, Tachyon constructs an accurate statistical
4444
estimate of where time is spent. The more samples collected, the
4545
more precise this estimate becomes.
4646

47+
.. only:: html
48+
49+
The following interactive visualization demonstrates how sampling profiling
50+
works. Press **Play** to watch a Python program execute, and observe how the
51+
profiler periodically captures snapshots of the call stack. Adjust the
52+
**sample interval** to see how sampling frequency affects the results.
53+
54+
.. raw:: html
55+
:file: profiling-sampling-visualization.html
56+
57+
.. only:: not html
58+
59+
.. note::
60+
61+
An interactive visualization of sampling profiling is available in the
62+
HTML version of this documentation.
63+
4764

4865
How time is estimated
4966
---------------------
@@ -354,7 +371,7 @@ Together, these determine how many samples will be collected during a profiling
354371
session.
355372

356373
The :option:`--sampling-rate` option (:option:`-r`) sets how frequently samples
357-
are collected. The default is 1 kHz (10,000 samples per second)::
374+
are collected. The default is 1 kHz (1,000 samples per second)::
358375

359376
python -m profiling.sampling run -r 20khz script.py
360377

Doc/library/stdtypes.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2870,6 +2870,14 @@ expression support in the :mod:`re` module).
28702870
You can use :meth:`str.maketrans` to create a translation map from
28712871
character-to-character mappings in different formats.
28722872

2873+
The following example uses a mapping to replace ``'a'`` with ``'X'``,
2874+
``'b'`` with ``'Y'``, and delete ``'c'``:
2875+
2876+
.. doctest::
2877+
2878+
>>> 'abc123'.translate({ord('a'): 'X', ord('b'): 'Y', ord('c'): None})
2879+
'XY123'
2880+
28732881
See also the :mod:`codecs` module for a more flexible approach to custom
28742882
character mappings.
28752883

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
"""
2+
Sphinx extension to generate profiler trace data during docs build.
3+
4+
This extension executes a demo Python program with sys.settrace() to capture
5+
the execution trace and injects it into the profiling visualization JS file.
6+
"""
7+
8+
import json
9+
import re
10+
import sys
11+
from io import StringIO
12+
from pathlib import Path
13+
14+
from sphinx.errors import ExtensionError
15+
16+
DEMO_SOURCE = """\
17+
def add(a, b):
18+
return a + b
19+
20+
def multiply(x, y):
21+
result = 0
22+
for i in range(y):
23+
result = add(result, x)
24+
return result
25+
26+
def calculate(a, b):
27+
sum_val = add(a, b)
28+
product = multiply(a, b)
29+
return sum_val + product
30+
31+
def main():
32+
result = calculate(3, 4)
33+
print(f"Result: {result}")
34+
35+
main()
36+
"""
37+
38+
PLACEHOLDER = "/* PROFILING_TRACE_DATA */null"
39+
40+
41+
def generate_trace(source: str) -> list[dict]:
42+
"""
43+
Execute the source code with tracing enabled and capture execution events.
44+
"""
45+
trace_events = []
46+
timestamp = [0]
47+
timestamp_step = 50
48+
tracing_active = [False]
49+
pending_line = [None]
50+
51+
def tracer(frame, event, arg):
52+
if frame.f_code.co_filename != '<demo>':
53+
return tracer
54+
55+
func_name = frame.f_code.co_name
56+
lineno = frame.f_lineno
57+
58+
if event == 'line' and not tracing_active[0]:
59+
pending_line[0] = {'type': 'line', 'line': lineno}
60+
return tracer
61+
62+
# Start tracing only once main() is called
63+
if event == 'call' and func_name == 'main':
64+
tracing_active[0] = True
65+
# Emit the buffered line event (the main() call line) at ts=0
66+
if pending_line[0]:
67+
pending_line[0]['ts'] = 0
68+
trace_events.append(pending_line[0])
69+
pending_line[0] = None
70+
timestamp[0] = timestamp_step
71+
72+
# Skip events until we've entered main()
73+
if not tracing_active[0]:
74+
return tracer
75+
76+
if event == 'call':
77+
trace_events.append({
78+
'type': 'call',
79+
'func': func_name,
80+
'line': lineno,
81+
'ts': timestamp[0],
82+
})
83+
elif event == 'line':
84+
trace_events.append({
85+
'type': 'line',
86+
'line': lineno,
87+
'ts': timestamp[0],
88+
})
89+
elif event == 'return':
90+
try:
91+
value = arg if arg is None else repr(arg)
92+
except Exception:
93+
value = '<unprintable>'
94+
trace_events.append({
95+
'type': 'return',
96+
'func': func_name,
97+
'ts': timestamp[0],
98+
'value': value,
99+
})
100+
101+
if func_name == 'main':
102+
tracing_active[0] = False
103+
104+
timestamp[0] += timestamp_step
105+
return tracer
106+
107+
# Suppress print output during tracing
108+
old_stdout = sys.stdout
109+
sys.stdout = StringIO()
110+
111+
old_trace = sys.gettrace()
112+
sys.settrace(tracer)
113+
try:
114+
code = compile(source, '<demo>', 'exec')
115+
exec(code, {'__name__': '__main__'})
116+
finally:
117+
sys.settrace(old_trace)
118+
sys.stdout = old_stdout
119+
120+
return trace_events
121+
122+
123+
def inject_trace(app, exception):
124+
if exception:
125+
return
126+
127+
js_path = (
128+
Path(app.outdir) / '_static' / 'profiling-sampling-visualization.js'
129+
)
130+
131+
if not js_path.exists():
132+
return
133+
134+
trace = generate_trace(DEMO_SOURCE)
135+
136+
demo_data = {'source': DEMO_SOURCE.rstrip(), 'trace': trace, 'samples': []}
137+
138+
demo_json = json.dumps(demo_data, indent=2)
139+
content = js_path.read_text(encoding='utf-8')
140+
141+
pattern = r"(const DEMO_SIMPLE\s*=\s*/\* PROFILING_TRACE_DATA \*/)[^;]+;"
142+
143+
if re.search(pattern, content):
144+
content = re.sub(
145+
pattern, lambda m: f"{m.group(1)} {demo_json};", content
146+
)
147+
js_path.write_text(content, encoding='utf-8')
148+
print(
149+
f"profiling_trace: Injected {len(trace)} trace events into {js_path.name}"
150+
)
151+
else:
152+
raise ExtensionError(
153+
f"profiling_trace: Placeholder pattern not found in {js_path.name}"
154+
)
155+
156+
157+
def setup(app):
158+
app.connect('build-finished', inject_trace)
159+
app.add_js_file('profiling-sampling-visualization.js')
160+
app.add_css_file('profiling-sampling-visualization.css')
161+
162+
return {
163+
'version': '1.0',
164+
'parallel_read_safe': True,
165+
'parallel_write_safe': True,
166+
}

0 commit comments

Comments
 (0)