]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-135953: Properly obtain main thread identifier in Gecko Collector (#146045)
authorflow <flowlnlnln@gmail.com>
Sun, 22 Mar 2026 18:53:00 +0000 (15:53 -0300)
committerGitHub <noreply@github.com>
Sun, 22 Mar 2026 18:53:00 +0000 (18:53 +0000)
Lib/profiling/sampling/constants.py
Lib/profiling/sampling/gecko_collector.py
Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py
Lib/test/test_profiling/test_sampling_profiler/test_collectors.py
Misc/NEWS.d/next/Tools-Demos/2026-03-22-00-00-00.gh-issue-135953.IptOwg.rst [new file with mode: 0644]
Modules/_remote_debugging/_remote_debugging.h
Modules/_remote_debugging/module.c
Modules/_remote_debugging/threads.c

index 58a57700fbdd4a32db63a3bfe1e970d576327726..a364d0b8fde1e049b84f6a960d7d3d64f446d5ba 100644 (file)
@@ -37,6 +37,7 @@ try:
         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 @@ except ImportError:
     THREAD_STATUS_UNKNOWN = (1 << 2)
     THREAD_STATUS_GIL_REQUESTED = (1 << 3)
     THREAD_STATUS_HAS_EXCEPTION = (1 << 4)
+    THREAD_STATUS_MAIN_THREAD = (1 << 5)
index 28ef9b69bf796897b235c44e7a7bd20af42cc65d..8986194268b3ce47f49518c009ceb483210032dd 100644 (file)
@@ -9,7 +9,7 @@ import time
 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 @@ except ImportError:
     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
@@ -174,15 +175,16 @@ class GeckoCollector(Collector):
             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)
+                    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)
@@ -288,18 +290,12 @@ class GeckoCollector(Collector):
 
         self.sample_count += len(times)
 
-    def _create_thread(self, tid):
+    def _create_thread(self, tid, is_main_thread):
         """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
-
         thread = {
             "name": f"Thread-{tid}",
-            "isMainThread": is_main,
+            "isMainThread": is_main_thread,
             "processStartupTime": 0,
             "processShutdownTime": None,
             "registerTime": 0,
index 033a533fe5444ec27a9abaca15d5a36fa1051126..29f83c843561cd0bbda74771166c11d2c954d6a9 100644 (file)
@@ -18,9 +18,11 @@ try:
         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:
@@ -318,6 +320,7 @@ class TestBinaryRoundTrip(BinaryFormatTestBase):
             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 @@ class TestBinaryRoundTrip(BinaryFormatTestBase):
         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 = [
index 8e6afa91e89dafa022ed3fb9f29d697ea24beb38..06c9e51e0c9c55e7c181d0309943f8d8a609debc 100644 (file)
@@ -28,6 +28,7 @@ try:
         THREAD_STATUS_HAS_GIL,
         THREAD_STATUS_ON_CPU,
         THREAD_STATUS_GIL_REQUESTED,
+        THREAD_STATUS_MAIN_THREAD,
     )
 except ImportError:
     raise unittest.SkipTest(
@@ -524,6 +525,7 @@ class TestSampleProfilerComponents(unittest.TestCase):
                     MockThreadInfo(
                         1,
                         [MockFrameInfo("file.py", 10, "func1"), MockFrameInfo("file.py", 20, "func2")],
+                        status=THREAD_STATUS_MAIN_THREAD,
                     )
                 ],
             )
@@ -556,6 +558,7 @@ class TestSampleProfilerComponents(unittest.TestCase):
         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)
diff --git a/Misc/NEWS.d/next/Tools-Demos/2026-03-22-00-00-00.gh-issue-135953.IptOwg.rst b/Misc/NEWS.d/next/Tools-Demos/2026-03-22-00-00-00.gh-issue-135953.IptOwg.rst
new file mode 100644 (file)
index 0000000..50f39a8
--- /dev/null
@@ -0,0 +1,3 @@
+Properly identify the main thread in the Gecko profiler collector by
+using a status flag from the interpreter state instead of relying on
+:func:`threading.main_thread` in the collector process.
index 7bcb2f483234ecb6d6cd1b9c8f4533c7268f7ab4..570f6b23b7584979b607e7ba9936e8a1c8bad7e5 100644 (file)
@@ -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)                              \
@@ -575,7 +576,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,
+    uintptr_t main_thread_tstate
 );
 
 /* Thread stopping functions (for blocking mode) */
index 040bd3db3773154a4a84468cd1f8eafaaf675e3f..4f294b80ba0739b98c303d640c83457ce4c4080b 100644 (file)
@@ -583,11 +583,16 @@ _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);
+
         while (current_tstate != 0) {
             uintptr_t prev_tstate = current_tstate;
             PyObject* frame_info = unwind_stack_for_thread(self, &current_tstate,
                                                            gil_holder_tstate,
-                                                           gc_frame);
+                                                           gc_frame,
+                                                           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 ||
@@ -1207,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;
index 3100b83c8f489978803d22470e7656de34937476..527957c6fef067bff197fdabced514dee697dcf6 100644 (file)
@@ -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,
+    uintptr_t main_thread_tstate
 ) {
     PyObject *frame_info = NULL;
     PyObject *thread_id = NULL;
@@ -395,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) {