https://github.com/python/cpython/commit/87ac0bc66a298b040c4b4c6c2eb83628bf10abf9
commit: 87ac0bc66a298b040c4b4c6c2eb83628bf10abf9
branch: main
author: László Kiss Kollár <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-06-27T17:29:50Z
summary:
gh-152434: Fix async-aware Gecko collection (#152442)
files:
A Misc/NEWS.d/next/Library/2026-06-25-20-00-00.gh-issue-151496.kMnT3x.rst
M Lib/profiling/sampling/gecko_collector.py
M Lib/test/test_profiling/test_sampling_profiler/test_collectors.py
diff --git a/Lib/profiling/sampling/gecko_collector.py
b/Lib/profiling/sampling/gecko_collector.py
index 2bb5bd2f664d59..2de8cce387e7f2 100644
--- a/Lib/profiling/sampling/gecko_collector.py
+++ b/Lib/profiling/sampling/gecko_collector.py
@@ -250,6 +250,25 @@ def collect(self, stack_frames, timestamps_us=None):
self.interval = (times[-1] - self.last_sample_time) /
self.sample_count
self.last_sample_time = times[-1]
+ # Process async tasks
+ if stack_frames and hasattr(stack_frames[0], "awaited_by"):
+ for frames, thread_id, _ in self._iter_async_frames(stack_frames):
+ frames = filter_internal_frames(frames)
+ if not frames:
+ continue
+
+ if thread_id not in self.threads:
+ self.threads[thread_id] = self._create_thread(
+ thread_id, False
+ )
+
+ self._record_stack_sample(
+ self.threads[thread_id], frames, thread_id, times,
first_time
+ )
+
+ self.sample_count += len(times)
+ return
+
# Process threads
for interpreter_info in stack_frames:
for thread_info in interpreter_info.threads:
@@ -333,37 +352,43 @@ def collect(self, stack_frames, timestamps_us=None):
if not frames:
continue
- # Process stack once to get stack_index
- stack_index = self._process_stack(thread_data, frames)
-
- # Add samples with timestamps
- thread_spill = thread_data["_spill"]
- for t in times:
- thread_spill.append_sample(stack_index, t)
-
- # Handle opcodes
- if self.opcodes_enabled and frames:
- leaf_frame = frames[0]
- filename, location, funcname, opcode = leaf_frame
- if isinstance(location, tuple):
- lineno, _, col_offset, _ = location
- else:
- lineno = location
- col_offset = -1
-
- current_state = (opcode, lineno, col_offset, funcname,
filename)
-
- if tid not in self.opcode_state:
- self.opcode_state[tid] = (*current_state, first_time)
- elif self.opcode_state[tid][:5] != current_state:
- prev_opcode, prev_lineno, prev_col, prev_funcname,
prev_filename, prev_start = self.opcode_state[tid]
- self._add_opcode_interval_marker(
- tid, prev_opcode, prev_lineno, prev_col,
prev_funcname, prev_start, first_time
- )
- self.opcode_state[tid] = (*current_state, first_time)
+ self._record_stack_sample(
+ thread_data, frames, tid, times, first_time
+ )
self.sample_count += len(times)
+ def _record_stack_sample(self, thread_data, frames, tid, times,
first_time):
+ stack_index = self._process_stack(thread_data, frames)
+
+ thread_spill = thread_data["_spill"]
+ for t in times:
+ thread_spill.append_sample(stack_index, t)
+
+ if self.opcodes_enabled and frames:
+ leaf_frame = frames[0]
+ filename, location, funcname, opcode = leaf_frame
+ if isinstance(location, tuple):
+ lineno, _, col_offset, _ = location
+ else:
+ lineno = location
+ col_offset = -1
+
+ current_state = (opcode, lineno, col_offset, funcname, filename)
+
+ if tid not in self.opcode_state:
+ self.opcode_state[tid] = (*current_state, first_time)
+ elif self.opcode_state[tid][:5] != current_state:
+ (
+ prev_opcode, prev_lineno, prev_col, prev_funcname,
+ prev_filename, prev_start
+ ) = self.opcode_state[tid]
+ self._add_opcode_interval_marker(
+ tid, prev_opcode, prev_lineno, prev_col, prev_funcname,
+ prev_start, first_time
+ )
+ self.opcode_state[tid] = (*current_state, first_time)
+
def _create_thread(self, tid, is_main_thread):
"""Create a new thread structure with processed profile format."""
if self.spill_dir is None:
diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py
b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py
index 56f3fe5e1c2605..d440c44385e671 100644
--- a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py
+++ b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py
@@ -40,7 +40,16 @@
from test.support import captured_stdout, captured_stderr
-from .mocks import MockFrameInfo, MockThreadInfo, MockInterpreterInfo,
LocationInfo, make_diff_collector_with_mock_baseline
+from .mocks import (
+ MockAwaitedInfo,
+ MockCoroInfo,
+ MockFrameInfo,
+ MockInterpreterInfo,
+ MockTaskInfo,
+ MockThreadInfo,
+ LocationInfo,
+ make_diff_collector_with_mock_baseline,
+)
from .helpers import close_and_unlink, jsonl_tables
@@ -673,6 +682,48 @@ def test_gecko_collector_basic(self):
self.assertGreater(stack_table["length"], 0)
self.assertGreater(len(stack_table["frame"]), 0)
+ def test_gecko_collector_async_aware(self):
+ collector = GeckoCollector(1000)
+
+ parent = MockTaskInfo(
+ task_id=1,
+ task_name="Parent",
+ coroutine_stack=[
+ MockCoroInfo(
+ task_name="Parent",
+ call_stack=[MockFrameInfo("parent.py", 10, "parent_fn")],
+ )
+ ],
+ )
+ child = MockTaskInfo(
+ task_id=2,
+ task_name="Child",
+ coroutine_stack=[
+ MockCoroInfo(
+ task_name="Child",
+ call_stack=[MockFrameInfo("child.py", 20, "child_fn")],
+ )
+ ],
+ awaited_by=[MockCoroInfo(task_name=1, call_stack=[])],
+ )
+
+ collector.collect(
+ [MockAwaitedInfo(thread_id=100, awaited_by=[parent, child])],
+ timestamps_us=[1000, 2000],
+ )
+ profile_data = export_gecko_profile(self, collector)
+
+ self.assertEqual(len(profile_data["threads"]), 1)
+ thread_data = profile_data["threads"][0]
+ self.assertEqual(thread_data["samples"]["length"], 2)
+
+ string_array = profile_data["shared"]["stringArray"]
+ self.assertIn("parent_fn", string_array)
+ self.assertIn("child_fn", string_array)
+ self.assertIn("Parent", string_array)
+ self.assertIn("Child", string_array)
+ self.assertEqual(thread_data["markers"]["length"], 0)
+
@unittest.skipIf(is_emscripten, "threads not available")
def test_gecko_collector_export(self):
"""Test Gecko profile export functionality."""
diff --git
a/Misc/NEWS.d/next/Library/2026-06-25-20-00-00.gh-issue-151496.kMnT3x.rst
b/Misc/NEWS.d/next/Library/2026-06-25-20-00-00.gh-issue-151496.kMnT3x.rst
new file mode 100644
index 00000000000000..994e5dadcf2e8a
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-06-25-20-00-00.gh-issue-151496.kMnT3x.rst
@@ -0,0 +1,3 @@
+Fixed ``profiling.sampling --gecko`` with ``--async-aware`` by flattening
+async task stacks before generating Gecko samples. ``--binary`` now rejects
+``--async-aware`` until the binary format supports async task data.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]