Skip to content

Commit 5d2771d

Browse files
committed
gh-142927: Hide _sync_coordinator frames from profiler output
When running scripts via "python -m profiling.sampling run", the internal _sync_coordinator module appears in stack traces between runpy and user code. These frames are implementation details that clutter the output and provide no useful information to users analyzing their program's behavior. The fix adds a filter_internal_frames function that removes frames from _sync_coordinator.py anywhere in the call stack. This is applied in both the base Collector._iter_all_frames method and directly in GeckoCollector which bypasses the iterator. Tests cover all collector types: pstats, flamegraph, collapsed stack, and gecko formats.
1 parent 513ae17 commit 5d2771d

File tree

4 files changed

+184
-3
lines changed

4 files changed

+184
-3
lines changed

Lib/profiling/sampling/collector.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
THREAD_STATUS_GIL_REQUESTED,
77
THREAD_STATUS_UNKNOWN,
88
THREAD_STATUS_HAS_EXCEPTION,
9+
_INTERNAL_FRAME_SUFFIXES,
910
)
1011

1112
try:
@@ -42,6 +43,44 @@ def extract_lineno(location):
4243
return 0
4344
return location[0]
4445

46+
def _is_internal_frame(frame):
47+
"""Check if a frame is from an internal profiler module.
48+
49+
Args:
50+
frame: A frame object with filename attribute or a tuple with filename
51+
52+
Returns:
53+
bool: True if the frame is from an internal module that should be hidden
54+
"""
55+
if isinstance(frame, tuple):
56+
filename = frame[0] if frame else ""
57+
else:
58+
filename = getattr(frame, "filename", "")
59+
60+
if not filename:
61+
return False
62+
63+
return filename.endswith(_INTERNAL_FRAME_SUFFIXES)
64+
65+
66+
def filter_internal_frames(frames):
67+
"""Filter out internal profiler frames from the stack.
68+
69+
Internal frames (like _sync_coordinator) can appear anywhere in the
70+
call stack. This removes all such frames.
71+
72+
Args:
73+
frames: List of frame objects in leaf-to-root order
74+
75+
Returns:
76+
List of frames with internal frames removed
77+
"""
78+
if not frames:
79+
return frames
80+
81+
return [f for f in frames if not _is_internal_frame(f)]
82+
83+
4584
class Collector(ABC):
4685
@abstractmethod
4786
def collect(self, stack_frames, timestamps_us=None):
@@ -63,6 +102,11 @@ def collect_failed_sample(self):
63102
def export(self, filename):
64103
"""Export collected data to a file."""
65104

105+
@staticmethod
106+
def _filter_internal_frames(frames):
107+
"""Filter out internal profiler frames from the bottom of the stack."""
108+
return filter_internal_frames(frames)
109+
66110
def _iter_all_frames(self, stack_frames, skip_idle=False):
67111
for interpreter_info in stack_frames:
68112
for thread_info in interpreter_info.threads:
@@ -76,7 +120,10 @@ def _iter_all_frames(self, stack_frames, skip_idle=False):
76120
continue
77121
frames = thread_info.frame_info
78122
if frames:
79-
yield frames, thread_info.thread_id
123+
# Filter out internal profiler frames from the bottom of the stack
124+
frames = self._filter_internal_frames(frames)
125+
if frames:
126+
yield frames, thread_info.thread_id
80127

81128
def _iter_async_frames(self, awaited_info_list):
82129
# Phase 1: Index tasks and build parent relationships with pre-computed selection

Lib/profiling/sampling/constants.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@
2323
# Format: (lineno, end_lineno, col_offset, end_col_offset)
2424
DEFAULT_LOCATION = (0, 0, -1, -1)
2525

26+
# Internal frame path suffixes to filter from profiling output
27+
# These are internal profiler modules that should not appear in user-facing output
28+
_INTERNAL_FRAME_SUFFIXES = (
29+
"_sync_coordinator.py",
30+
)
31+
2632
# Thread status flags
2733
try:
2834
from _remote_debugging import (

Lib/profiling/sampling/gecko_collector.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import threading
77
import time
88

9-
from .collector import Collector
9+
from .collector import Collector, filter_internal_frames
1010
from .opcode_utils import get_opcode_info, format_opcode
1111
try:
1212
from _remote_debugging import THREAD_STATUS_HAS_GIL, THREAD_STATUS_ON_CPU, THREAD_STATUS_UNKNOWN, THREAD_STATUS_GIL_REQUESTED, THREAD_STATUS_HAS_EXCEPTION
@@ -172,7 +172,7 @@ def collect(self, stack_frames, timestamps_us=None):
172172
# Process threads
173173
for interpreter_info in stack_frames:
174174
for thread_info in interpreter_info.threads:
175-
frames = thread_info.frame_info
175+
frames = filter_internal_frames(thread_info.frame_info)
176176
tid = thread_info.thread_id
177177

178178
# Initialize thread if needed

Lib/test/test_profiling/test_sampling_profiler/test_collectors.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1825,3 +1825,131 @@ def test_gecko_collector_frame_format(self):
18251825
thread = profile["threads"][0]
18261826
# Should have recorded 3 functions
18271827
self.assertEqual(thread["funcTable"]["length"], 3)
1828+
1829+
1830+
class TestInternalFrameFiltering(unittest.TestCase):
1831+
"""Tests for filtering internal profiler frames from output."""
1832+
1833+
def test_filter_internal_frames(self):
1834+
"""Test that _sync_coordinator frames are filtered from anywhere in stack."""
1835+
from profiling.sampling.collector import filter_internal_frames
1836+
1837+
# Stack with _sync_coordinator in the middle (realistic scenario)
1838+
frames = [
1839+
MockFrameInfo("user_script.py", 10, "user_func"),
1840+
MockFrameInfo("/path/to/_sync_coordinator.py", 100, "main"),
1841+
MockFrameInfo("<frozen runpy>", 87, "_run_code"),
1842+
]
1843+
1844+
filtered = filter_internal_frames(frames)
1845+
self.assertEqual(len(filtered), 2)
1846+
self.assertEqual(filtered[0].filename, "user_script.py")
1847+
self.assertEqual(filtered[1].filename, "<frozen runpy>")
1848+
1849+
def test_pstats_collector_filters_internal_frames(self):
1850+
"""Test that PstatsCollector filters out internal frames."""
1851+
collector = PstatsCollector(sample_interval_usec=1000)
1852+
1853+
frames = [
1854+
MockInterpreterInfo(
1855+
0,
1856+
[
1857+
MockThreadInfo(
1858+
1,
1859+
[
1860+
MockFrameInfo("user_script.py", 10, "user_func"),
1861+
MockFrameInfo("/path/to/_sync_coordinator.py", 100, "main"),
1862+
MockFrameInfo("<frozen runpy>", 87, "_run_code"),
1863+
],
1864+
status=THREAD_STATUS_HAS_GIL,
1865+
)
1866+
],
1867+
)
1868+
]
1869+
collector.collect(frames)
1870+
1871+
self.assertEqual(len(collector.result), 2)
1872+
self.assertIn(("user_script.py", 10, "user_func"), collector.result)
1873+
self.assertIn(("<frozen runpy>", 87, "_run_code"), collector.result)
1874+
1875+
def test_gecko_collector_filters_internal_frames(self):
1876+
"""Test that GeckoCollector filters out internal frames."""
1877+
collector = GeckoCollector(sample_interval_usec=1000)
1878+
1879+
frames = [
1880+
MockInterpreterInfo(
1881+
0,
1882+
[
1883+
MockThreadInfo(
1884+
1,
1885+
[
1886+
MockFrameInfo("app.py", 50, "run"),
1887+
MockFrameInfo("/lib/_sync_coordinator.py", 100, "main"),
1888+
],
1889+
status=THREAD_STATUS_HAS_GIL,
1890+
)
1891+
],
1892+
)
1893+
]
1894+
collector.collect(frames)
1895+
1896+
profile = collector._build_profile()
1897+
string_array = profile["shared"]["stringArray"]
1898+
1899+
# Should not contain _sync_coordinator functions
1900+
for s in string_array:
1901+
self.assertNotIn("_sync_coordinator", s)
1902+
1903+
def test_flamegraph_collector_filters_internal_frames(self):
1904+
"""Test that FlamegraphCollector filters out internal frames."""
1905+
collector = FlamegraphCollector(sample_interval_usec=1000)
1906+
1907+
frames = [
1908+
MockInterpreterInfo(
1909+
0,
1910+
[
1911+
MockThreadInfo(
1912+
1,
1913+
[
1914+
MockFrameInfo("app.py", 50, "run"),
1915+
MockFrameInfo("/lib/_sync_coordinator.py", 100, "main"),
1916+
MockFrameInfo("<frozen runpy>", 87, "_run_code"),
1917+
],
1918+
status=THREAD_STATUS_HAS_GIL,
1919+
)
1920+
],
1921+
)
1922+
]
1923+
collector.collect(frames)
1924+
1925+
data = collector._convert_to_flamegraph_format()
1926+
strings = data.get("strings", [])
1927+
1928+
for s in strings:
1929+
self.assertNotIn("_sync_coordinator", s)
1930+
1931+
def test_collapsed_stack_collector_filters_internal_frames(self):
1932+
"""Test that CollapsedStackCollector filters out internal frames."""
1933+
collector = CollapsedStackCollector(sample_interval_usec=1000)
1934+
1935+
frames = [
1936+
MockInterpreterInfo(
1937+
0,
1938+
[
1939+
MockThreadInfo(
1940+
1,
1941+
[
1942+
MockFrameInfo("app.py", 50, "run"),
1943+
MockFrameInfo("/lib/_sync_coordinator.py", 100, "main"),
1944+
],
1945+
status=THREAD_STATUS_HAS_GIL,
1946+
)
1947+
],
1948+
)
1949+
]
1950+
collector.collect(frames)
1951+
1952+
# Check that no stack contains _sync_coordinator
1953+
for (call_tree, _), _ in collector.stack_counter.items():
1954+
for filename, _, _ in call_tree:
1955+
self.assertNotIn("_sync_coordinator", filename)

0 commit comments

Comments
 (0)