https://github.com/jeffreytan81 updated https://github.com/llvm/llvm-project/pull/98568
>From 13af0ff31688ca0a23f1fec65ca2d5797b65e31f Mon Sep 17 00:00:00 2001 From: jeffreytan81 <jeffrey...@fb.com> Date: Thu, 11 Jul 2024 17:24:41 -0700 Subject: [PATCH 1/3] Support single stopped event in lldb-dap --- .../test/tools/lldb-dap/dap_server.py | 21 ++++- .../test/tools/lldb-dap/lldbdap_testcase.py | 5 +- .../API/tools/lldb-dap/stop-events/Makefile | 4 + .../stop-events/TestDAP_stopEvents.py | 92 +++++++++++++++++++ .../API/tools/lldb-dap/stop-events/main.cpp | 74 +++++++++++++++ lldb/tools/lldb-dap/DAP.cpp | 2 +- lldb/tools/lldb-dap/DAP.h | 3 + lldb/tools/lldb-dap/lldb-dap.cpp | 47 +++++++++- 8 files changed, 240 insertions(+), 8 deletions(-) create mode 100644 lldb/test/API/tools/lldb-dap/stop-events/Makefile create mode 100644 lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stopEvents.py create mode 100644 lldb/test/API/tools/lldb-dap/stop-events/main.cpp diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py index a324af57b61df..84e6fe82a06dc 100644 --- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py @@ -125,6 +125,8 @@ def __init__(self, recv, send, init_commands, log_file=None): self.initialize_body = None self.thread_stop_reasons = {} self.breakpoint_events = [] + self.thread_events_body = [] + self.stopped_events = [] self.progress_events = [] self.reverse_requests = [] self.sequence = 1 @@ -227,6 +229,8 @@ def handle_recv_packet(self, packet): # When a new process is attached or launched, remember the # details that are available in the body of the event self.process_event_body = body + elif event == "continued": + self.stopped_events = [] elif event == "stopped": # Each thread that stops with a reason will send a # 'stopped' event. We need to remember the thread stop @@ -234,6 +238,7 @@ def handle_recv_packet(self, packet): # that information. self._process_stopped() tid = body["threadId"] + self.stopped_events.append(packet) self.thread_stop_reasons[tid] = body elif event == "breakpoint": # Breakpoint events come in when a breakpoint has locations @@ -242,6 +247,10 @@ def handle_recv_packet(self, packet): self.breakpoint_events.append(packet) # no need to add 'breakpoint' event packets to our packets list return keepGoing + elif event == "thread": + self.thread_events_body.append(body) + # no need to add 'thread' event packets to our packets list + return keepGoing elif event.startswith("progress"): # Progress events come in as 'progressStart', 'progressUpdate', # and 'progressEnd' events. Keep these around in case test @@ -418,6 +427,15 @@ def get_threads(self): self.request_threads() return self.threads + def get_thread_events(self, reason=None): + if reason == None: + return self.thread_events_body + else: + return [body for body in self.thread_events_body if body["reason"] == reason] + + def get_stopped_events(self): + return self.stopped_events + def get_thread_id(self, threadIndex=0): """Utility function to get the first thread ID in the thread list. If the thread list is empty, then fetch the threads. @@ -707,7 +725,7 @@ def request_evaluate(self, expression, frameIndex=0, threadId=None, context=None } return self.send_recv(command_dict) - def request_initialize(self, sourceInitFile): + def request_initialize(self, sourceInitFile, singleStoppedEvent=False): command_dict = { "command": "initialize", "type": "request", @@ -723,6 +741,7 @@ def request_initialize(self, sourceInitFile): "supportsVariableType": True, "supportsStartDebuggingRequest": True, "sourceInitFile": sourceInitFile, + "singleStoppedEvent": singleStoppedEvent, }, } response = self.send_recv(command_dict) diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py index a312a88ebd7e5..90ab2a8d4898a 100644 --- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py @@ -350,6 +350,7 @@ def launch( cwd=None, env=None, stopOnEntry=False, + singleStoppedEvent=False, disableASLR=True, disableSTDIO=False, shellExpandArguments=False, @@ -387,7 +388,7 @@ def cleanup(): self.addTearDownHook(cleanup) # Initialize and launch the program - self.dap_server.request_initialize(sourceInitFile) + self.dap_server.request_initialize(sourceInitFile, singleStoppedEvent) response = self.dap_server.request_launch( program, args=args, @@ -432,6 +433,7 @@ def build_and_launch( cwd=None, env=None, stopOnEntry=False, + singleStoppedEvent=False, disableASLR=True, disableSTDIO=False, shellExpandArguments=False, @@ -468,6 +470,7 @@ def build_and_launch( cwd, env, stopOnEntry, + singleStoppedEvent, disableASLR, disableSTDIO, shellExpandArguments, diff --git a/lldb/test/API/tools/lldb-dap/stop-events/Makefile b/lldb/test/API/tools/lldb-dap/stop-events/Makefile new file mode 100644 index 0000000000000..de4ec12b13cbc --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/stop-events/Makefile @@ -0,0 +1,4 @@ +ENABLE_THREADS := YES +CXX_SOURCES := main.cpp + +include Makefile.rules diff --git a/lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stopEvents.py b/lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stopEvents.py new file mode 100644 index 0000000000000..ce4ef7f2fabd3 --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stopEvents.py @@ -0,0 +1,92 @@ +""" +Test lldb-dap setBreakpoints request +""" + +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +import lldbdap_testcase +from lldbsuite.test import lldbutil + + +class TestDAP_stopEvents(lldbdap_testcase.DAPTestCaseBase): + + @skipIfWindows + @skipIfRemote + def test_single_stop_event(self): + """ + Ensure single stopped event is sent during stop when singleStoppedEvent + is set to True. + """ + program = self.getBuildArtifact("a.out") + self.build_and_launch(program, singleStoppedEvent=True) + source = "main.cpp" + breakpoint_line = line_number(source, "// Set breakpoint1 here") + lines = [breakpoint_line] + # Set breakpoint in the thread function + breakpoint_ids = self.set_source_breakpoints(source, lines) + self.assertEqual( + len(breakpoint_ids), len(lines), "expect correct number of breakpoints" + ) + self.continue_to_breakpoints(breakpoint_ids) + self.assertEqual( + len(self.dap_server.get_stopped_events()), 1, "expect one thread stopped" + ) + + loop_count = 10 + while loop_count > 0: + self.dap_server.request_continue() + stopped_event = self.dap_server.wait_for_stopped() + self.assertEqual( + len(self.dap_server.get_stopped_events()), + 1, + "expect one thread stopped", + ) + loop_count -= 1 + + @skipIfWindows + @skipIfRemote + def test_correct_thread_count(self): + """ + Test that the correct number of threads are reported in the stop event. + No thread exited events are sent. + """ + program = self.getBuildArtifact("a.out") + self.build_and_launch(program, singleStoppedEvent=True) + source = "main.cpp" + breakpoint_line = line_number(source, "// break worker thread here") + lines = [breakpoint_line] + # Set breakpoint in the thread function + breakpoint_ids = self.set_source_breakpoints(source, lines) + self.assertEqual( + len(breakpoint_ids), len(lines), "expect correct number of breakpoints" + ) + self.continue_to_breakpoints(breakpoint_ids) + + threads = self.dap_server.get_threads() + self.assertEqual(len(threads), 2, "expect two threads in first worker thread") + + self.dap_server.request_continue() + stopped_event = self.dap_server.wait_for_stopped() + threads = self.dap_server.get_threads() + self.assertEqual( + len(threads), 3, "expect three threads in second worker thread" + ) + + main_thread_breakpoint_line = line_number(source, "// break main thread here") + # Set breakpoint in the thread function + main_breakpoint_ids = self.set_source_breakpoints( + source, [main_thread_breakpoint_line] + ) + self.continue_to_breakpoints(main_breakpoint_ids) + + threads = self.dap_server.get_threads() + self.assertEqual( + len(threads), 3, "expect three threads in second worker thread" + ) + + exited_threads = self.dap_server.get_thread_events("exited") + self.assertEqual( + len(exited_threads), + 0, + "expect no threads exited after hitting main thread breakpoint during context switch", + ) diff --git a/lldb/test/API/tools/lldb-dap/stop-events/main.cpp b/lldb/test/API/tools/lldb-dap/stop-events/main.cpp new file mode 100644 index 0000000000000..de870dbc51062 --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/stop-events/main.cpp @@ -0,0 +1,74 @@ +#include <condition_variable> +#include <iostream> +#include <mutex> +#include <thread> + +std::mutex mtx; +std::condition_variable cv; +int ready_id = 0; + +void worker(int id) { + std::cout << "Worker " << id << " executing..." << std::endl; + // Signal the main thread to continue main thread + { + std::lock_guard<std::mutex> lock(mtx); + ready_id = id; // break worker thread here + } + cv.notify_one(); + + // Simulate some work + std::this_thread::sleep_for(std::chrono::seconds(10)); + std::cout << "Worker " << id << " finished." << std::endl; +} + +void thread_proc(int threadId) { + std::mutex repro_mtx; + for (;;) { + int i = 0; + ++i; // Set breakpoint1 here + repro_mtx.lock(); + std::cout << "Thread " << threadId << " is running, " << i << std::endl; + repro_mtx.unlock(); + + // Simulate some work + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + i += 10; // Set breakpoint2 here + repro_mtx.lock(); + std::cout << "Thread " << threadId << " finished" << std::endl; + repro_mtx.unlock(); // Unlock the mutex after printing + } +} + +void run_threads_in_loop(int numThreads) { + std::thread threads[numThreads]; + // Create threads + for (int i = 0; i < numThreads; ++i) { + threads[i] = std::thread(thread_proc, i); + } + for (int i = 0; i < numThreads; ++i) { + threads[i].join(); + } +} + +int main() { + // Create the first worker thread + std::thread t1(worker, 1); + + // Wait until signaled by the first thread + std::unique_lock<std::mutex> lock(mtx); + cv.wait(lock, [] { return ready_id == 1; }); + + // Create the second worker thread + std::thread t2(worker, 2); + + // Wait until signaled by the second thread + cv.wait(lock, [] { return ready_id == 2; }); + + // Join the first thread to ensure main waits for it to finish + t1.join(); // break main thread here + t2.join(); + + run_threads_in_loop(100); + return 0; +} diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp index 0196aed819f2b..ba74d9fda2d3f 100644 --- a/lldb/tools/lldb-dap/DAP.cpp +++ b/lldb/tools/lldb-dap/DAP.cpp @@ -34,7 +34,7 @@ DAP g_dap; DAP::DAP() : broadcaster("lldb-dap"), exception_breakpoints(), focus_tid(LLDB_INVALID_THREAD_ID), stop_at_entry(false), is_attach(false), - enable_auto_variable_summaries(false), + single_stopped_event(false), enable_auto_variable_summaries(false), enable_synthetic_child_debugging(false), restarting_process_id(LLDB_INVALID_PROCESS_ID), configuration_done_sent(false), waiting_for_run_in_terminal(false), diff --git a/lldb/tools/lldb-dap/DAP.h b/lldb/tools/lldb-dap/DAP.h index 37e57d58968d9..38126005d753c 100644 --- a/lldb/tools/lldb-dap/DAP.h +++ b/lldb/tools/lldb-dap/DAP.h @@ -174,6 +174,9 @@ struct DAP { llvm::once_flag terminated_event_flag; bool stop_at_entry; bool is_attach; + // Whether to send single stopped event when multiple stopped events + // occured at the same time (eg. breakpoints by threads simultanously) + bool single_stopped_event; bool enable_auto_variable_summaries; bool enable_synthetic_child_debugging; // The process event thread normally responds to process exited events by diff --git a/lldb/tools/lldb-dap/lldb-dap.cpp b/lldb/tools/lldb-dap/lldb-dap.cpp index b74474b9d383c..36eb543fbe1dc 100644 --- a/lldb/tools/lldb-dap/lldb-dap.cpp +++ b/lldb/tools/lldb-dap/lldb-dap.cpp @@ -243,8 +243,6 @@ void SendThreadStoppedEvent() { if (process.IsValid()) { auto state = process.GetState(); if (state == lldb::eStateStopped) { - llvm::DenseSet<lldb::tid_t> old_thread_ids; - old_thread_ids.swap(g_dap.thread_ids); uint32_t stop_id = process.GetStopID(); const uint32_t num_threads = process.GetNumThreads(); @@ -255,28 +253,34 @@ void SendThreadStoppedEvent() { lldb::tid_t first_tid_with_reason = LLDB_INVALID_THREAD_ID; uint32_t num_threads_with_reason = 0; bool focus_thread_exists = false; + lldb::SBThread focus_thread, first_thread_with_reason; for (uint32_t thread_idx = 0; thread_idx < num_threads; ++thread_idx) { lldb::SBThread thread = process.GetThreadAtIndex(thread_idx); const lldb::tid_t tid = thread.GetThreadID(); const bool has_reason = ThreadHasStopReason(thread); // If the focus thread doesn't have a stop reason, clear the thread ID if (tid == g_dap.focus_tid) { + focus_thread = thread; focus_thread_exists = true; if (!has_reason) g_dap.focus_tid = LLDB_INVALID_THREAD_ID; } if (has_reason) { ++num_threads_with_reason; - if (first_tid_with_reason == LLDB_INVALID_THREAD_ID) + if (first_tid_with_reason == LLDB_INVALID_THREAD_ID) { first_tid_with_reason = tid; + first_thread_with_reason = thread; + } } } // We will have cleared g_dap.focus_tid if the focus thread doesn't have // a stop reason, so if it was cleared, or wasn't set, or doesn't exist, // then set the focus thread to the first thread with a stop reason. - if (!focus_thread_exists || g_dap.focus_tid == LLDB_INVALID_THREAD_ID) + if (!focus_thread_exists || g_dap.focus_tid == LLDB_INVALID_THREAD_ID) { g_dap.focus_tid = first_tid_with_reason; + focus_thread = first_thread_with_reason; + } // If no threads stopped with a reason, then report the first one so // we at least let the UI know we stopped. @@ -284,16 +288,40 @@ void SendThreadStoppedEvent() { lldb::SBThread thread = process.GetThreadAtIndex(0); g_dap.focus_tid = thread.GetThreadID(); g_dap.SendJSON(CreateThreadStopped(thread, stop_id)); + } else if (g_dap.single_stopped_event) { + // If single_stopped_event option is true only one stopped event will + // be sent during debugger stop. The focused thread's stopped event is + // preferred if it is stopped with a reason; otherwise, we simply use + // the first stopped thread. + // + // This option would be preferred for VSCode IDE because multiple + // stopped events would cause confusing UX. + // + // TODO: do we need to give priority to exception/signal stopped event + // over normal stepping complete/breakpoint? + // + + assert(focus_thread.IsValid()); + assert(g_dap.focus_tid == focus_thread.GetThreadID()); + g_dap.SendJSON(CreateThreadStopped(focus_thread, stop_id)); } else { for (uint32_t thread_idx = 0; thread_idx < num_threads; ++thread_idx) { lldb::SBThread thread = process.GetThreadAtIndex(thread_idx); - g_dap.thread_ids.insert(thread.GetThreadID()); if (ThreadHasStopReason(thread)) { g_dap.SendJSON(CreateThreadStopped(thread, stop_id)); } } } + // Update existing thread ids and send thread exit event + // for non-exist ones. + llvm::DenseSet<lldb::tid_t> old_thread_ids; + old_thread_ids.swap(g_dap.thread_ids); + for (uint32_t thread_idx = 0; thread_idx < num_threads; ++thread_idx) { + lldb::SBThread thread = process.GetThreadAtIndex(thread_idx); + g_dap.thread_ids.insert(thread.GetThreadID()); + } + for (auto tid : old_thread_ids) { auto end = g_dap.thread_ids.end(); auto pos = g_dap.thread_ids.find(tid); @@ -880,6 +908,10 @@ void request_attach(const llvm::json::Object &request) { void request_continue(const llvm::json::Object &request) { llvm::json::Object response; FillResponse(request, response); + auto arguments = request.getObject("arguments"); + lldb::SBThread thread = g_dap.GetLLDBThread(*arguments); + g_dap.focus_tid = thread.GetThreadID(); + lldb::SBProcess process = g_dap.target.GetProcess(); lldb::SBError error = process.Continue(); llvm::json::Object body; @@ -1625,6 +1657,11 @@ void request_initialize(const llvm::json::Object &request) { "Get or set the repl behavior of lldb-dap evaluation requests."); g_dap.progress_event_thread = std::thread(ProgressEventThreadFunction); + // singleStoppedEvent option is not from formal DAP specification. It is an + // lldb specific option to experiment stopped events behaivor against + // application with multiple threads. + g_dap.single_stopped_event = + GetBoolean(arguments, "singleStoppedEvent", false); // Start our event thread so we can receive events from the debugger, target, // process and more. >From e564ad381eda8438d89949a0ac262f56f3425e43 Mon Sep 17 00:00:00 2001 From: jeffreytan81 <jeffrey...@fb.com> Date: Thu, 11 Jul 2024 17:47:34 -0700 Subject: [PATCH 2/3] Fix formatter --- .../Python/lldbsuite/test/tools/lldb-dap/dap_server.py | 4 +++- .../test/API/tools/lldb-dap/stop-events/TestDAP_stopEvents.py | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py index 84e6fe82a06dc..284bb1781ead9 100644 --- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py @@ -431,7 +431,9 @@ def get_thread_events(self, reason=None): if reason == None: return self.thread_events_body else: - return [body for body in self.thread_events_body if body["reason"] == reason] + return [ + body for body in self.thread_events_body if body["reason"] == reason + ] def get_stopped_events(self): return self.stopped_events diff --git a/lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stopEvents.py b/lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stopEvents.py index ce4ef7f2fabd3..00d8b7dd1e8e8 100644 --- a/lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stopEvents.py +++ b/lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stopEvents.py @@ -9,7 +9,6 @@ class TestDAP_stopEvents(lldbdap_testcase.DAPTestCaseBase): - @skipIfWindows @skipIfRemote def test_single_stop_event(self): >From eacd9c68859e39db616f0fca5a28518e5fb9f5c0 Mon Sep 17 00:00:00 2001 From: jeffreytan81 <jeffrey...@fb.com> Date: Fri, 12 Jul 2024 09:26:52 -0700 Subject: [PATCH 3/3] Fix formatter again --- .../packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py index 284bb1781ead9..ef9799765733e 100644 --- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py @@ -434,7 +434,7 @@ def get_thread_events(self, reason=None): return [ body for body in self.thread_events_body if body["reason"] == reason ] - + def get_stopped_events(self): return self.stopped_events _______________________________________________ lldb-commits mailing list lldb-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits