Skip to content

Commit 09ce592

Browse files
authored
gh-142927: Hide _sync_coordinator frames from profiler output (#143337)
1 parent 315f474 commit 09ce592

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

0 commit comments

Comments
 (0)