Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
570 changes: 570 additions & 0 deletions Doc/_static/profiling-sampling-visualization.css

Large diffs are not rendered by default.

1,163 changes: 1,163 additions & 0 deletions Doc/_static/profiling-sampling-visualization.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
'issue_role',
'lexers',
'misc_news',
'profiling_trace',
'pydoc_topics',
'pyspecific',
'sphinx.ext.coverage',
Expand Down
1 change: 1 addition & 0 deletions Doc/library/profiling-sampling-visualization.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div id="sampling-profiler-viz" class="sampling-profiler-viz"></div>
19 changes: 18 additions & 1 deletion Doc/library/profiling.sampling.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,23 @@ of samples over a profiling session, Tachyon constructs an accurate statistical
estimate of where time is spent. The more samples collected, the
more precise this estimate becomes.

.. only:: html

The following interactive visualization demonstrates how sampling profiling
works. Press **Play** to watch a Python program execute, and observe how the
profiler periodically captures snapshots of the call stack. Adjust the
**sample interval** to see how sampling frequency affects the results.

.. raw:: html
:file: profiling-sampling-visualization.html

.. only:: not html

.. note::

An interactive visualization of sampling profiling is available in the
HTML version of this documentation.


How time is estimated
---------------------
Expand Down Expand Up @@ -354,7 +371,7 @@ Together, these determine how many samples will be collected during a profiling
session.

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

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

Expand Down
166 changes: 166 additions & 0 deletions Doc/tools/extensions/profiling_trace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""
Sphinx extension to generate profiler trace data during docs build.

This extension executes a demo Python program with sys.settrace() to capture
the execution trace and injects it into the profiling visualization JS file.
"""

import json
import re
import sys
from io import StringIO
from pathlib import Path

from sphinx.errors import ExtensionError

DEMO_SOURCE = """\
def add(a, b):
return a + b

def multiply(x, y):
result = 0
for i in range(y):
result = add(result, x)
return result

def calculate(a, b):
sum_val = add(a, b)
product = multiply(a, b)
return sum_val + product

def main():
result = calculate(3, 4)
print(f"Result: {result}")

main()
"""

PLACEHOLDER = "/* PROFILING_TRACE_DATA */null"


def generate_trace(source: str) -> list[dict]:
"""
Execute the source code with tracing enabled and capture execution events.
"""
trace_events = []
timestamp = [0]
timestamp_step = 50
tracing_active = [False]
pending_line = [None]

def tracer(frame, event, arg):
if frame.f_code.co_filename != '<demo>':
return tracer

func_name = frame.f_code.co_name
lineno = frame.f_lineno

if event == 'line' and not tracing_active[0]:
pending_line[0] = {'type': 'line', 'line': lineno}
return tracer

# Start tracing only once main() is called
if event == 'call' and func_name == 'main':
tracing_active[0] = True
# Emit the buffered line event (the main() call line) at ts=0
if pending_line[0]:
pending_line[0]['ts'] = 0
trace_events.append(pending_line[0])
pending_line[0] = None
timestamp[0] = timestamp_step

# Skip events until we've entered main()
if not tracing_active[0]:
return tracer

if event == 'call':
trace_events.append({
'type': 'call',
'func': func_name,
'line': lineno,
'ts': timestamp[0],
})
elif event == 'line':
trace_events.append({
'type': 'line',
'line': lineno,
'ts': timestamp[0],
})
elif event == 'return':
try:
value = arg if arg is None else repr(arg)
except Exception:
value = '<unprintable>'
trace_events.append({
'type': 'return',
'func': func_name,
'ts': timestamp[0],
'value': value,
})

if func_name == 'main':
tracing_active[0] = False

timestamp[0] += timestamp_step
return tracer

# Suppress print output during tracing
old_stdout = sys.stdout
sys.stdout = StringIO()

old_trace = sys.gettrace()
sys.settrace(tracer)
try:
code = compile(source, '<demo>', 'exec')
exec(code, {'__name__': '__main__'})
finally:
sys.settrace(old_trace)
sys.stdout = old_stdout

return trace_events


def inject_trace(app, exception):
if exception:
return

js_path = (
Path(app.outdir) / '_static' / 'profiling-sampling-visualization.js'
)

if not js_path.exists():
return

trace = generate_trace(DEMO_SOURCE)

demo_data = {'source': DEMO_SOURCE.rstrip(), 'trace': trace, 'samples': []}

demo_json = json.dumps(demo_data, indent=2)
content = js_path.read_text(encoding='utf-8')

pattern = r"(const DEMO_SIMPLE\s*=\s*/\* PROFILING_TRACE_DATA \*/)[^;]+;"

if re.search(pattern, content):
content = re.sub(
pattern, lambda m: f"{m.group(1)} {demo_json};", content
)
js_path.write_text(content, encoding='utf-8')
print(
f"profiling_trace: Injected {len(trace)} trace events into {js_path.name}"
)
else:
raise ExtensionError(
f"profiling_trace: Placeholder pattern not found in {js_path.name}"
)


def setup(app):
app.connect('build-finished', inject_trace)
app.add_js_file('profiling-sampling-visualization.js')
app.add_css_file('profiling-sampling-visualization.css')

return {
'version': '1.0',
'parallel_read_safe': True,
'parallel_write_safe': True,
}
2 changes: 1 addition & 1 deletion Lib/test/test_cext/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def main():
print(f"Add PCbuild directory: {pcbuild}")

# Display information to help debugging
for env_name in ('CC', 'CFLAGS'):
for env_name in ('CC', 'CFLAGS', 'CPPFLAGS'):
if env_name in os.environ:
print(f"{env_name} env var: {os.environ[env_name]!r}")
else:
Expand Down
5 changes: 3 additions & 2 deletions Lib/test/test_cppext/extension.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
#ifdef TEST_INTERNAL_C_API
// gh-135906: Check for compiler warnings in the internal C API
# include "internal/pycore_frame.h"
// mimalloc emits compiler warnings when Python is built on Windows.
# if !defined(MS_WINDOWS)
// mimalloc emits compiler warnings when Python is built on Windows
// and macOS.
# if !defined(MS_WINDOWS) && !defined(__APPLE__)
# include "internal/pycore_backoff.h"
# include "internal/pycore_cell.h"
# endif
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_cppext/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def main():
print(f"Add PCbuild directory: {pcbuild}")

# Display information to help debugging
for env_name in ('CC', 'CFLAGS', 'CPPFLAGS'):
for env_name in ('CC', 'CXX', 'CFLAGS', 'CPPFLAGS', 'CXXFLAGS'):
if env_name in os.environ:
print(f"{env_name} env var: {os.environ[env_name]!r}")
else:
Expand Down
Loading