From 4a82ac9d678fccb486b113e2a25462e13a5996bf Mon Sep 17 00:00:00 2001 From: Sofia Donato Ferreira Date: Mon, 16 Mar 2026 22:49:03 -0300 Subject: [PATCH 1/6] profiling(Gecko): Properly obtain main thread identifier Since running a profiler via CLI (python -m profiling.sampling run) spawns a new subprocess where the actual user-specified code will run, a call to threading.main_thread() in the collector's process will not return the profiled process's main thread. To combat this, we rely on the fact that thread objects are inserted in such a way that the first object in the list represents the oldest ThreadState object [1], which corresponds to a ThreadState associated with the main thread. [1] - https://github.com/python/cpython/blob/1b118353bb0a9d816de6ef673f3b11775de5bec5/Include/internal/pycore_interp_structs.h#L831 Signed-off-by: Sofia Donato Ferreira --- Lib/profiling/sampling/gecko_collector.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Lib/profiling/sampling/gecko_collector.py b/Lib/profiling/sampling/gecko_collector.py index 28ef9b69bf7968..752b57571f6ec8 100644 --- a/Lib/profiling/sampling/gecko_collector.py +++ b/Lib/profiling/sampling/gecko_collector.py @@ -170,6 +170,7 @@ def collect(self, stack_frames, timestamps_us=None): self.last_sample_time = times[-1] # Process threads + main_tid = None for interpreter_info in stack_frames: for thread_info in interpreter_info.threads: frames = filter_internal_frames(thread_info.frame_info) @@ -177,7 +178,11 @@ def collect(self, stack_frames, timestamps_us=None): # Initialize thread if needed if tid not in self.threads: - self.threads[tid] = self._create_thread(tid) + # Since 'threads' is in order from oldest to newest, + # we know the first thread must be the main thread. + if len(self.threads) == 0: + main_tid = tid + self.threads[tid] = self._create_thread(tid, main_tid) thread_data = self.threads[tid] @@ -288,14 +293,10 @@ def collect(self, stack_frames, timestamps_us=None): self.sample_count += len(times) - def _create_thread(self, tid): + def _create_thread(self, tid, main_tid): """Create a new thread structure with processed profile format.""" - # Determine if this is the main thread - try: - is_main = tid == threading.main_thread().ident - except (RuntimeError, AttributeError): - is_main = False + is_main = tid == main_tid thread = { "name": f"Thread-{tid}", From 8920f2f062bf3c93090c5ed2d1fab079baa6645f Mon Sep 17 00:00:00 2001 From: Sofia Donato Ferreira Date: Tue, 17 Mar 2026 15:41:52 -0300 Subject: [PATCH 2/6] profiling(gecko): take last thread as main thread instead of first one The ordering is actually newest -> oldest, since the _remote_debugging code traverses the ThreadState linked list in the intepreter state, appending to a list of threads in-order. Signed-off-by: Sofia Donato Ferreira --- Lib/profiling/sampling/gecko_collector.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Lib/profiling/sampling/gecko_collector.py b/Lib/profiling/sampling/gecko_collector.py index 752b57571f6ec8..3e39715dce2dec 100644 --- a/Lib/profiling/sampling/gecko_collector.py +++ b/Lib/profiling/sampling/gecko_collector.py @@ -170,18 +170,16 @@ def collect(self, stack_frames, timestamps_us=None): self.last_sample_time = times[-1] # Process threads - main_tid = None for interpreter_info in stack_frames: + # Since 'threads' is in order from newest to oldest, + # we know the first thread must be the main thread. + main_tid = interpreter_info.threads[-1].thread_id for thread_info in interpreter_info.threads: frames = filter_internal_frames(thread_info.frame_info) tid = thread_info.thread_id # Initialize thread if needed if tid not in self.threads: - # Since 'threads' is in order from oldest to newest, - # we know the first thread must be the main thread. - if len(self.threads) == 0: - main_tid = tid self.threads[tid] = self._create_thread(tid, main_tid) thread_data = self.threads[tid] From d9b0a79ba7a170cae4404ad8ddea0a32c102fae4 Mon Sep 17 00:00:00 2001 From: Sofia Donato Ferreira Date: Tue, 17 Mar 2026 16:50:08 -0300 Subject: [PATCH 3/6] profiling(Gecko): check if thread list is not empty before accessing it It seems to sometimes happen that some samples (possibly at the very start / end) do not have any active threads in the interpreter state. It is OK to make main_tid maybe None here, since if it is, the threads list is empty, and so the for loop will not execute anyways. Signed-off-by: Sofia Donato Ferreira --- Lib/profiling/sampling/gecko_collector.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/profiling/sampling/gecko_collector.py b/Lib/profiling/sampling/gecko_collector.py index 3e39715dce2dec..7b42253e50f636 100644 --- a/Lib/profiling/sampling/gecko_collector.py +++ b/Lib/profiling/sampling/gecko_collector.py @@ -173,7 +173,9 @@ def collect(self, stack_frames, timestamps_us=None): for interpreter_info in stack_frames: # Since 'threads' is in order from newest to oldest, # we know the first thread must be the main thread. - main_tid = interpreter_info.threads[-1].thread_id + main_tid = None + if len(interpreter_info.threads) != 0: + main_tid = interpreter_info.threads[-1].thread_id for thread_info in interpreter_info.threads: frames = filter_internal_frames(thread_info.frame_info) tid = thread_info.thread_id From f186d3ffedcb6806ebaa4a791c09376d53cd0786 Mon Sep 17 00:00:00 2001 From: Sofia Donato Ferreira Date: Sat, 21 Mar 2026 16:57:19 -0300 Subject: [PATCH 4/6] profiling(gecko): retrieve main thread id directly from interpreter state Signed-off-by: Sofia Donato Ferreira --- Lib/profiling/sampling/gecko_collector.py | 6 +----- .../test_binary_format.py | 4 ++-- Modules/_remote_debugging/_remote_debugging.h | 3 ++- Modules/_remote_debugging/binary_io_reader.c | 2 +- Modules/_remote_debugging/binary_io_writer.c | 2 +- Modules/_remote_debugging/module.c | 20 ++++++++++++++++--- Modules/_remote_debugging/threads.c | 4 +++- 7 files changed, 27 insertions(+), 14 deletions(-) diff --git a/Lib/profiling/sampling/gecko_collector.py b/Lib/profiling/sampling/gecko_collector.py index 7b42253e50f636..7a0f5f356f8d11 100644 --- a/Lib/profiling/sampling/gecko_collector.py +++ b/Lib/profiling/sampling/gecko_collector.py @@ -171,11 +171,7 @@ def collect(self, stack_frames, timestamps_us=None): # Process threads for interpreter_info in stack_frames: - # Since 'threads' is in order from newest to oldest, - # we know the first thread must be the main thread. - main_tid = None - if len(interpreter_info.threads) != 0: - main_tid = interpreter_info.threads[-1].thread_id + main_tid = interpreter_info.main_thread_id for thread_info in interpreter_info.threads: frames = filter_internal_frames(thread_info.frame_info) tid = thread_info.thread_id diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py b/Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py index 033a533fe5444e..df632386786499 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py @@ -47,9 +47,9 @@ def make_thread(thread_id, frames, status=0): return ThreadInfo((thread_id, status, frames)) -def make_interpreter(interp_id, threads): +def make_interpreter(interp_id, threads, main_thread_id=0): """Create an InterpreterInfo struct sequence.""" - return InterpreterInfo((interp_id, threads)) + return InterpreterInfo((interp_id, main_thread_id, threads)) def extract_lineno(location): diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index 7bcb2f483234ec..ab431ee42ff675 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -575,7 +575,8 @@ extern PyObject* unwind_stack_for_thread( RemoteUnwinderObject *unwinder, uintptr_t *current_tstate, uintptr_t gil_holder_tstate, - uintptr_t gc_frame + uintptr_t gc_frame, + uint64_t *current_thread_id ); /* Thread stopping functions (for blocking mode) */ diff --git a/Modules/_remote_debugging/binary_io_reader.c b/Modules/_remote_debugging/binary_io_reader.c index cb58a0ed199d4a..4a04f5f86a54f2 100644 --- a/Modules/_remote_debugging/binary_io_reader.c +++ b/Modules/_remote_debugging/binary_io_reader.c @@ -828,7 +828,7 @@ build_sample_list(RemoteDebuggingState *state, BinaryReader *reader, goto error; } PyStructSequence_SetItem(interp_info, 0, iid); - PyStructSequence_SetItem(interp_info, 1, thread_list); + PyStructSequence_SetItem(interp_info, 2, thread_list); thread_list = NULL; sample_list = PyList_New(1); diff --git a/Modules/_remote_debugging/binary_io_writer.c b/Modules/_remote_debugging/binary_io_writer.c index c129c93efe23c5..93074db0932655 100644 --- a/Modules/_remote_debugging/binary_io_writer.c +++ b/Modules/_remote_debugging/binary_io_writer.c @@ -1008,7 +1008,7 @@ binary_writer_write_sample(BinaryWriter *writer, PyObject *stack_frames, uint64_ PyObject *interp_info = PyList_GET_ITEM(stack_frames, i); PyObject *interp_id_obj = PyStructSequence_GET_ITEM(interp_info, 0); - PyObject *threads = PyStructSequence_GET_ITEM(interp_info, 1); + PyObject *threads = PyStructSequence_GET_ITEM(interp_info, 2); unsigned long interp_id_long = PyLong_AsUnsignedLong(interp_id_obj); if (interp_id_long == (unsigned long)-1 && PyErr_Occurred()) { diff --git a/Modules/_remote_debugging/module.c b/Modules/_remote_debugging/module.c index 040bd3db377315..429a5193de66d9 100644 --- a/Modules/_remote_debugging/module.c +++ b/Modules/_remote_debugging/module.c @@ -106,6 +106,7 @@ PyStructSequence_Desc ThreadInfo_desc = { // InterpreterInfo structseq type static PyStructSequence_Field InterpreterInfo_fields[] = { {"interpreter_id", "Interpreter ID"}, + {"main_thread_id", "Main thread ID"}, {"threads", "List of threads in this interpreter"}, {NULL} }; @@ -114,7 +115,7 @@ PyStructSequence_Desc InterpreterInfo_desc = { "_remote_debugging.InterpreterInfo", "Information about an interpreter", InterpreterInfo_fields, - 2 + 3 }; // AwaitedInfo structseq type @@ -583,11 +584,19 @@ _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self current_tstate = self->tstate_addr; } + // Acquire main thread state information + uintptr_t main_thread_tstate = GET_MEMBER(uintptr_t, interp_state_buffer, + self->debug_offsets.interpreter_state.threads_main); + + PyObject *main_thread_id = NULL; + + uint64_t prev_thread_id = 0; while (current_tstate != 0) { uintptr_t prev_tstate = current_tstate; PyObject* frame_info = unwind_stack_for_thread(self, ¤t_tstate, gil_holder_tstate, - gc_frame); + gc_frame, + &prev_thread_id); if (!frame_info) { // Check if this was an intentional skip due to mode-based filtering if ((self->mode == PROFILING_MODE_CPU || self->mode == PROFILING_MODE_GIL || @@ -613,6 +622,10 @@ _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self goto exit; } + if (prev_tstate == main_thread_tstate) { + main_thread_id = PyLong_FromUnsignedLongLong(prev_thread_id); + } + if (PyList_Append(interpreter_threads, frame_info) == -1) { Py_DECREF(frame_info); Py_DECREF(interpreter_threads); @@ -648,7 +661,8 @@ _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self } PyStructSequence_SetItem(interpreter_info, 0, interp_id); // steals reference - PyStructSequence_SetItem(interpreter_info, 1, interpreter_threads); // steals reference + PyStructSequence_SetItem(interpreter_info, 1, main_thread_id); // steals reference + PyStructSequence_SetItem(interpreter_info, 2, interpreter_threads); // steals reference // Add this interpreter to the result list if (PyList_Append(result, interpreter_info) == -1) { diff --git a/Modules/_remote_debugging/threads.c b/Modules/_remote_debugging/threads.c index 3100b83c8f4899..2a46ccd0b3ddb9 100644 --- a/Modules/_remote_debugging/threads.c +++ b/Modules/_remote_debugging/threads.c @@ -291,7 +291,8 @@ unwind_stack_for_thread( RemoteUnwinderObject *unwinder, uintptr_t *current_tstate, uintptr_t gil_holder_tstate, - uintptr_t gc_frame + uintptr_t gc_frame, + uint64_t *current_tid ) { PyObject *frame_info = NULL; PyObject *thread_id = NULL; @@ -309,6 +310,7 @@ unwind_stack_for_thread( STATS_ADD(unwinder, memory_bytes_read, unwinder->debug_offsets.thread_state.size); long tid = GET_MEMBER(long, ts, unwinder->debug_offsets.thread_state.native_thread_id); + *current_tid = tid; // Read GC collecting state from the interpreter (before any skip checks) uintptr_t interp_addr = GET_MEMBER(uintptr_t, ts, unwinder->debug_offsets.thread_state.interp); From 5849aad7269517895444a0b4215c0319cab56411 Mon Sep 17 00:00:00 2001 From: Sofia Donato Ferreira Date: Sat, 21 Mar 2026 17:59:55 -0300 Subject: [PATCH 5/6] profiling(gecko): add main_thread_id to MockInterpreter and add check in Gecko test Signed-off-by: Sofia Donato Ferreira --- Lib/test/test_profiling/test_sampling_profiler/mocks.py | 5 +++-- .../test_profiling/test_sampling_profiler/test_collectors.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_profiling/test_sampling_profiler/mocks.py b/Lib/test/test_profiling/test_sampling_profiler/mocks.py index 4e0f7a87c6da54..15f6097991e6a9 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/mocks.py +++ b/Lib/test/test_profiling/test_sampling_profiler/mocks.py @@ -50,12 +50,13 @@ def __repr__(self): class MockInterpreterInfo: """Mock InterpreterInfo for testing since the real one isn't accessible.""" - def __init__(self, interpreter_id, threads): + def __init__(self, interpreter_id, threads, main_thread_id=None): self.interpreter_id = interpreter_id + self.main_thread_id = main_thread_id self.threads = threads def __repr__(self): - return f"MockInterpreterInfo(interpreter_id={self.interpreter_id}, threads={self.threads})" + return f"MockInterpreterInfo(interpreter_id={self.interpreter_id}, main_thread_id={self.main_thread_id}, threads={self.threads})" class MockCoroInfo: 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 8e6afa91e89daf..64f6b800722cfe 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py @@ -526,6 +526,7 @@ def test_gecko_collector_basic(self): [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")], ) ], + main_thread_id=1, ) ] collector.collect(test_frames) @@ -556,6 +557,7 @@ def test_gecko_collector_basic(self): threads = profile_data["threads"] self.assertEqual(len(threads), 1) thread_data = threads[0] + self.assertTrue(thread_data["isMainThread"]) # Verify thread structure self.assertIn("samples", thread_data) From ad5b78c2bd878c43f31e547f20660a2c7c8335e4 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Sun, 22 Mar 2026 02:02:42 +0000 Subject: [PATCH 6/6] profiling(gecko): use status flag for main thread instead of InterpreterInfo field Replace the main_thread_id field in InterpreterInfo with a THREAD_STATUS_MAIN_THREAD status flag set directly in the thread unwinding code. This simplifies the struct from 3 to 2 fields and removes the need to compare thread IDs at the Python level. --- Lib/profiling/sampling/constants.py | 2 ++ Lib/profiling/sampling/gecko_collector.py | 15 ++++---- .../test_sampling_profiler/mocks.py | 5 ++- .../test_binary_format.py | 36 +++++++++++++++++-- .../test_sampling_profiler/test_collectors.py | 3 +- Modules/_remote_debugging/_remote_debugging.h | 3 +- Modules/_remote_debugging/binary_io_reader.c | 2 +- Modules/_remote_debugging/binary_io_writer.c | 2 +- Modules/_remote_debugging/module.c | 18 ++++------ Modules/_remote_debugging/threads.c | 7 ++-- 10 files changed, 62 insertions(+), 31 deletions(-) diff --git a/Lib/profiling/sampling/constants.py b/Lib/profiling/sampling/constants.py index 58a57700fbdd4a..a364d0b8fde1e0 100644 --- a/Lib/profiling/sampling/constants.py +++ b/Lib/profiling/sampling/constants.py @@ -37,6 +37,7 @@ THREAD_STATUS_UNKNOWN, THREAD_STATUS_GIL_REQUESTED, THREAD_STATUS_HAS_EXCEPTION, + THREAD_STATUS_MAIN_THREAD, ) except ImportError: # Fallback for tests or when module is not available @@ -45,3 +46,4 @@ THREAD_STATUS_UNKNOWN = (1 << 2) THREAD_STATUS_GIL_REQUESTED = (1 << 3) THREAD_STATUS_HAS_EXCEPTION = (1 << 4) + THREAD_STATUS_MAIN_THREAD = (1 << 5) diff --git a/Lib/profiling/sampling/gecko_collector.py b/Lib/profiling/sampling/gecko_collector.py index 7a0f5f356f8d11..8986194268b3ce 100644 --- a/Lib/profiling/sampling/gecko_collector.py +++ b/Lib/profiling/sampling/gecko_collector.py @@ -9,7 +9,7 @@ from .collector import Collector, filter_internal_frames from .opcode_utils import get_opcode_info, format_opcode try: - from _remote_debugging import THREAD_STATUS_HAS_GIL, THREAD_STATUS_ON_CPU, THREAD_STATUS_UNKNOWN, THREAD_STATUS_GIL_REQUESTED, THREAD_STATUS_HAS_EXCEPTION + from _remote_debugging import THREAD_STATUS_HAS_GIL, THREAD_STATUS_ON_CPU, THREAD_STATUS_UNKNOWN, THREAD_STATUS_GIL_REQUESTED, THREAD_STATUS_HAS_EXCEPTION, THREAD_STATUS_MAIN_THREAD except ImportError: # Fallback if module not available (shouldn't happen in normal use) THREAD_STATUS_HAS_GIL = (1 << 0) @@ -17,6 +17,7 @@ THREAD_STATUS_UNKNOWN = (1 << 2) THREAD_STATUS_GIL_REQUESTED = (1 << 3) THREAD_STATUS_HAS_EXCEPTION = (1 << 4) + THREAD_STATUS_MAIN_THREAD = (1 << 5) # Categories matching Firefox Profiler expectations @@ -171,19 +172,19 @@ def collect(self, stack_frames, timestamps_us=None): # Process threads for interpreter_info in stack_frames: - main_tid = interpreter_info.main_thread_id for thread_info in interpreter_info.threads: frames = filter_internal_frames(thread_info.frame_info) tid = thread_info.thread_id + status_flags = thread_info.status + is_main_thread = bool(status_flags & THREAD_STATUS_MAIN_THREAD) # Initialize thread if needed if tid not in self.threads: - self.threads[tid] = self._create_thread(tid, main_tid) + self.threads[tid] = self._create_thread(tid, is_main_thread) thread_data = self.threads[tid] # Decode status flags - status_flags = thread_info.status has_gil = bool(status_flags & THREAD_STATUS_HAS_GIL) on_cpu = bool(status_flags & THREAD_STATUS_ON_CPU) gil_requested = bool(status_flags & THREAD_STATUS_GIL_REQUESTED) @@ -289,14 +290,12 @@ def collect(self, stack_frames, timestamps_us=None): self.sample_count += len(times) - def _create_thread(self, tid, main_tid): + def _create_thread(self, tid, is_main_thread): """Create a new thread structure with processed profile format.""" - is_main = tid == main_tid - thread = { "name": f"Thread-{tid}", - "isMainThread": is_main, + "isMainThread": is_main_thread, "processStartupTime": 0, "processShutdownTime": None, "registerTime": 0, diff --git a/Lib/test/test_profiling/test_sampling_profiler/mocks.py b/Lib/test/test_profiling/test_sampling_profiler/mocks.py index 15f6097991e6a9..4e0f7a87c6da54 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/mocks.py +++ b/Lib/test/test_profiling/test_sampling_profiler/mocks.py @@ -50,13 +50,12 @@ def __repr__(self): class MockInterpreterInfo: """Mock InterpreterInfo for testing since the real one isn't accessible.""" - def __init__(self, interpreter_id, threads, main_thread_id=None): + def __init__(self, interpreter_id, threads): self.interpreter_id = interpreter_id - self.main_thread_id = main_thread_id self.threads = threads def __repr__(self): - return f"MockInterpreterInfo(interpreter_id={self.interpreter_id}, main_thread_id={self.main_thread_id}, threads={self.threads})" + return f"MockInterpreterInfo(interpreter_id={self.interpreter_id}, threads={self.threads})" class MockCoroInfo: diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py b/Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py index df632386786499..29f83c843561cd 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py @@ -18,9 +18,11 @@ THREAD_STATUS_UNKNOWN, THREAD_STATUS_GIL_REQUESTED, THREAD_STATUS_HAS_EXCEPTION, + THREAD_STATUS_MAIN_THREAD, ) from profiling.sampling.binary_collector import BinaryCollector from profiling.sampling.binary_reader import BinaryReader + from profiling.sampling.gecko_collector import GeckoCollector ZSTD_AVAILABLE = _remote_debugging.zstd_available() except ImportError: @@ -47,9 +49,9 @@ def make_thread(thread_id, frames, status=0): return ThreadInfo((thread_id, status, frames)) -def make_interpreter(interp_id, threads, main_thread_id=0): +def make_interpreter(interp_id, threads): """Create an InterpreterInfo struct sequence.""" - return InterpreterInfo((interp_id, main_thread_id, threads)) + return InterpreterInfo((interp_id, threads)) def extract_lineno(location): @@ -318,6 +320,7 @@ def test_status_flags_preserved(self): THREAD_STATUS_UNKNOWN, THREAD_STATUS_GIL_REQUESTED, THREAD_STATUS_HAS_EXCEPTION, + THREAD_STATUS_MAIN_THREAD, THREAD_STATUS_HAS_GIL | THREAD_STATUS_ON_CPU, THREAD_STATUS_HAS_GIL | THREAD_STATUS_HAS_EXCEPTION, THREAD_STATUS_HAS_GIL @@ -342,6 +345,35 @@ def test_status_flags_preserved(self): self.assertEqual(count, len(statuses)) self.assert_samples_equal(samples, collector) + def test_binary_replay_preserves_main_thread_for_gecko(self): + """Binary replay preserves main thread identity for GeckoCollector.""" + samples = [ + [ + make_interpreter( + 0, + [ + make_thread( + 1, + [make_frame("main.py", 10, "main")], + THREAD_STATUS_MAIN_THREAD, + ), + make_thread(2, [make_frame("worker.py", 20, "worker")]), + ], + ) + ] + ] + filename = self.create_binary_file(samples) + collector = GeckoCollector(1000) + + with BinaryReader(filename) as reader: + count = reader.replay_samples(collector) + + self.assertEqual(count, 2) + profile = collector._build_profile() + threads = {thread["tid"]: thread for thread in profile["threads"]} + self.assertTrue(threads[1]["isMainThread"]) + self.assertFalse(threads[2]["isMainThread"]) + def test_multiple_threads_per_sample(self): """Multiple threads in one sample roundtrip exactly.""" threads = [ 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 64f6b800722cfe..06c9e51e0c9c55 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py @@ -28,6 +28,7 @@ THREAD_STATUS_HAS_GIL, THREAD_STATUS_ON_CPU, THREAD_STATUS_GIL_REQUESTED, + THREAD_STATUS_MAIN_THREAD, ) except ImportError: raise unittest.SkipTest( @@ -524,9 +525,9 @@ def test_gecko_collector_basic(self): MockThreadInfo( 1, [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")], + status=THREAD_STATUS_MAIN_THREAD, ) ], - main_thread_id=1, ) ] collector.collect(test_frames) diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index ab431ee42ff675..570f6b23b75849 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -172,6 +172,7 @@ typedef enum _WIN32_THREADSTATE { #define THREAD_STATUS_UNKNOWN (1 << 2) #define THREAD_STATUS_GIL_REQUESTED (1 << 3) #define THREAD_STATUS_HAS_EXCEPTION (1 << 4) +#define THREAD_STATUS_MAIN_THREAD (1 << 5) /* Exception cause macro */ #define set_exception_cause(unwinder, exc_type, message) \ @@ -576,7 +577,7 @@ extern PyObject* unwind_stack_for_thread( uintptr_t *current_tstate, uintptr_t gil_holder_tstate, uintptr_t gc_frame, - uint64_t *current_thread_id + uintptr_t main_thread_tstate ); /* Thread stopping functions (for blocking mode) */ diff --git a/Modules/_remote_debugging/binary_io_reader.c b/Modules/_remote_debugging/binary_io_reader.c index 4a04f5f86a54f2..cb58a0ed199d4a 100644 --- a/Modules/_remote_debugging/binary_io_reader.c +++ b/Modules/_remote_debugging/binary_io_reader.c @@ -828,7 +828,7 @@ build_sample_list(RemoteDebuggingState *state, BinaryReader *reader, goto error; } PyStructSequence_SetItem(interp_info, 0, iid); - PyStructSequence_SetItem(interp_info, 2, thread_list); + PyStructSequence_SetItem(interp_info, 1, thread_list); thread_list = NULL; sample_list = PyList_New(1); diff --git a/Modules/_remote_debugging/binary_io_writer.c b/Modules/_remote_debugging/binary_io_writer.c index 93074db0932655..c129c93efe23c5 100644 --- a/Modules/_remote_debugging/binary_io_writer.c +++ b/Modules/_remote_debugging/binary_io_writer.c @@ -1008,7 +1008,7 @@ binary_writer_write_sample(BinaryWriter *writer, PyObject *stack_frames, uint64_ PyObject *interp_info = PyList_GET_ITEM(stack_frames, i); PyObject *interp_id_obj = PyStructSequence_GET_ITEM(interp_info, 0); - PyObject *threads = PyStructSequence_GET_ITEM(interp_info, 2); + PyObject *threads = PyStructSequence_GET_ITEM(interp_info, 1); unsigned long interp_id_long = PyLong_AsUnsignedLong(interp_id_obj); if (interp_id_long == (unsigned long)-1 && PyErr_Occurred()) { diff --git a/Modules/_remote_debugging/module.c b/Modules/_remote_debugging/module.c index 429a5193de66d9..4f294b80ba0739 100644 --- a/Modules/_remote_debugging/module.c +++ b/Modules/_remote_debugging/module.c @@ -106,7 +106,6 @@ PyStructSequence_Desc ThreadInfo_desc = { // InterpreterInfo structseq type static PyStructSequence_Field InterpreterInfo_fields[] = { {"interpreter_id", "Interpreter ID"}, - {"main_thread_id", "Main thread ID"}, {"threads", "List of threads in this interpreter"}, {NULL} }; @@ -115,7 +114,7 @@ PyStructSequence_Desc InterpreterInfo_desc = { "_remote_debugging.InterpreterInfo", "Information about an interpreter", InterpreterInfo_fields, - 3 + 2 }; // AwaitedInfo structseq type @@ -588,15 +587,12 @@ _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self uintptr_t main_thread_tstate = GET_MEMBER(uintptr_t, interp_state_buffer, self->debug_offsets.interpreter_state.threads_main); - PyObject *main_thread_id = NULL; - - uint64_t prev_thread_id = 0; while (current_tstate != 0) { uintptr_t prev_tstate = current_tstate; PyObject* frame_info = unwind_stack_for_thread(self, ¤t_tstate, gil_holder_tstate, gc_frame, - &prev_thread_id); + main_thread_tstate); if (!frame_info) { // Check if this was an intentional skip due to mode-based filtering if ((self->mode == PROFILING_MODE_CPU || self->mode == PROFILING_MODE_GIL || @@ -622,10 +618,6 @@ _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self goto exit; } - if (prev_tstate == main_thread_tstate) { - main_thread_id = PyLong_FromUnsignedLongLong(prev_thread_id); - } - if (PyList_Append(interpreter_threads, frame_info) == -1) { Py_DECREF(frame_info); Py_DECREF(interpreter_threads); @@ -661,8 +653,7 @@ _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self } PyStructSequence_SetItem(interpreter_info, 0, interp_id); // steals reference - PyStructSequence_SetItem(interpreter_info, 1, main_thread_id); // steals reference - PyStructSequence_SetItem(interpreter_info, 2, interpreter_threads); // steals reference + PyStructSequence_SetItem(interpreter_info, 1, interpreter_threads); // steals reference // Add this interpreter to the result list if (PyList_Append(result, interpreter_info) == -1) { @@ -1221,6 +1212,9 @@ _remote_debugging_exec(PyObject *m) if (PyModule_AddIntConstant(m, "THREAD_STATUS_HAS_EXCEPTION", THREAD_STATUS_HAS_EXCEPTION) < 0) { return -1; } + if (PyModule_AddIntConstant(m, "THREAD_STATUS_MAIN_THREAD", THREAD_STATUS_MAIN_THREAD) < 0) { + return -1; + } if (RemoteDebugging_InitState(st) < 0) { return -1; diff --git a/Modules/_remote_debugging/threads.c b/Modules/_remote_debugging/threads.c index 2a46ccd0b3ddb9..527957c6fef067 100644 --- a/Modules/_remote_debugging/threads.c +++ b/Modules/_remote_debugging/threads.c @@ -292,7 +292,7 @@ unwind_stack_for_thread( uintptr_t *current_tstate, uintptr_t gil_holder_tstate, uintptr_t gc_frame, - uint64_t *current_tid + uintptr_t main_thread_tstate ) { PyObject *frame_info = NULL; PyObject *thread_id = NULL; @@ -310,7 +310,6 @@ unwind_stack_for_thread( STATS_ADD(unwinder, memory_bytes_read, unwinder->debug_offsets.thread_state.size); long tid = GET_MEMBER(long, ts, unwinder->debug_offsets.thread_state.native_thread_id); - *current_tid = tid; // Read GC collecting state from the interpreter (before any skip checks) uintptr_t interp_addr = GET_MEMBER(uintptr_t, ts, unwinder->debug_offsets.thread_state.interp); @@ -397,6 +396,10 @@ unwind_stack_for_thread( status_flags |= THREAD_STATUS_ON_CPU; } + if (*current_tstate == main_thread_tstate) { + status_flags |= THREAD_STATUS_MAIN_THREAD; + } + // Check if we should skip this thread based on mode int should_skip = 0; if (unwinder->skip_non_matching_threads) {