Skip to content
Open
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
33 changes: 30 additions & 3 deletions Lib/profiling/sampling/gecko_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
import io
import json
import os
from pathlib import Path, PureWindowsPath
import platform
import sys
import tempfile
import threading
import time

from .collector import Collector, filter_internal_frames
from .module_utils import extract_module_name, get_python_path_info
from .opcode_utils import get_opcode_info, format_opcode
try:
from _remote_debugging import THREAD_STATUS_HAS_GIL, THREAD_STATUS_ON_CPU, THREAD_STATUS_UNKNOWN, THREAD_STATUS_GIL_REQUESTED, THREAD_STATUS_HAS_EXCEPTION, THREAD_STATUS_MAIN_THREAD
Expand Down Expand Up @@ -144,6 +146,8 @@ def __init__(self, sample_interval_usec, *, skip_idle=False, opcodes=False):
self.opcodes_enabled = opcodes
self.start_time = time.monotonic() * 1000 # milliseconds since start

self._path_info = get_python_path_info()

# Global string table (shared across all threads)
self.global_strings = ["(root)"] # Start with root
self.global_string_map = {"(root)": 0}
Expand Down Expand Up @@ -643,9 +647,32 @@ def _get_or_create_resource(self, thread_data, filename):
resource_types = resource_table["type"]

resource_idx = len(resource_names)
resource_name = (
os.path.basename(filename) if "/" in filename else filename
)
if not filename or filename.startswith("<"):
resource_name = filename
else:
module_name, module_type = extract_module_name(
filename.replace("\\", "/"), self._path_info
)
if module_type != "other":
resource_name = module_name
else:
path = (
PureWindowsPath(filename)
if ("\\" in filename or (len(filename) > 1 and filename[1] == ":"))
else Path(filename)
)
if path.is_absolute() or bool(path.anchor):
parent = path.parent.name
name = path.name
if name == "__init__.py" and parent:
resource_name = parent
elif parent:
resource_name = f"{parent}/{name}"
else:
resource_name = name or filename
else:
resource_name = module_name

name_idx = self._intern_string(resource_name)

resource_libs.append(None)
Expand Down
122 changes: 122 additions & 0 deletions Lib/test/test_profiling/test_sampling_profiler/test_collectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,128 @@ def test_gecko_collector_basic(self):
self.assertGreater(stack_table["length"], 0)
self.assertGreater(len(stack_table["frame"]), 0)

def test_gecko_collector_resource_module_names(self):
"""Test that GeckoCollector resolves resources to module names and
distinguishes files with the same basename in different packages."""
collector = GeckoCollector(1000)

# 1. Package __init__.py files
file_pkg_a = "pkg_a/__init__.py"
file_pkg_b = "pkg_b/__init__.py"

# 2. Submodule names
file_submod = "pkg_a/utils.py"

# 3. Standard library file (using collector._path_info)
stdlib_dir = collector._path_info.get("stdlib")
file_stdlib = (
str(stdlib_dir / "os.py")
if stdlib_dir
else "/usr/lib/python3.15/os.py"
)

# 4. Synthetic files
file_stdin = "<stdin>"
file_string = "<string>"

# 5. Unresolved external paths (absolute paths outside sys.path)
file_ext_pkg = "/opt/external/deep/pkg_x/__init__.py"
file_ext_tool = "/opt/external/deep/pkg_x/tool.py"
file_root_tool = "/root_tool.py"

# 6. Windows-style backslash paths (relative and absolute)
file_win_pkg = r"pkg_w\__init__.py"
file_win_tool = r"pkg_w\utils.py"
file_win_ext_pkg = r"C:\external\pkg_x\__init__.py"
file_win_ext_tool = r"C:\external\pkg_x\tool.py"
file_win_root_tool = r"C:\tool.py"

test_cases = [
(file_pkg_a, 10, "func_pkg_a"),
(file_pkg_b, 20, "func_pkg_b"),
(file_submod, 30, "func_submod"),
(file_stdlib, 40, "func_stdlib"),
(file_stdin, 1, "func_stdin"),
(file_string, 2, "func_string"),
(file_ext_pkg, 10, "func_ext_pkg"),
(file_ext_tool, 20, "func_ext_tool"),
(file_root_tool, 30, "func_root_tool"),
(file_win_pkg, 10, "func_win_pkg"),
(file_win_tool, 20, "func_win_tool"),
(file_win_ext_pkg, 10, "func_win_ext_pkg"),
(file_win_ext_tool, 20, "func_win_ext_tool"),
(file_win_root_tool, 30, "func_win_root_tool"),
]

frames = [MockFrameInfo(fn, line, name) for fn, line, name in test_cases]
test_frames = [
MockInterpreterInfo(
0,
[
MockThreadInfo(
1,
frames,
status=THREAD_STATUS_MAIN_THREAD,
)
],
)
]
collector.collect(test_frames)
profile_data = collector._build_profile()

string_array = profile_data["shared"]["stringArray"]
thread_data = profile_data["threads"][0]
func_table = thread_data["funcTable"]
resource_table = thread_data["resourceTable"]

self.assertEqual(resource_table["length"], len(resource_table["name"]))
for r_type in resource_table["type"]:
self.assertEqual(r_type, gecko_collector.RESOURCE_TYPE_LIBRARY)

funcs_by_name = {
string_array[func_table["name"][i]]: (
func_table["resource"][i],
string_array[
resource_table["name"][func_table["resource"][i]]
],
)
for i in range(func_table["length"])
}

# 1. Distinct resource table entries and names for duplicate packages
res_idx_a, res_name_a = funcs_by_name["func_pkg_a"]
res_idx_b, res_name_b = funcs_by_name["func_pkg_b"]
self.assertNotEqual(res_idx_a, res_idx_b)
self.assertEqual(res_name_a, "pkg_a")
self.assertEqual(res_name_b, "pkg_b")
name_idx_a = resource_table["name"][res_idx_a]
name_idx_b = resource_table["name"][res_idx_b]
self.assertNotEqual(name_idx_a, name_idx_b)
self.assertEqual(string_array[name_idx_a], "pkg_a")
self.assertEqual(string_array[name_idx_b], "pkg_b")

# 2. Submodule names resolve to dotted names
self.assertEqual(funcs_by_name["func_submod"][1], "pkg_a.utils")

# 3. Standard library resolves to exact module name
self.assertEqual(funcs_by_name["func_stdlib"][1], "os")

# 4. Synthetic files preserve sensible names
self.assertEqual(funcs_by_name["func_stdin"][1], "<stdin>")
self.assertEqual(funcs_by_name["func_string"][1], "<string>")

# 5. Unresolved external paths provide readable fallbacks
self.assertEqual(funcs_by_name["func_ext_pkg"][1], "pkg_x")
self.assertEqual(funcs_by_name["func_ext_tool"][1], "pkg_x/tool.py")
self.assertEqual(funcs_by_name["func_root_tool"][1], "root_tool.py")

# 6. Windows-style paths resolve platform-independently
self.assertEqual(funcs_by_name["func_win_pkg"][1], "pkg_w")
self.assertEqual(funcs_by_name["func_win_tool"][1], "pkg_w.utils")
self.assertEqual(funcs_by_name["func_win_ext_pkg"][1], "pkg_x")
self.assertEqual(funcs_by_name["func_win_ext_tool"][1], "pkg_x/tool.py")
self.assertEqual(funcs_by_name["func_win_root_tool"][1], "tool.py")

def test_gecko_collector_async_aware(self):
collector = GeckoCollector(1000)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Record Python module names for resources in the Gecko sampling profiler
(:mod:`profiling.sampling`) where available.
Loading