Skip to content

Commit 1e9010c

Browse files
committed
gh-138385: Sample all interpreters in the tachyon profiler
1 parent 552cf86 commit 1e9010c

File tree

6 files changed

+472
-268
lines changed

6 files changed

+472
-268
lines changed

Lib/profiling/sampling/collector.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,11 @@ def collect(self, stack_frames):
99
@abstractmethod
1010
def export(self, filename):
1111
"""Export collected data to a file."""
12+
13+
def _iter_all_frames(self, stack_frames):
14+
"""Iterate over all frame stacks from all interpreters and threads."""
15+
for interpreter_info in stack_frames:
16+
for thread_info in interpreter_info.threads:
17+
frames = thread_info.frame_info
18+
if frames:
19+
yield frames

Lib/profiling/sampling/pstats_collector.py

Lines changed: 22 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,43 +15,33 @@ def __init__(self, sample_interval_usec):
1515
lambda: collections.defaultdict(int)
1616
)
1717

18-
def collect(self, stack_frames):
19-
for thread_id, frames in stack_frames:
20-
if not frames:
21-
continue
22-
23-
# Process each frame in the stack to track cumulative calls
24-
for frame in frames:
25-
location = (frame.filename, frame.lineno, frame.funcname)
26-
self.result[location]["cumulative_calls"] += 1
18+
def _process_frames(self, frames):
19+
"""Process a single thread's frame stack."""
20+
if not frames:
21+
return
22+
23+
# Process each frame in the stack to track cumulative calls
24+
for frame in frames:
25+
location = (frame.filename, frame.lineno, frame.funcname)
26+
self.result[location]["cumulative_calls"] += 1
2727

28-
# The top frame gets counted as an inline call (directly executing)
29-
top_frame = frames[0]
30-
top_location = (
31-
top_frame.filename,
32-
top_frame.lineno,
33-
top_frame.funcname,
34-
)
28+
# The top frame gets counted as an inline call (directly executing)
29+
top_location = (frames[0].filename, frames[0].lineno, frames[0].funcname)
30+
self.result[top_location]["direct_calls"] += 1
3531

36-
self.result[top_location]["direct_calls"] += 1
32+
# Track caller-callee relationships for call graph
33+
for i in range(1, len(frames)):
34+
callee_frame = frames[i - 1]
35+
caller_frame = frames[i]
3736

38-
# Track caller-callee relationships for call graph
39-
for i in range(1, len(frames)):
40-
callee_frame = frames[i - 1]
41-
caller_frame = frames[i]
37+
callee = (callee_frame.filename, callee_frame.lineno, callee_frame.funcname)
38+
caller = (caller_frame.filename, caller_frame.lineno, caller_frame.funcname)
4239

43-
callee = (
44-
callee_frame.filename,
45-
callee_frame.lineno,
46-
callee_frame.funcname,
47-
)
48-
caller = (
49-
caller_frame.filename,
50-
caller_frame.lineno,
51-
caller_frame.funcname,
52-
)
40+
self.callers[callee][caller] += 1
5341

54-
self.callers[callee][caller] += 1
42+
def collect(self, stack_frames):
43+
for frames in self._iter_all_frames(stack_frames):
44+
self._process_frames(frames)
5545

5646
def export(self, filename):
5747
self.create_stats()

Lib/profiling/sampling/stack_collector.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,22 @@ def __init__(self):
99
self.call_trees = []
1010
self.function_samples = collections.defaultdict(int)
1111

12+
def _process_frames(self, frames):
13+
"""Process a single thread's frame stack."""
14+
if not frames:
15+
return
16+
17+
# Store the complete call stack (reverse order - root first)
18+
call_tree = list(reversed(frames))
19+
self.call_trees.append(call_tree)
20+
21+
# Count samples per function
22+
for frame in frames:
23+
self.function_samples[frame] += 1
24+
1225
def collect(self, stack_frames):
13-
for thread_id, frames in stack_frames:
14-
if frames:
15-
# Store the complete call stack (reverse order - root first)
16-
call_tree = list(reversed(frames))
17-
self.call_trees.append(call_tree)
18-
19-
# Count samples per function
20-
for frame in frames:
21-
self.function_samples[frame] += 1
26+
for frames in self._iter_all_frames(stack_frames):
27+
self._process_frames(frames)
2228

2329

2430
class CollapsedStackCollector(StackTraceCollector):

Lib/test/test_external_inspection.py

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,27 @@ def foo():
140140
]
141141
# Is possible that there are more threads, so we check that the
142142
# expected stack traces are in the result (looking at you Windows!)
143-
self.assertIn((ANY, thread_expected_stack_trace), stack_trace)
143+
found_expected_stack = False
144+
for interpreter_info in stack_trace:
145+
for thread_info in interpreter_info.threads:
146+
if thread_info.frame_info == thread_expected_stack_trace:
147+
found_expected_stack = True
148+
break
149+
if found_expected_stack:
150+
break
151+
self.assertTrue(found_expected_stack, "Expected thread stack trace not found")
144152

145153
# Check that the main thread stack trace is in the result
146154
frame = FrameInfo([script_name, 19, "<module>"])
147-
for _, stack in stack_trace:
148-
if frame in stack:
155+
main_thread_found = False
156+
for interpreter_info in stack_trace:
157+
for thread_info in interpreter_info.threads:
158+
if frame in thread_info.frame_info:
159+
main_thread_found = True
160+
break
161+
if main_thread_found:
149162
break
150-
else:
151-
self.fail("Main thread stack trace not found in result")
163+
self.assertTrue(main_thread_found, "Main thread stack trace not found in result")
152164

153165
@skip_if_not_supported
154166
@unittest.skipIf(
@@ -1086,13 +1098,17 @@ def test_self_trace(self):
10861098
# Is possible that there are more threads, so we check that the
10871099
# expected stack traces are in the result (looking at you Windows!)
10881100
this_tread_stack = None
1089-
for thread_id, stack in stack_trace:
1090-
if thread_id == threading.get_native_id():
1091-
this_tread_stack = stack
1101+
# New format: [InterpreterInfo(interpreter_id, [ThreadInfo(...)])]
1102+
for interpreter_info in stack_trace:
1103+
for thread_info in interpreter_info.threads:
1104+
if thread_info.thread_id == threading.get_native_id():
1105+
this_tread_stack = thread_info.frame_info
1106+
break
1107+
if this_tread_stack:
10921108
break
10931109
self.assertIsNotNone(this_tread_stack)
10941110
self.assertEqual(
1095-
stack[:2],
1111+
this_tread_stack[:2],
10961112
[
10971113
FrameInfo(
10981114
[
@@ -1203,15 +1219,20 @@ def main_work():
12031219
# Wait for the main thread to start its busy work
12041220
all_traces = unwinder_all.get_stack_trace()
12051221
found = False
1206-
for thread_id, stack in all_traces:
1207-
if not stack:
1208-
continue
1209-
current_frame = stack[0]
1210-
if (
1211-
current_frame.funcname == "main_work"
1212-
and current_frame.lineno > 15
1213-
):
1214-
found = True
1222+
# New format: [InterpreterInfo(interpreter_id, [ThreadInfo(...)])]
1223+
for interpreter_info in all_traces:
1224+
for thread_info in interpreter_info.threads:
1225+
if not thread_info.frame_info:
1226+
continue
1227+
current_frame = thread_info.frame_info[0]
1228+
if (
1229+
current_frame.funcname == "main_work"
1230+
and current_frame.lineno > 15
1231+
):
1232+
found = True
1233+
break
1234+
if found:
1235+
break
12151236

12161237
if found:
12171238
break
@@ -1237,19 +1258,31 @@ def main_work():
12371258
p.terminate()
12381259
p.wait(timeout=SHORT_TIMEOUT)
12391260

1240-
# Verify we got multiple threads in all_traces
1261+
# Count total threads across all interpreters in all_traces
1262+
total_threads = sum(len(interpreter_info.threads) for interpreter_info in all_traces)
12411263
self.assertGreater(
1242-
len(all_traces), 1, "Should have multiple threads"
1264+
total_threads, 1, "Should have multiple threads"
12431265
)
12441266

1245-
# Verify we got exactly one thread in gil_traces
1267+
# Count total threads across all interpreters in gil_traces
1268+
total_gil_threads = sum(len(interpreter_info.threads) for interpreter_info in gil_traces)
12461269
self.assertEqual(
1247-
len(gil_traces), 1, "Should have exactly one GIL holder"
1270+
total_gil_threads, 1, "Should have exactly one GIL holder"
12481271
)
12491272

1250-
# The GIL holder should be in the all_traces list
1251-
gil_thread_id = gil_traces[0][0]
1252-
all_thread_ids = [trace[0] for trace in all_traces]
1273+
# Get the GIL holder thread ID
1274+
gil_thread_id = None
1275+
for interpreter_info in gil_traces:
1276+
if interpreter_info.threads:
1277+
gil_thread_id = interpreter_info.threads[0].thread_id
1278+
break
1279+
1280+
# Get all thread IDs from all_traces
1281+
all_thread_ids = []
1282+
for interpreter_info in all_traces:
1283+
for thread_info in interpreter_info.threads:
1284+
all_thread_ids.append(thread_info.thread_id)
1285+
12531286
self.assertIn(
12541287
gil_thread_id,
12551288
all_thread_ids,

0 commit comments

Comments
 (0)