[Lldb-commits] [lldb] [lldb] Avoid per-thread searches when setting stop info (PR #236818)

Zack Johnson via lldb-commits lldb-commits at lists.llvm.org
Thu Oct 29 10:00:36 PDT 2026


https://github.com/zacklj89 updated https://github.com/llvm/llvm-project/pull/236818

>From 79bdb5ad47c8454a57702f6380a34b5ea6d12df5 Mon Sep 17 00:00:00 2001
From: Zack Johnson <zackj at nvidia.com>
Date: Thu, 29 Oct 2026 10:53:38 -0400
Subject: [PATCH 1/2] [lldb] Avoid per-thread searches when setting stop info

At a stop, ProcessGDBRemote::SetThreadStopInfo runs for every thread.
Each call searched the thread list for the thread, and m_thread_ids for
the position of the thread's PC in the stop reply's thread-pcs, so a stop
took time quadratic in the number of threads.

Callers that have the thread now pass it in, and WillPublicStop finds the
threads in a map. m_thread_ids is assigned in several places, so rather than
rely on each of them to clear the map, it checks the position it finds against
m_thread_ids and rebuilds the map when the position is missing or different.
---
 .../Process/gdb-remote/ProcessGDBRemote.cpp   | 55 +++++++++++++++----
 .../Process/gdb-remote/ProcessGDBRemote.h     | 12 +++-
 .../TestThreadPCsReordered.py                 | 55 +++++++++++++++++++
 .../many_threads_at_breakpoint/Makefile       |  3 +
 .../TestManyThreadsAtBreakpoint.py            | 42 ++++++++++++++
 .../many_threads_at_breakpoint/main.cpp       | 31 +++++++++++
 6 files changed, 184 insertions(+), 14 deletions(-)
 create mode 100644 lldb/test/API/functionalities/gdb_remote_client/TestThreadPCsReordered.py
 create mode 100644 lldb/test/API/functionalities/thread/many_threads_at_breakpoint/Makefile
 create mode 100644 lldb/test/API/functionalities/thread/many_threads_at_breakpoint/TestManyThreadsAtBreakpoint.py
 create mode 100644 lldb/test/API/functionalities/thread/many_threads_at_breakpoint/main.cpp

diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
index 540f620be4c957..4091849620619b 100644
--- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
@@ -1819,6 +1819,26 @@ void ProcessGDBRemote::SetThreadPc(const ThreadSP &thread_sp, uint64_t index) {
   }
 }
 
+std::optional<size_t> ProcessGDBRemote::FindThreadIDIndex(lldb::tid_t tid) {
+  // This runs for every thread at a stop, so searching m_thread_ids would make
+  // the stop quadratic in the number of threads. m_thread_ids is rewritten in
+  // several places that leave m_thread_id_positions alone, so trust a cached
+  // position only if m_thread_ids still has tid there, and rebuild otherwise.
+  std::lock_guard<std::recursive_mutex> guard(m_thread_list_real.GetMutex());
+  auto it = m_thread_id_positions.find(tid);
+  if (it != m_thread_id_positions.end() && it->second < m_thread_ids.size() &&
+      m_thread_ids[it->second] == tid)
+    return it->second;
+
+  m_thread_id_positions.clear();
+  for (auto [i, id] : llvm::enumerate(m_thread_ids))
+    m_thread_id_positions.try_emplace(id, i);
+  it = m_thread_id_positions.find(tid);
+  if (it == m_thread_id_positions.end())
+    return std::nullopt;
+  return it->second;
+}
+
 bool ProcessGDBRemote::GetThreadStopInfoFromJSON(
     ThreadGDBRemote *thread, const StructuredData::ObjectSP &thread_infos_sp) {
   // See if we got thread stop infos for all threads via the "jThreadsInfo"
@@ -1835,7 +1855,8 @@ bool ProcessGDBRemote::GetThreadStopInfoFromJSON(
           if (thread_dict->GetValueForKeyAsInteger<lldb::tid_t>(
                   "tid", tid, LLDB_INVALID_THREAD_ID)) {
             if (tid == thread->GetID())
-              return (bool)SetThreadStopInfo(thread_dict);
+              return (bool)SetThreadStopInfo(thread_dict,
+                                             thread->shared_from_this());
           }
         }
       }
@@ -1909,14 +1930,15 @@ ThreadSP ProcessGDBRemote::SetThreadStopInfo(
     LazyBool associated_with_dispatch_queue, addr_t dispatch_queue_t,
     std::string &queue_name, QueueKind queue_kind, uint64_t queue_serial,
     std::vector<lldb::addr_t> &added_binaries,
-    StructuredData::ObjectSP &detailed_binaries_info) {
+    StructuredData::ObjectSP &detailed_binaries_info, ThreadSP thread_sp) {
 
   if (tid == LLDB_INVALID_THREAD_ID)
     return nullptr;
 
-  ThreadSP thread_sp;
-  // Scope for "locker" below
-  {
+  // Stop info is set for every thread at a stop, so a search of the thread
+  // list here would take time quadratic in the number of threads. Search only
+  // when the caller does not have the thread.
+  if (!thread_sp || thread_sp->GetProtocolID() != tid) {
     // m_thread_list_real does have its own mutex, but we need to hold onto the
     // mutex between the call to m_thread_list_real.FindThreadByID(...) and the
     // m_thread_list_real.AddThread(...) so it doesn't change on us
@@ -1935,9 +1957,8 @@ ThreadSP ProcessGDBRemote::SetThreadStopInfo(
 
   reg_ctx_sp->InvalidateIfNeeded(true);
 
-  auto iter = llvm::find(m_thread_ids, tid);
-  if (iter != m_thread_ids.end())
-    SetThreadPc(thread_sp, iter - m_thread_ids.begin());
+  if (std::optional<size_t> index = FindThreadIDIndex(tid))
+    SetThreadPc(thread_sp, *index);
 
   ParseExpeditedRegisters(expedited_register_map, thread_sp);
 
@@ -2244,7 +2265,8 @@ ProcessGDBRemote::HandleThreadAsyncInterrupt(uint8_t signo,
 }
 
 lldb::ThreadSP
-ProcessGDBRemote::SetThreadStopInfo(StructuredData::Dictionary *thread_dict) {
+ProcessGDBRemote::SetThreadStopInfo(StructuredData::Dictionary *thread_dict,
+                                    lldb::ThreadSP thread_sp) {
   static constexpr llvm::StringLiteral g_key_tid("tid");
   static constexpr llvm::StringLiteral g_key_name("name");
   static constexpr llvm::StringLiteral g_key_reason("reason");
@@ -2427,7 +2449,8 @@ ProcessGDBRemote::SetThreadStopInfo(StructuredData::Dictionary *thread_dict) {
       tid, expedited_register_map, signo, thread_name, reason, description,
       exc_type, exc_data, thread_dispatch_qaddr, queue_vars_valid,
       associated_with_dispatch_queue, dispatch_queue_t, queue_name, queue_kind,
-      queue_serial_number, added_binaries, detailed_binaries_info);
+      queue_serial_number, added_binaries, detailed_binaries_info,
+      std::move(thread_sp));
 }
 
 StateType ProcessGDBRemote::SetThreadStopInfo(StringExtractor &stop_packet) {
@@ -2915,12 +2938,20 @@ void ProcessGDBRemote::WillPublicStop() {
     // and memory that was in the jThreadsInfo response.
     StructuredData::Array *thread_infos = threads_info_sp->GetAsArray();
     if (thread_infos) {
+      std::unordered_map<lldb::tid_t, ThreadSP> threads;
+      for (const ThreadSP &thread_sp : m_thread_list_real.Threads())
+        threads.try_emplace(thread_sp->GetProtocolID(), thread_sp);
       const size_t n = thread_infos->GetSize();
       for (size_t i = 0; i < n; ++i) {
         StructuredData::Dictionary *thread_dict =
             thread_infos->GetItemAtIndex(i)->GetAsDictionary();
-        if (thread_dict)
-          SetThreadStopInfo(thread_dict);
+        if (!thread_dict)
+          continue;
+        lldb::tid_t tid = LLDB_INVALID_THREAD_ID;
+        thread_dict->GetValueForKeyAsInteger("tid", tid);
+        auto thread = threads.find(tid);
+        SetThreadStopInfo(thread_dict,
+                          thread == threads.end() ? nullptr : thread->second);
       }
     }
   }
diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h
index 018d0699ef1586..1a029d58fca3db 100644
--- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h
+++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h
@@ -15,6 +15,7 @@
 #include <mutex>
 #include <optional>
 #include <string>
+#include <unordered_map>
 #include <vector>
 
 #include "lldb/Core/Diagnostics.h"
@@ -308,6 +309,8 @@ class ProcessGDBRemote : public Process,
   tid_collection m_thread_ids; // Thread IDs for all threads. This list gets
                                // updated after stopping
   std::vector<lldb::addr_t> m_thread_pcs;     // PC values for all the threads.
+  /// The position of each ID in m_thread_ids, for FindThreadIDIndex.
+  std::unordered_map<lldb::tid_t, size_t> m_thread_id_positions;
   /// Stop info caches filled at a stop and reset by WillResume, which runs on
   /// another thread. Hold the lock only long enough to copy the shared pointer
   /// out. The copy keeps the JSON alive past a reset, and reading the JSON
@@ -406,7 +409,8 @@ class ProcessGDBRemote : public Process,
   GetThreadStopInfoFromJSON(ThreadGDBRemote *thread,
                             const StructuredData::ObjectSP &thread_infos_sp);
 
-  lldb::ThreadSP SetThreadStopInfo(StructuredData::Dictionary *thread_dict);
+  lldb::ThreadSP SetThreadStopInfo(StructuredData::Dictionary *thread_dict,
+                                   lldb::ThreadSP thread_sp = nullptr);
 
   lldb::ThreadSP
   SetThreadStopInfo(lldb::tid_t tid,
@@ -419,7 +423,8 @@ class ProcessGDBRemote : public Process,
                     lldb::addr_t dispatch_queue_t, std::string &queue_name,
                     lldb::QueueKind queue_kind, uint64_t queue_serial,
                     std::vector<lldb::addr_t> &added_binaries,
-                    StructuredData::ObjectSP &detailed_binaries_info);
+                    StructuredData::ObjectSP &detailed_binaries_info,
+                    lldb::ThreadSP thread_sp = nullptr);
 
   void ClearThreadIDList();
 
@@ -538,6 +543,9 @@ class ProcessGDBRemote : public Process,
                              const std::string &description) override;
 
   void SetThreadPc(const lldb::ThreadSP &thread_sp, uint64_t index);
+
+  std::optional<size_t> FindThreadIDIndex(lldb::tid_t tid);
+
   using ModuleCacheKey = std::pair<std::string, std::string>;
   // KeyInfo for the cached module spec DenseMap.
   // The invariant is that all real keys will have the file and architecture
diff --git a/lldb/test/API/functionalities/gdb_remote_client/TestThreadPCsReordered.py b/lldb/test/API/functionalities/gdb_remote_client/TestThreadPCsReordered.py
new file mode 100644
index 00000000000000..9fc7bddbbced2d
--- /dev/null
+++ b/lldb/test/API/functionalities/gdb_remote_client/TestThreadPCsReordered.py
@@ -0,0 +1,55 @@
+"""
+Test that each thread keeps its own PC from a stop reply's thread-pcs when the
+next stop lists the same threads in another order.
+
+lldb finds a thread's PC by the thread's position in the stop reply's threads
+list, and caches the positions. A cache kept from the first stop would give
+each thread another thread's PC at the second.
+"""
+
+import lldb
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test.decorators import *
+from lldbsuite.test.gdbclientutils import *
+from lldbsuite.test.lldbgdbclient import GDBRemoteTestBase
+
+
+class TestThreadPCsReordered(GDBRemoteTestBase):
+    @skipIfXmlSupportMissing
+    def test_thread_order_changes(self):
+        class MyResponder(MockGDBServerResponder):
+            # Neither stop sends jstopinfo, and jThreadsInfo is unsupported,
+            # so thread-pcs is the only source of PCs.
+            def haltReason(self):
+                return "T05thread:1;threads:1,2,3;thread-pcs:1000,2000,3000;"
+
+            def cont(self):
+                return "T05thread:3;threads:3,1,2;thread-pcs:3100,1100,2100;"
+
+            def qXferRead(self, obj, annex, offset, length):
+                if annex == "target.xml":
+                    return (
+                        """<?xml version="1.0"?>
+                        <target version="1.0">
+                          <architecture>i386:x86-64</architecture>
+                          <feature name="org.gnu.gdb.i386.core">
+                            <reg name="rip" bitsize="64" regnum="0" type="code_ptr" group="general"/>
+                          </feature>
+                        </target>""",
+                        False,
+                    )
+                return None, False
+
+        self.server.responder = MyResponder()
+        target = self.dbg.CreateTarget("")
+        process = self.connect(target)
+
+        def pcs():
+            return {
+                thread.GetThreadID(): thread.GetFrameAtIndex(0).GetPC()
+                for thread in process
+            }
+
+        self.assertEqual(pcs(), {1: 0x1000, 2: 0x2000, 3: 0x3000})
+        self.assertSuccess(process.Continue())
+        self.assertEqual(pcs(), {1: 0x1100, 2: 0x2100, 3: 0x3100})
diff --git a/lldb/test/API/functionalities/thread/many_threads_at_breakpoint/Makefile b/lldb/test/API/functionalities/thread/many_threads_at_breakpoint/Makefile
new file mode 100644
index 00000000000000..566938ca0cc4e1
--- /dev/null
+++ b/lldb/test/API/functionalities/thread/many_threads_at_breakpoint/Makefile
@@ -0,0 +1,3 @@
+CXX_SOURCES := main.cpp
+ENABLE_THREADS := YES
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/thread/many_threads_at_breakpoint/TestManyThreadsAtBreakpoint.py b/lldb/test/API/functionalities/thread/many_threads_at_breakpoint/TestManyThreadsAtBreakpoint.py
new file mode 100644
index 00000000000000..6fa185a9d4cfd6
--- /dev/null
+++ b/lldb/test/API/functionalities/thread/many_threads_at_breakpoint/TestManyThreadsAtBreakpoint.py
@@ -0,0 +1,42 @@
+"""
+Test that each of many threads stopped at one breakpoint gets its own stop
+info, PC and registers.
+
+The threads reach the breakpoint together, so one stop can report many of them.
+Each thread first stores its own thread ID in a local, so a thread given
+another thread's PC or registers shows the wrong ID in its frame.
+"""
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+import lldbsuite.test.lldbutil as lldbutil
+
+
+ at requireThreadSupport
+class ManyThreadsAtBreakpointTestCase(TestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+
+    def test(self):
+        """Test that every thread stops at the breakpoint once, in its own frame."""
+        self.build()
+        _, process, _, bkpt = lldbutil.run_to_source_breakpoint(
+            self, "// break here", lldb.SBFileSpec("main.cpp"), only_one_thread=False
+        )
+        line = line_number("main.cpp", "// break here")
+
+        indexes = []
+        stopped = lldbutil.get_threads_stopped_at_breakpoint(process, bkpt)
+        while stopped:
+            for thread in stopped:
+                frame = thread.GetFrameAtIndex(0)
+                self.assertEqual(frame.GetLineEntry().GetLine(), line)
+                self.assertEqual(
+                    frame.FindVariable("tid").GetValueAsUnsigned(), thread.GetThreadID()
+                )
+                indexes.append(frame.FindVariable("index").GetValueAsSigned())
+            stopped = lldbutil.continue_to_breakpoint(process, bkpt)
+
+        # main.cpp starts 32 threads.
+        self.assertEqual(sorted(indexes), list(range(32)))
+        self.assertState(process.GetState(), lldb.eStateExited)
diff --git a/lldb/test/API/functionalities/thread/many_threads_at_breakpoint/main.cpp b/lldb/test/API/functionalities/thread/many_threads_at_breakpoint/main.cpp
new file mode 100644
index 00000000000000..49719bb3c03886
--- /dev/null
+++ b/lldb/test/API/functionalities/thread/many_threads_at_breakpoint/main.cpp
@@ -0,0 +1,31 @@
+// Threads stop at one breakpoint together. Each one first records the ID the
+// system gave it, so the test can check that the frame lldb shows for a thread
+// is that thread's own.
+
+#include <cstdio>
+#include <thread>
+#include <vector>
+
+// thread.h needs pthread_t on Darwin, which <thread> declares.
+#include "pseudo_barrier.h"
+#include "thread.h"
+
+pseudo_barrier_t g_barrier;
+
+void print_index(int index) {
+  uint64_t tid = get_thread_id();
+  // Release the threads together, so that several reach the breakpoint at once.
+  pseudo_barrier_wait(g_barrier);
+  printf("thread %d: %llu\n", index, (unsigned long long)tid); // break here
+}
+
+int main() {
+  constexpr int num_threads = 32;
+  pseudo_barrier_init(g_barrier, num_threads);
+  std::vector<std::thread> threads;
+  for (int i = 0; i < num_threads; ++i)
+    threads.emplace_back(print_index, i);
+  for (std::thread &thread : threads)
+    thread.join();
+  return 0;
+}

>From 66729f41879289c1b282687288eaf140c521c796 Mon Sep 17 00:00:00 2001
From: Zack Johnson <zackj at nvidia.com>
Date: Thu, 29 Oct 2026 12:59:44 -0400
Subject: [PATCH 2/2] [lldb] Skip TestManyThreadsAtBreakpoint on Windows

---
 .../many_threads_at_breakpoint/TestManyThreadsAtBreakpoint.py    | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lldb/test/API/functionalities/thread/many_threads_at_breakpoint/TestManyThreadsAtBreakpoint.py b/lldb/test/API/functionalities/thread/many_threads_at_breakpoint/TestManyThreadsAtBreakpoint.py
index 6fa185a9d4cfd6..26e63d92ba397f 100644
--- a/lldb/test/API/functionalities/thread/many_threads_at_breakpoint/TestManyThreadsAtBreakpoint.py
+++ b/lldb/test/API/functionalities/thread/many_threads_at_breakpoint/TestManyThreadsAtBreakpoint.py
@@ -14,6 +14,7 @@
 
 
 @requireThreadSupport
+ at skipIfWindows  # On Windows, lldb reports only some of the threads at the breakpoint.
 class ManyThreadsAtBreakpointTestCase(TestBase):
     NO_DEBUG_INFO_TESTCASE = True
 



More information about the lldb-commits mailing list