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
17 changes: 17 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,20 @@ Lib/test/certdata/*.0 generated
Lib/test/levenshtein_examples.json generated
Lib/test/test_stable_abi_ctypes.py generated
Lib/token.py generated

# Preserve canonical profiling resource bytes for frozen asset hashing.
Lib/profiling/sampling/_flamegraph_assets/flamegraph.css text eol=lf
Lib/profiling/sampling/_flamegraph_assets/flamegraph.js text eol=lf
Lib/profiling/sampling/_flamegraph_assets/flamegraph_template.html text eol=lf
Lib/profiling/sampling/_heatmap_assets/heatmap.css text eol=lf
Lib/profiling/sampling/_heatmap_assets/heatmap.js text eol=lf
Lib/profiling/sampling/_heatmap_assets/heatmap_index.js text eol=lf
Lib/profiling/sampling/_heatmap_assets/heatmap_index_template.html text eol=lf
Lib/profiling/sampling/_heatmap_assets/heatmap_pyfile_template.html text eol=lf
Lib/profiling/sampling/_heatmap_assets/heatmap_shared.js text eol=lf
Lib/profiling/sampling/_shared_assets/base.css text eol=lf
Lib/profiling/sampling/_shared_assets/base.js text eol=lf
Lib/profiling/sampling/_vendor/d3/7.8.5/d3.min.js text eol=lf
Lib/profiling/sampling/_vendor/d3-flame-graph/4.1.3/d3-flamegraph-tooltip.min.js text eol=lf
Lib/profiling/sampling/_vendor/d3-flame-graph/4.1.3/d3-flamegraph.css text eol=lf
Lib/profiling/sampling/_vendor/d3-flame-graph/4.1.3/d3-flamegraph.min.js text eol=lf
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ homepage = "https://rustpython.github.io/"
license_file = "LICENSE"
authors = ["RustPython Team"]
publisher = "RustPython Team"
resources = ["LICENSE", "README.md", "Lib"]
resources = ["LICENSE", "crates/pylib/LICENSE-profiling", "README.md", "Lib"]
icons = ["32x32.png"]

[package.metadata.packager.nsis]
Expand Down
47 changes: 29 additions & 18 deletions Lib/asyncio/tools.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Tools to analyze tasks running in asyncio programs."""

from collections import defaultdict, namedtuple
from collections import defaultdict
from itertools import count
from enum import Enum
import sys
Expand All @@ -27,10 +27,10 @@ def __init__(
# ─── indexing helpers ───────────────────────────────────────────
def _format_stack_entry(elem: str|FrameInfo) -> str:
if not isinstance(elem, str):
if elem.lineno == 0 and elem.filename == "":
if elem.location.lineno == 0 and elem.filename == "":
return f"{elem.funcname}"
else:
return f"{elem.funcname} {elem.filename}:{elem.lineno}"
return f"{elem.funcname} {elem.filename}:{elem.location.lineno}"
return elem


Expand Down Expand Up @@ -231,27 +231,38 @@ def exit_with_permission_help_text():
print(
"Error: The specified process cannot be attached to due to insufficient permissions.\n"
"See the Python documentation for details on required privileges and troubleshooting:\n"
"https://docs.python.org/3.14/howto/remote_debugging.html#permission-requirements\n"
"https://docs.python.org/3/howto/remote_debugging.html#permission-requirements\n",
file=sys.stderr,
)
sys.exit(1)


def _get_awaited_by_tasks(pid: int) -> list:
try:
return get_all_awaited_by(pid)
except RuntimeError as e:
while e.__context__ is not None:
e = e.__context__
print(f"Error retrieving tasks: {e}")
sys.exit(1)
except PermissionError as e:
exit_with_permission_help_text()
_TRANSIENT_ERRORS = (RuntimeError, OSError, UnicodeDecodeError, MemoryError)


def _get_awaited_by_tasks(pid: int, retries: int = 3) -> list:
for attempt in range(retries + 1):
try:
return get_all_awaited_by(pid)
except PermissionError:
exit_with_permission_help_text()
except ProcessLookupError:
print(f"Error: process {pid} not found.", file=sys.stderr)
sys.exit(1)
except _TRANSIENT_ERRORS as e:
if attempt < retries:
continue
if isinstance(e, RuntimeError):
while e.__context__ is not None:
e = e.__context__
print(f"Error retrieving tasks: {e}", file=sys.stderr)
sys.exit(1)


def display_awaited_by_tasks_table(pid: int) -> None:
def display_awaited_by_tasks_table(pid: int, retries: int = 3) -> None:
"""Build and print a table of all pending tasks under `pid`."""

tasks = _get_awaited_by_tasks(pid)
tasks = _get_awaited_by_tasks(pid, retries=retries)
table = build_task_table(tasks)
# Print the table in a simple tabular format
print(
Expand All @@ -262,10 +273,10 @@ def display_awaited_by_tasks_table(pid: int) -> None:
print(f"{row[0]:<10} {row[1]:<20} {row[2]:<20} {row[3]:<50} {row[4]:<50} {row[5]:<15} {row[6]:<15}")


def display_awaited_by_tasks_tree(pid: int) -> None:
def display_awaited_by_tasks_tree(pid: int, retries: int = 3) -> None:
"""Build and print a tree of all pending tasks under `pid`."""

tasks = _get_awaited_by_tasks(pid)
tasks = _get_awaited_by_tasks(pid, retries=retries)
try:
result = build_async_tree(tasks)
except CycleFoundException as e:
Expand Down
13 changes: 13 additions & 0 deletions Lib/cProfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Compatibility wrapper for cProfile module.
This module maintains backward compatibility by importing from the new
profiling.tracing module.
"""

from profiling.tracing import run, runctx, Profile

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the profiling parent to frozen stdlib

In the default WASM/freeze-stdlib configuration, import cProfile reaches this import but cannot resolve its parent package: the freeze compiler only emits entries for .py files and only marks __init__.py entries as packages (crates/derive-impl/src/compile_bytecode.rs:172-235), while a repo-wide search finds no Lib/profiling/__init__.py and the frozen importer does exact-name lookups. The frozen map therefore contains profiling.tracing but no profiling, so deployments without the Lib filesystem cannot use the newly added cProfile module; add a minimal parent package or synthesize a frozen namespace entry.

AGENTS.md reference: AGENTS.md:L7-L11

Useful? React with 👍 / 👎.


__all__ = ["run", "runctx", "Profile"]

if __name__ == "__main__":
from profiling.tracing.__main__ import main
main()
10 changes: 9 additions & 1 deletion Lib/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,18 @@
import sys
import time
import marshal
import warnings

__all__ = ["run", "runctx", "Profile"]

# Emit deprecation warning as per PEP 799
warnings.warn(
"The profile module is deprecated and will be removed in Python 3.17. "
"Use profiling.tracing (or cProfile) for tracing profilers instead.",
DeprecationWarning,
stacklevel=2
)

# Sample timer for use with
#i_count = 0
#def integer_timer():
Expand Down Expand Up @@ -598,7 +607,6 @@ def main():
'__file__': spec.origin,
'__name__': spec.name,
'__package__': None,
'__cached__': None,
}
try:
runctx(code, globs, None, options.outfile, options.sort)
Expand Down
13 changes: 13 additions & 0 deletions Lib/profiling/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Python profiling tools.
This package provides two types of profilers:
- profiling.tracing: Deterministic tracing profiler that instruments every
function call and return. Higher overhead but provides exact call counts
and timing.
- profiling.sampling: Statistical sampling profiler that periodically samples
the call stack. Low overhead and suitable for production use.
"""

__all__ = ("tracing", "sampling")
23 changes: 23 additions & 0 deletions Lib/profiling/sampling/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Statistical sampling profiler for Python.
This module provides low-overhead profiling by periodically sampling the
call stack rather than tracing every function call.
"""

from .collector import Collector
from .pstats_collector import PstatsCollector
from .stack_collector import CollapsedStackCollector
from .heatmap_collector import HeatmapCollector
from .gecko_collector import GeckoCollector
from .jsonl_collector import JsonlCollector
from .string_table import StringTable

__all__ = (
"Collector",
"PstatsCollector",
"CollapsedStackCollector",
"HeatmapCollector",
"GeckoCollector",
"JsonlCollector",
"StringTable",
)
Binary file added Lib/profiling/sampling/_assets/tachyon-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions Lib/profiling/sampling/_css_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import importlib.resources


def get_combined_css(component: str) -> str:
template_dir = importlib.resources.files(__package__)

base_css = (template_dir / "_shared_assets" / "base.css").read_text(encoding="utf-8")

if component == "flamegraph":
component_css = (
template_dir / "_flamegraph_assets" / "flamegraph.css"
).read_text(encoding="utf-8")
elif component == "heatmap":
component_css = (template_dir / "_heatmap_assets" / "heatmap.css").read_text(
encoding="utf-8"
)
else:
raise ValueError(
f"Unknown component: {component}. Expected 'flamegraph' or 'heatmap'."
)

return f"{base_css}\n\n{component_css}"
Loading
Loading