[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