Skip to content

Commit 0629b78

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 0629b78

File tree

4 files changed

+164
-3
lines changed

4 files changed

+164
-3
lines changed

Lib/profiling/sampling/collector.py

Lines changed: 28 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,25 @@ def extract_lineno(location):
4243
return 0
4344
return location[0]
4445

46+
def _is_internal_frame(frame):
47+
if isinstance(frame, tuple):
48+
filename = frame[0] if frame else ""
49+
else:
50+
filename = getattr(frame, "filename", "")
51+
52+
if not filename:
53+
return False
54+
55+
return filename.endswith(_INTERNAL_FRAME_SUFFIXES)
56+
57+
58+
def filter_internal_frames(frames):
59+
if not frames:
60+
return frames
61+
62+
return [f for f in frames if not _is_internal_frame(f)]
63+
64+
4565
class Collector(ABC):
4666
@abstractmethod
4767
def collect(self, stack_frames, timestamps_us=None):
@@ -63,6 +83,10 @@ def collect_failed_sample(self):
6383
def export(self, filename):
6484
"""Export collected data to a file."""
6585

86+
@staticmethod
87+
def _filter_internal_frames(frames):
88+
return filter_internal_frames(frames)
89+
6690
def _iter_all_frames(self, stack_frames, skip_idle=False):
6791
for interpreter_info in stack_frames:
6892
for thread_info in interpreter_info.threads:
@@ -76,7 +100,10 @@ def _iter_all_frames(self, stack_frames, skip_idle=False):
76100
continue
77101
frames = thread_info.frame_info
78102
if frames:
79-
yield frames, thread_info.thread_id
103+
# Filter out internal profiler frames from the bottom of the stack
104+
frames = self._filter_internal_frames(frames)
105+
if frames:
106+
yield frames, thread_info.thread_id
80107

81108
def _iter_async_frames(self, awaited_info_list):
82109
# 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)