Author: John Harrison Date: 2024-09-10T12:40:20-07:00 New Revision: 5b4100cc354148a1140546e7f5ac2bf380bc5eff
URL: https://github.com/llvm/llvm-project/commit/5b4100cc354148a1140546e7f5ac2bf380bc5eff DIFF: https://github.com/llvm/llvm-project/commit/5b4100cc354148a1140546e7f5ac2bf380bc5eff.diff LOG: [lldb-dap] Improve `stackTrace` and `exceptionInfo` DAP request handlers (#105905) Refactoring `stackTrace` to perform frame look ups in a more on-demand fashion to improve overall performance. Additionally adding additional information to the `exceptionInfo` request to report exception stacks there instead of merging the exception stack into the stack trace. The `exceptionInfo` request is only called if a stop event occurs with `reason='exception'`, which should mitigate the performance of `SBThread::GetCurrentException` calls. Adding unit tests for exception handling and stack trace supporting. Added: lldb/test/API/tools/lldb-dap/exception/cpp/Makefile lldb/test/API/tools/lldb-dap/exception/cpp/TestDAP_exception_cpp.py lldb/test/API/tools/lldb-dap/exception/cpp/main.cpp lldb/test/API/tools/lldb-dap/exception/main.c lldb/test/API/tools/lldb-dap/exception/objc/Makefile lldb/test/API/tools/lldb-dap/exception/objc/TestDAP_exception_objc.py lldb/test/API/tools/lldb-dap/exception/objc/main.m lldb/test/API/tools/lldb-dap/extendedStackTrace/Makefile lldb/test/API/tools/lldb-dap/extendedStackTrace/TestDAP_extendedStackTrace.py lldb/test/API/tools/lldb-dap/extendedStackTrace/main.m Modified: lldb/packages/Python/lldbsuite/test/lldbplatformutil.py lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py lldb/test/API/tools/lldb-dap/exception/Makefile lldb/test/API/tools/lldb-dap/exception/TestDAP_exception.py lldb/test/API/tools/lldb-dap/stackTrace/TestDAP_stackTrace.py lldb/test/API/tools/lldb-dap/stackTrace/main.c lldb/test/API/tools/lldb-dap/stackTraceMissingFunctionName/TestDAP_stackTraceMissingFunctionName.py lldb/tools/lldb-dap/JSONUtils.cpp lldb/tools/lldb-dap/JSONUtils.h lldb/tools/lldb-dap/README.md lldb/tools/lldb-dap/lldb-dap.cpp lldb/tools/lldb-dap/package.json Removed: lldb/test/API/tools/lldb-dap/exception/main.cpp ################################################################################ diff --git a/lldb/packages/Python/lldbsuite/test/lldbplatformutil.py b/lldb/packages/Python/lldbsuite/test/lldbplatformutil.py index 602e15d207e94a..3d8c713562e9bf 100644 --- a/lldb/packages/Python/lldbsuite/test/lldbplatformutil.py +++ b/lldb/packages/Python/lldbsuite/test/lldbplatformutil.py @@ -181,6 +181,22 @@ def findMainThreadCheckerDylib(): return "" +def findBacktraceRecordingDylib(): + if not platformIsDarwin(): + return "" + + if getPlatform() in lldbplatform.translate(lldbplatform.darwin_embedded): + return "/Developer/usr/lib/libBacktraceRecording.dylib" + + with os.popen("xcode-select -p") as output: + xcode_developer_path = output.read().strip() + mtc_dylib_path = "%s/usr/lib/libBacktraceRecording.dylib" % xcode_developer_path + if os.path.isfile(mtc_dylib_path): + return mtc_dylib_path + + return "" + + class _PlatformContext(object): """Value object class which contains platform-specific options.""" 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 b095171d8fd1a4..c6417760f17a2b 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 @@ -707,6 +707,17 @@ def request_evaluate(self, expression, frameIndex=0, threadId=None, context=None } return self.send_recv(command_dict) + def request_exceptionInfo(self, threadId=None): + if threadId is None: + threadId = self.get_thread_id() + args_dict = {"threadId": threadId} + command_dict = { + "command": "exceptionInfo", + "type": "request", + "arguments": args_dict, + } + return self.send_recv(command_dict) + def request_initialize(self, sourceInitFile): command_dict = { "command": "initialize", @@ -754,6 +765,7 @@ def request_launch( runInTerminal=False, postRunCommands=None, enableAutoVariableSummaries=False, + enableDisplayExtendedBacktrace=False, enableSyntheticChildDebugging=False, commandEscapePrefix=None, customFrameFormat=None, @@ -806,6 +818,7 @@ def request_launch( args_dict["enableAutoVariableSummaries"] = enableAutoVariableSummaries args_dict["enableSyntheticChildDebugging"] = enableSyntheticChildDebugging + args_dict["enableDisplayExtendedBacktrace"] = enableDisplayExtendedBacktrace args_dict["commandEscapePrefix"] = commandEscapePrefix command_dict = {"command": "launch", "type": "request", "arguments": args_dict} 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 709b7aff11d7f2..7b192d62881729 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 @@ -103,13 +103,14 @@ def verify_breakpoint_hit(self, breakpoint_ids): return self.assertTrue(False, "breakpoint not hit") - def verify_stop_exception_info(self, expected_description): + def verify_stop_exception_info(self, expected_description, timeout=timeoutval): """Wait for the process we are debugging to stop, and verify the stop reason is 'exception' and that the description matches 'expected_description' """ - stopped_events = self.dap_server.wait_for_stopped() + stopped_events = self.dap_server.wait_for_stopped(timeout=timeout) for stopped_event in stopped_events: + print("stopped_event", stopped_event) if "body" in stopped_event: body = stopped_event["body"] if "reason" not in body: @@ -180,6 +181,10 @@ def get_stackFrames(self, threadId=None, startFrame=None, levels=None, dump=Fals ) return stackFrames + def get_exceptionInfo(self, threadId=None): + response = self.dap_server.request_exceptionInfo(threadId=threadId) + return self.get_dict_value(response, ["body"]) + def get_source_and_line(self, threadId=None, frameIndex=0): stackFrames = self.get_stackFrames( threadId=threadId, startFrame=frameIndex, levels=1 @@ -381,6 +386,7 @@ def launch( expectFailure=False, postRunCommands=None, enableAutoVariableSummaries=False, + enableDisplayExtendedBacktrace=False, enableSyntheticChildDebugging=False, commandEscapePrefix=None, customFrameFormat=None, @@ -422,6 +428,7 @@ def cleanup(): runInTerminal=runInTerminal, postRunCommands=postRunCommands, enableAutoVariableSummaries=enableAutoVariableSummaries, + enableDisplayExtendedBacktrace=enableDisplayExtendedBacktrace, enableSyntheticChildDebugging=enableSyntheticChildDebugging, commandEscapePrefix=commandEscapePrefix, customFrameFormat=customFrameFormat, @@ -461,6 +468,7 @@ def build_and_launch( postRunCommands=None, lldbDAPEnv=None, enableAutoVariableSummaries=False, + enableDisplayExtendedBacktrace=False, enableSyntheticChildDebugging=False, commandEscapePrefix=None, customFrameFormat=None, @@ -497,6 +505,7 @@ def build_and_launch( postRunCommands=postRunCommands, enableAutoVariableSummaries=enableAutoVariableSummaries, enableSyntheticChildDebugging=enableSyntheticChildDebugging, + enableDisplayExtendedBacktrace=enableDisplayExtendedBacktrace, commandEscapePrefix=commandEscapePrefix, customFrameFormat=customFrameFormat, customThreadFormat=customThreadFormat, diff --git a/lldb/test/API/tools/lldb-dap/exception/Makefile b/lldb/test/API/tools/lldb-dap/exception/Makefile index 99998b20bcb050..10495940055b63 100644 --- a/lldb/test/API/tools/lldb-dap/exception/Makefile +++ b/lldb/test/API/tools/lldb-dap/exception/Makefile @@ -1,3 +1,3 @@ -CXX_SOURCES := main.cpp +C_SOURCES := main.c include Makefile.rules diff --git a/lldb/test/API/tools/lldb-dap/exception/TestDAP_exception.py b/lldb/test/API/tools/lldb-dap/exception/TestDAP_exception.py index 8c2c0154ba65c0..39d73737b7e8c0 100644 --- a/lldb/test/API/tools/lldb-dap/exception/TestDAP_exception.py +++ b/lldb/test/API/tools/lldb-dap/exception/TestDAP_exception.py @@ -1,5 +1,5 @@ """ -Test exception behavior in DAP +Test exception behavior in DAP with signal. """ @@ -16,8 +16,10 @@ def test_stopped_description(self): event. """ program = self.getBuildArtifact("a.out") - print("test_stopped_description called", flush=True) self.build_and_launch(program) - self.dap_server.request_continue() self.assertTrue(self.verify_stop_exception_info("signal SIGABRT")) + exceptionInfo = self.get_exceptionInfo() + self.assertEqual(exceptionInfo["breakMode"], "always") + self.assertEqual(exceptionInfo["description"], "signal SIGABRT") + self.assertEqual(exceptionInfo["exceptionId"], "signal") diff --git a/lldb/test/API/tools/lldb-dap/exception/cpp/Makefile b/lldb/test/API/tools/lldb-dap/exception/cpp/Makefile new file mode 100644 index 00000000000000..99998b20bcb050 --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/exception/cpp/Makefile @@ -0,0 +1,3 @@ +CXX_SOURCES := main.cpp + +include Makefile.rules diff --git a/lldb/test/API/tools/lldb-dap/exception/cpp/TestDAP_exception_cpp.py b/lldb/test/API/tools/lldb-dap/exception/cpp/TestDAP_exception_cpp.py new file mode 100644 index 00000000000000..6471e2b87251a7 --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/exception/cpp/TestDAP_exception_cpp.py @@ -0,0 +1,26 @@ +""" +Test exception behavior in DAP with c++ throw. +""" + + +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +import lldbdap_testcase + + +class TestDAP_exception_cpp(lldbdap_testcase.DAPTestCaseBase): + @skipIfWindows + def test_stopped_description(self): + """ + Test that exception description is shown correctly in stopped + event. + """ + program = self.getBuildArtifact("a.out") + self.build_and_launch(program) + self.dap_server.request_continue() + self.assertTrue(self.verify_stop_exception_info("signal SIGABRT")) + exceptionInfo = self.get_exceptionInfo() + self.assertEqual(exceptionInfo["breakMode"], "always") + self.assertEqual(exceptionInfo["description"], "signal SIGABRT") + self.assertEqual(exceptionInfo["exceptionId"], "signal") + self.assertIsNotNone(exceptionInfo["details"]) diff --git a/lldb/test/API/tools/lldb-dap/exception/cpp/main.cpp b/lldb/test/API/tools/lldb-dap/exception/cpp/main.cpp new file mode 100644 index 00000000000000..39d89b95319a8c --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/exception/cpp/main.cpp @@ -0,0 +1,6 @@ +#include <stdexcept> + +int main(int argc, char const *argv[]) { + throw std::invalid_argument("throwing exception for testing"); + return 0; +} diff --git a/lldb/test/API/tools/lldb-dap/exception/main.cpp b/lldb/test/API/tools/lldb-dap/exception/main.c similarity index 56% rename from lldb/test/API/tools/lldb-dap/exception/main.cpp rename to lldb/test/API/tools/lldb-dap/exception/main.c index b940d07c6f2bb3..a653ac5d82aa3a 100644 --- a/lldb/test/API/tools/lldb-dap/exception/main.cpp +++ b/lldb/test/API/tools/lldb-dap/exception/main.c @@ -1,6 +1,6 @@ #include <signal.h> -int main() { +int main(int argc, char const *argv[]) { raise(SIGABRT); return 0; } diff --git a/lldb/test/API/tools/lldb-dap/exception/objc/Makefile b/lldb/test/API/tools/lldb-dap/exception/objc/Makefile new file mode 100644 index 00000000000000..9b6528337cb9d8 --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/exception/objc/Makefile @@ -0,0 +1,9 @@ +OBJC_SOURCES := main.m + +CFLAGS_EXTRAS := -w + +USE_SYSTEM_STDLIB := 1 + +LD_EXTRAS := -framework Foundation + +include Makefile.rules diff --git a/lldb/test/API/tools/lldb-dap/exception/objc/TestDAP_exception_objc.py b/lldb/test/API/tools/lldb-dap/exception/objc/TestDAP_exception_objc.py new file mode 100644 index 00000000000000..777d55f48e8504 --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/exception/objc/TestDAP_exception_objc.py @@ -0,0 +1,27 @@ +""" +Test exception behavior in DAP with obj-c throw. +""" + + +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +import lldbdap_testcase + + +class TestDAP_exception_objc(lldbdap_testcase.DAPTestCaseBase): + @skipUnlessDarwin + def test_stopped_description(self): + """ + Test that exception description is shown correctly in stopped event. + """ + program = self.getBuildArtifact("a.out") + self.build_and_launch(program) + self.dap_server.request_continue() + self.assertTrue(self.verify_stop_exception_info("signal SIGABRT")) + exception_info = self.get_exceptionInfo() + self.assertEqual(exception_info["breakMode"], "always") + self.assertEqual(exception_info["description"], "signal SIGABRT") + self.assertEqual(exception_info["exceptionId"], "signal") + exception_details = exception_info["details"] + self.assertRegex(exception_details["message"], "SomeReason") + self.assertRegex(exception_details["stackTrace"], "main.m") diff --git a/lldb/test/API/tools/lldb-dap/exception/objc/main.m b/lldb/test/API/tools/lldb-dap/exception/objc/main.m new file mode 100644 index 00000000000000..e8db04fb40de15 --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/exception/objc/main.m @@ -0,0 +1,8 @@ +#import <Foundation/Foundation.h> + +int main(int argc, char const *argv[]) { + @throw [[NSException alloc] initWithName:@"ThrownException" + reason:@"SomeReason" + userInfo:nil]; + return 0; +} diff --git a/lldb/test/API/tools/lldb-dap/extendedStackTrace/Makefile b/lldb/test/API/tools/lldb-dap/extendedStackTrace/Makefile new file mode 100644 index 00000000000000..e4ee1a0506c0cf --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/extendedStackTrace/Makefile @@ -0,0 +1,5 @@ +OBJC_SOURCES := main.m + +USE_SYSTEM_STDLIB := 1 + +include Makefile.rules diff --git a/lldb/test/API/tools/lldb-dap/extendedStackTrace/TestDAP_extendedStackTrace.py b/lldb/test/API/tools/lldb-dap/extendedStackTrace/TestDAP_extendedStackTrace.py new file mode 100644 index 00000000000000..0cc8534daf4e94 --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/extendedStackTrace/TestDAP_extendedStackTrace.py @@ -0,0 +1,104 @@ +""" +Test lldb-dap stackTrace request with an extended backtrace thread. +""" + + +import os + +import lldbdap_testcase +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +from lldbsuite.test.lldbplatformutil import * + + +class TestDAP_extendedStackTrace(lldbdap_testcase.DAPTestCaseBase): + @skipUnlessDarwin + def test_stackTrace(self): + """ + Tests the 'stackTrace' packet on a thread with an extended backtrace. + """ + backtrace_recording_lib = findBacktraceRecordingDylib() + if not backtrace_recording_lib: + self.skipTest( + "Skipped because libBacktraceRecording.dylib was present on the system." + ) + + if not os.path.isfile("/usr/lib/system/introspection/libdispatch.dylib"): + self.skipTest( + "Skipped because introspection libdispatch dylib is not present." + ) + + program = self.getBuildArtifact("a.out") + + self.build_and_launch( + program, + env=[ + "DYLD_LIBRARY_PATH=/usr/lib/system/introspection", + "DYLD_INSERT_LIBRARIES=" + backtrace_recording_lib, + ], + enableDisplayExtendedBacktrace=True, + ) + source = "main.m" + breakpoint = line_number(source, "breakpoint 1") + lines = [breakpoint] + + breakpoint_ids = self.set_source_breakpoints(source, lines) + self.assertEqual( + len(breakpoint_ids), len(lines), "expect correct number of breakpoints" + ) + + events = self.continue_to_next_stop() + + stackFrames, totalFrames = self.get_stackFrames_and_totalFramesCount( + threadId=events[0]["body"]["threadId"] + ) + self.assertGreaterEqual(len(stackFrames), 3, "expect >= 3 frames") + self.assertEqual(len(stackFrames), totalFrames) + self.assertEqual(stackFrames[0]["name"], "one") + self.assertEqual(stackFrames[1]["name"], "two") + self.assertEqual(stackFrames[2]["name"], "three") + + stackLabels = [ + (i, frame) + for i, frame in enumerate(stackFrames) + if frame.get("presentationHint", "") == "label" + ] + self.assertEqual(len(stackLabels), 2, "expected two label stack frames") + self.assertRegex( + stackLabels[0][1]["name"], + "Enqueued from com.apple.root.default-qos \(Thread \d\)", + ) + self.assertRegex( + stackLabels[1][1]["name"], + "Enqueued from com.apple.main-thread \(Thread \d\)", + ) + + for i, frame in stackLabels: + # Ensure requesting startFrame+levels across thread backtraces works as expected. + stackFrames, totalFrames = self.get_stackFrames_and_totalFramesCount( + threadId=events[0]["body"]["threadId"], startFrame=i - 1, levels=3 + ) + self.assertEqual(len(stackFrames), 3, "expected 3 frames with levels=3") + self.assertGreaterEqual( + totalFrames, i + 3, "total frames should include a pagination offset" + ) + self.assertEqual(stackFrames[1], frame) + + # Ensure requesting startFrame+levels at the beginning of a thread backtraces works as expected. + stackFrames, totalFrames = self.get_stackFrames_and_totalFramesCount( + threadId=events[0]["body"]["threadId"], startFrame=i, levels=3 + ) + self.assertEqual(len(stackFrames), 3, "expected 3 frames with levels=3") + self.assertGreaterEqual( + totalFrames, i + 3, "total frames should include a pagination offset" + ) + self.assertEqual(stackFrames[0], frame) + + # Ensure requests with startFrame+levels that end precisely on the last frame includes the totalFrames pagination offset. + stackFrames, totalFrames = self.get_stackFrames_and_totalFramesCount( + threadId=events[0]["body"]["threadId"], startFrame=i - 1, levels=1 + ) + self.assertEqual(len(stackFrames), 1, "expected 1 frames with levels=1") + self.assertGreaterEqual( + totalFrames, i, "total frames should include a pagination offset" + ) diff --git a/lldb/test/API/tools/lldb-dap/extendedStackTrace/main.m b/lldb/test/API/tools/lldb-dap/extendedStackTrace/main.m new file mode 100644 index 00000000000000..d513880236c517 --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/extendedStackTrace/main.m @@ -0,0 +1,28 @@ +#import <dispatch/dispatch.h> +#include <stdio.h> + +void one() { + printf("one...\n"); // breakpoint 1 +} + +void two() { + printf("two...\n"); + one(); +} + +void three() { + printf("three...\n"); + two(); +} + +int main(int argc, char *argv[]) { + printf("main...\n"); + // Nest from main queue > global queue > main queue. + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), + ^{ + dispatch_async(dispatch_get_main_queue(), ^{ + three(); + }); + }); + dispatch_main(); +} diff --git a/lldb/test/API/tools/lldb-dap/stackTrace/TestDAP_stackTrace.py b/lldb/test/API/tools/lldb-dap/stackTrace/TestDAP_stackTrace.py index 0d7776faa4a9de..56ed1ebdf7ab44 100644 --- a/lldb/test/API/tools/lldb-dap/stackTrace/TestDAP_stackTrace.py +++ b/lldb/test/API/tools/lldb-dap/stackTrace/TestDAP_stackTrace.py @@ -1,13 +1,11 @@ """ -Test lldb-dap setBreakpoints request +Test lldb-dap stackTrace request """ import os -import dap_server import lldbdap_testcase -from lldbsuite.test import lldbutil from lldbsuite.test.decorators import * from lldbsuite.test.lldbtest import * @@ -17,11 +15,14 @@ class TestDAP_stackTrace(lldbdap_testcase.DAPTestCaseBase): source_key_path = ["source", "path"] line_key_path = ["line"] + # stackTrace additioanl frames for paginated traces + page_size = 20 + def verify_stackFrames(self, start_idx, stackFrames): frame_idx = start_idx for stackFrame in stackFrames: # Don't care about frame above main - if frame_idx > 20: + if frame_idx > 40: return self.verify_stackFrame(frame_idx, stackFrame) frame_idx += 1 @@ -33,7 +34,7 @@ def verify_stackFrame(self, frame_idx, stackFrame): if frame_idx == 0: expected_line = self.recurse_end expected_name = "recurse" - elif frame_idx < 20: + elif frame_idx < 40: expected_line = self.recurse_call expected_name = "recurse" else: @@ -83,10 +84,24 @@ def test_stackTrace(self): (stackFrames, totalFrames) = self.get_stackFrames_and_totalFramesCount() frameCount = len(stackFrames) self.assertGreaterEqual( - frameCount, 20, "verify we get at least 20 frames for all frames" + frameCount, 40, "verify we get at least 40 frames for all frames" + ) + self.assertEqual( + totalFrames, + frameCount, + "verify total frames returns a speculative page size", ) + self.verify_stackFrames(startFrame, stackFrames) + + # Verify totalFrames contains a speculative page size of additional frames with startFrame = 0 and levels = 0 + (stackFrames, totalFrames) = self.get_stackFrames_and_totalFramesCount( + startFrame=0, levels=10 + ) + self.assertEqual(len(stackFrames), 10, "verify we get levels=10 frames") self.assertEqual( - totalFrames, frameCount, "verify we get correct value for totalFrames count" + totalFrames, + len(stackFrames) + self.page_size, + "verify total frames returns a speculative page size", ) self.verify_stackFrames(startFrame, stackFrames) diff --git a/lldb/test/API/tools/lldb-dap/stackTrace/main.c b/lldb/test/API/tools/lldb-dap/stackTrace/main.c index 862473a3e6ac8c..25d81be08e7782 100644 --- a/lldb/test/API/tools/lldb-dap/stackTrace/main.c +++ b/lldb/test/API/tools/lldb-dap/stackTrace/main.c @@ -8,6 +8,6 @@ int recurse(int x) { } int main(int argc, char const *argv[]) { - recurse(20); // recurse invocation + recurse(40); // recurse invocation return 0; } diff --git a/lldb/test/API/tools/lldb-dap/stackTraceMissingFunctionName/TestDAP_stackTraceMissingFunctionName.py b/lldb/test/API/tools/lldb-dap/stackTraceMissingFunctionName/TestDAP_stackTraceMissingFunctionName.py index a04c752764fbb2..f2131d6a821217 100644 --- a/lldb/test/API/tools/lldb-dap/stackTraceMissingFunctionName/TestDAP_stackTraceMissingFunctionName.py +++ b/lldb/test/API/tools/lldb-dap/stackTraceMissingFunctionName/TestDAP_stackTraceMissingFunctionName.py @@ -2,13 +2,8 @@ Test lldb-dap stack trace response """ - -import dap_server from lldbsuite.test.decorators import * -import os - import lldbdap_testcase -from lldbsuite.test import lldbtest, lldbutil class TestDAP_stackTraceMissingFunctionName(lldbdap_testcase.DAPTestCaseBase): diff --git a/lldb/tools/lldb-dap/JSONUtils.cpp b/lldb/tools/lldb-dap/JSONUtils.cpp index 7338e7cf41eb03..342859adef214f 100644 --- a/lldb/tools/lldb-dap/JSONUtils.cpp +++ b/lldb/tools/lldb-dap/JSONUtils.cpp @@ -769,6 +769,28 @@ llvm::json::Value CreateStackFrame(lldb::SBFrame &frame) { return llvm::json::Value(std::move(object)); } +llvm::json::Value CreateExtendedStackFrameLabel(lldb::SBThread &thread) { + std::string name; + lldb::SBStream stream; + if (g_dap.thread_format && + thread.GetDescriptionWithFormat(g_dap.thread_format, stream).Success()) { + name = stream.GetData(); + } else { + const uint32_t thread_idx = thread.GetExtendedBacktraceOriginatingIndexID(); + const char *queue_name = thread.GetQueueName(); + if (queue_name != nullptr) { + name = llvm::formatv("Enqueued from {0} (Thread {1})", queue_name, + thread_idx); + } else { + name = llvm::formatv("Thread {0}", thread_idx); + } + } + + return llvm::json::Value(llvm::json::Object{{"id", thread.GetThreadID() + 1}, + {"name", name}, + {"presentationHint", "label"}}); +} + // Response to `setInstructionBreakpoints` request. // "Breakpoint": { // "type": "object", diff --git a/lldb/tools/lldb-dap/JSONUtils.h b/lldb/tools/lldb-dap/JSONUtils.h index b6356630b72682..f8fec22d7aa0ea 100644 --- a/lldb/tools/lldb-dap/JSONUtils.h +++ b/lldb/tools/lldb-dap/JSONUtils.h @@ -322,6 +322,25 @@ llvm::json::Value CreateSource(llvm::StringRef source_path); /// definition outlined by Microsoft. llvm::json::Value CreateStackFrame(lldb::SBFrame &frame); +/// Create a "StackFrame" label object for a LLDB thread. +/// +/// This function will fill in the following keys in the returned +/// object: +/// "id" - the thread ID as an integer +/// "name" - the thread name as a string which combines the LLDB +/// thread index ID along with the string name of the thread +/// from the OS if it has a name. +/// "presentationHint" - "label" +/// +/// \param[in] thread +/// The LLDB thread to use when populating out the "Thread" +/// object. +/// +/// \return +/// A "StackFrame" JSON object with that follows the formal JSON +/// definition outlined by Microsoft. +llvm::json::Value CreateExtendedStackFrameLabel(lldb::SBThread &thread); + /// Create a "instruction" object for a LLDB disassemble object as described in /// the Visual Studio Code debug adaptor definition. /// diff --git a/lldb/tools/lldb-dap/README.md b/lldb/tools/lldb-dap/README.md index 11a14d29ab51e2..ddc2017e843dc2 100644 --- a/lldb/tools/lldb-dap/README.md +++ b/lldb/tools/lldb-dap/README.md @@ -36,6 +36,9 @@ file that defines how your program will be run. The JSON configuration file can |**terminateCommands** |[string]| | LLDB commands executed when the debugging session ends. Commands and command output will be sent to the debugger console when they are executed. |**sourceMap** |[string[2]]| | Specify an array of path re-mappings. Each element in the array must be a two element array containing a source and destination pathname. |**debuggerRoot** | string| |Specify a working directory to use when launching lldb-dap. If the debug information in your executable contains relative paths, this option can be used so that `lldb-dap` can find source files and object files that have relative paths. +|**enableAutoVariableSummaries**|bool| | Enable auto generated summaries for variables when no summaries exist for a given type. This feature can cause performance delays in large projects when viewing variables. +|**enableDisplayExtendedBacktrace**|bool| | Enable language specific extended backtraces. +|**enableSyntheticChildDebugging**|bool| | If a variable is displayed using a synthetic children, also display the actual contents of the variable at the end under a [raw] entry. This is useful when creating sythetic child plug-ins as it lets you see the actual contents of the variable. ### Attaching Settings @@ -62,6 +65,9 @@ The JSON configuration file can contain the following `lldb-dap` specific launch |**exitCommands** |[string]| | LLDB commands executed when the program exits. Commands and command output will be sent to the debugger console when they are executed. |**terminateCommands** |[string]| | LLDB commands executed when the debugging session ends. Commands and command output will be sent to the debugger console when they are executed. |**attachCommands** |[string]| | LLDB commands that will be executed after **preRunCommands** which take place of the code that normally does the attach. The commands can create a new target and attach or launch it however desired. This allows custom launch and attach configurations. Core files can use `target create --core /path/to/core` to attach to core files. +|**enableAutoVariableSummaries**|bool| | Enable auto generated summaries for variables when no summaries exist for a given type. This feature can cause performance delays in large projects when viewing variables. +|**enableDisplayExtendedBacktrace**|bool| | Enable language specific extended backtraces. +|**enableSyntheticChildDebugging**|bool| | If a variable is displayed using a synthetic children, also display the actual contents of the variable at the end under a [raw] entry. This is useful when creating sythetic child plug-ins as it lets you see the actual contents of the variable. ### Example configurations diff --git a/lldb/tools/lldb-dap/lldb-dap.cpp b/lldb/tools/lldb-dap/lldb-dap.cpp index c5c4b09f15622b..51765cd28df8a6 100644 --- a/lldb/tools/lldb-dap/lldb-dap.cpp +++ b/lldb/tools/lldb-dap/lldb-dap.cpp @@ -112,6 +112,9 @@ typedef void (*RequestCallback)(const llvm::json::Object &command); enum LaunchMethod { Launch, Attach, AttachForSuspendedLaunch }; +/// Page size used for reporting addtional frames in the 'stackTrace' request. +constexpr int StackPageSize = 20; + /// Prints a welcome message on the editor if the preprocessor variable /// LLDB_DAP_WELCOME_MESSAGE is defined. static void PrintWelcomeMessage() { @@ -638,6 +641,79 @@ void SetSourceMapFromArguments(const llvm::json::Object &arguments) { } } +// Fill in the stack frames of the thread. +// +// Threads stacks may contain runtime specific extended backtraces, when +// constructing a stack trace first report the full thread stack trace then +// perform a breadth first traversal of any extended backtrace frames. +// +// For example: +// +// Thread (id=th0) stack=[s0, s1, s2, s3] +// \ Extended backtrace "libdispatch" Thread (id=th1) stack=[s0, s1] +// \ Extended backtrace "libdispatch" Thread (id=th2) stack=[s0, s1] +// \ Extended backtrace "Application Specific Backtrace" Thread (id=th3) +// stack=[s0, s1, s2] +// +// Which will flatten into: +// +// 0. th0->s0 +// 1. th0->s1 +// 2. th0->s2 +// 3. th0->s3 +// 4. label - Enqueued from th1, sf=-1, i=-4 +// 5. th1->s0 +// 6. th1->s1 +// 7. label - Enqueued from th2 +// 8. th2->s0 +// 9. th2->s1 +// 10. label - Application Specific Backtrace +// 11. th3->s0 +// 12. th3->s1 +// 13. th3->s2 +// +// s=3,l=3 = [th0->s3, label1, th1->s0] +bool FillStackFrames(lldb::SBThread &thread, llvm::json::Array &stack_frames, + int64_t &offset, const int64_t start_frame, + const int64_t levels) { + bool reached_end_of_stack = false; + for (int64_t i = start_frame; + static_cast<int64_t>(stack_frames.size()) < levels; i++) { + if (i == -1) { + stack_frames.emplace_back(CreateExtendedStackFrameLabel(thread)); + continue; + } + + lldb::SBFrame frame = thread.GetFrameAtIndex(i); + if (!frame.IsValid()) { + offset += thread.GetNumFrames() + 1 /* label between threads */; + reached_end_of_stack = true; + break; + } + + stack_frames.emplace_back(CreateStackFrame(frame)); + } + + if (g_dap.enable_display_extended_backtrace && reached_end_of_stack) { + // Check for any extended backtraces. + for (uint32_t bt = 0; + bt < thread.GetProcess().GetNumExtendedBacktraceTypes(); bt++) { + lldb::SBThread backtrace = thread.GetExtendedBacktraceThread( + thread.GetProcess().GetExtendedBacktraceTypeAtIndex(bt)); + if (!backtrace.IsValid()) + continue; + + reached_end_of_stack = FillStackFrames( + backtrace, stack_frames, offset, + (start_frame - offset) > 0 ? start_frame - offset : -1, levels); + if (static_cast<int64_t>(stack_frames.size()) >= levels) + break; + } + } + + return reached_end_of_stack; +} + // "AttachRequest": { // "allOf": [ { "$ref": "#/definitions/Request" }, { // "type": "object", @@ -1020,6 +1096,103 @@ void request_disconnect(const llvm::json::Object &request) { g_dap.disconnecting = true; } +// "ExceptionInfoRequest": { +// "allOf": [ { "$ref": "#/definitions/Request" }, { +// "type": "object", +// "description": "Retrieves the details of the exception that +// caused this event to be raised. Clients should only call this request if +// the corresponding capability `supportsExceptionInfoRequest` is true.", +// "properties": { +// "command": { +// "type": "string", +// "enum": [ "exceptionInfo" ] +// }, +// "arguments": { +// "$ref": "#/definitions/ExceptionInfoArguments" +// } +// }, +// "required": [ "command", "arguments" ] +// }] +// }, +// "ExceptionInfoArguments": { +// "type": "object", +// "description": "Arguments for `exceptionInfo` request.", +// "properties": { +// "threadId": { +// "type": "integer", +// "description": "Thread for which exception information should be +// retrieved." +// } +// }, +// "required": [ "threadId" ] +// }, +// "ExceptionInfoResponse": { +// "allOf": [ { "$ref": "#/definitions/Response" }, { +// "type": "object", +// "description": "Response to `exceptionInfo` request.", +// "properties": { +// "body": { +// "type": "object", +// "properties": { +// "exceptionId": { +// "type": "string", +// "description": "ID of the exception that was thrown." +// }, +// "description": { +// "type": "string", +// "description": "Descriptive text for the exception." +// }, +// "breakMode": { +// "$ref": "#/definitions/ExceptionBreakMode", +// "description": "Mode that caused the exception notification to +// be raised." +// }, +// "details": { +// "$ref": "#/definitions/ExceptionDetails", +// "description": "Detailed information about the exception." +// } +// }, +// "required": [ "exceptionId", "breakMode" ] +// } +// }, +// "required": [ "body" ] +// }] +// } +// "ExceptionDetails": { +// "type": "object", +// "description": "Detailed information about an exception that has +// occurred.", "properties": { +// "message": { +// "type": "string", +// "description": "Message contained in the exception." +// }, +// "typeName": { +// "type": "string", +// "description": "Short type name of the exception object." +// }, +// "fullTypeName": { +// "type": "string", +// "description": "Fully-qualified type name of the exception object." +// }, +// "evaluateName": { +// "type": "string", +// "description": "An expression that can be evaluated in the current +// scope to obtain the exception object." +// }, +// "stackTrace": { +// "type": "string", +// "description": "Stack trace at the time the exception was thrown." +// }, +// "innerException": { +// "type": "array", +// "items": { +// "$ref": "#/definitions/ExceptionDetails" +// }, +// "description": "Details of the exception contained by this exception, +// if any." +// } +// } +// }, void request_exceptionInfo(const llvm::json::Object &request) { llvm::json::Object response; FillResponse(request, response); @@ -1048,6 +1221,27 @@ void request_exceptionInfo(const llvm::json::Object &request) { } } body.try_emplace("breakMode", "always"); + auto exception = thread.GetCurrentException(); + if (exception.IsValid()) { + llvm::json::Object details; + lldb::SBStream stream; + if (exception.GetDescription(stream)) { + EmplaceSafeString(details, "message", stream.GetData()); + } + + auto exceptionBacktrace = thread.GetCurrentExceptionBacktrace(); + if (exceptionBacktrace.IsValid()) { + lldb::SBStream stream; + exceptionBacktrace.GetDescription(stream); + for (uint32_t i = 0; i < exceptionBacktrace.GetNumFrames(); i++) { + lldb::SBFrame frame = exceptionBacktrace.GetFrameAtIndex(i); + frame.GetDescription(stream); + } + EmplaceSafeString(details, "stackTrace", stream.GetData()); + } + + body.try_emplace("details", std::move(details)); + } // auto excInfoCount = thread.GetStopReasonDataCount(); // for (auto i=0; i<excInfoCount; ++i) { // uint64_t exc_data = thread.GetStopReasonDataAtIndex(i); @@ -3066,7 +3260,9 @@ void request_source(const llvm::json::Object &request) { // }, // "format": { // "$ref": "#/definitions/StackFrameFormat", -// "description": "Specifies details on how to format the stack frames." +// "description": "Specifies details on how to format the stack frames. +// The attribute is only honored by a debug adapter if the corresponding +// capability `supportsValueFormattingOptions` is true." // } // }, // "required": [ "threadId" ] @@ -3074,7 +3270,7 @@ void request_source(const llvm::json::Object &request) { // "StackTraceResponse": { // "allOf": [ { "$ref": "#/definitions/Response" }, { // "type": "object", -// "description": "Response to 'stackTrace' request.", +// "description": "Response to `stackTrace` request.", // "properties": { // "body": { // "type": "object", @@ -3090,7 +3286,13 @@ void request_source(const llvm::json::Object &request) { // }, // "totalFrames": { // "type": "integer", -// "description": "The total number of frames available." +// "description": "The total number of frames available in the +// stack. If omitted or if `totalFrames` is larger than the +// available frames, a client is expected to request frames until +// a request returns less frames than requested (which indicates +// the end of the stack). Returning monotonically increasing +// `totalFrames` values for subsequent requests can be used to +// enforce paging in the client." // } // }, // "required": [ "stackFrames" ] @@ -3105,85 +3307,22 @@ void request_stackTrace(const llvm::json::Object &request) { lldb::SBError error; auto arguments = request.getObject("arguments"); lldb::SBThread thread = g_dap.GetLLDBThread(*arguments); - llvm::json::Array stackFrames; + llvm::json::Array stack_frames; llvm::json::Object body; if (thread.IsValid()) { - const auto startFrame = GetUnsigned(arguments, "startFrame", 0); + const auto start_frame = GetUnsigned(arguments, "startFrame", 0); const auto levels = GetUnsigned(arguments, "levels", 0); - const auto endFrame = (levels == 0) ? INT64_MAX : (startFrame + levels); - auto totalFrames = thread.GetNumFrames(); - - // This will always return an invalid thread when - // libBacktraceRecording.dylib is not loaded or if there is no extended - // backtrace. - lldb::SBThread queue_backtrace_thread; - if (g_dap.enable_display_extended_backtrace) - queue_backtrace_thread = thread.GetExtendedBacktraceThread("libdispatch"); - if (queue_backtrace_thread.IsValid()) { - // One extra frame as a label to mark the enqueued thread. - totalFrames += queue_backtrace_thread.GetNumFrames() + 1; - } - - // This will always return an invalid thread when there is no exception in - // the current thread. - lldb::SBThread exception_backtrace_thread; - if (g_dap.enable_display_extended_backtrace) - exception_backtrace_thread = thread.GetCurrentExceptionBacktrace(); - - if (exception_backtrace_thread.IsValid()) { - // One extra frame as a label to mark the exception thread. - totalFrames += exception_backtrace_thread.GetNumFrames() + 1; - } - - for (uint32_t i = startFrame; i < endFrame; ++i) { - lldb::SBFrame frame; - std::string prefix; - if (i < thread.GetNumFrames()) { - frame = thread.GetFrameAtIndex(i); - } else if (queue_backtrace_thread.IsValid() && - i < (thread.GetNumFrames() + - queue_backtrace_thread.GetNumFrames() + 1)) { - if (i == thread.GetNumFrames()) { - const uint32_t thread_idx = - queue_backtrace_thread.GetExtendedBacktraceOriginatingIndexID(); - const char *queue_name = queue_backtrace_thread.GetQueueName(); - auto name = llvm::formatv("Enqueued from {0} (Thread {1})", - queue_name, thread_idx); - stackFrames.emplace_back( - llvm::json::Object{{"id", thread.GetThreadID() + 1}, - {"name", name}, - {"presentationHint", "label"}}); - continue; - } - frame = queue_backtrace_thread.GetFrameAtIndex( - i - thread.GetNumFrames() - 1); - } else if (exception_backtrace_thread.IsValid()) { - if (i == thread.GetNumFrames() + - (queue_backtrace_thread.IsValid() - ? queue_backtrace_thread.GetNumFrames() + 1 - : 0)) { - stackFrames.emplace_back( - llvm::json::Object{{"id", thread.GetThreadID() + 2}, - {"name", "Original Exception Backtrace"}, - {"presentationHint", "label"}}); - continue; - } - - frame = exception_backtrace_thread.GetFrameAtIndex( - i - thread.GetNumFrames() - - (queue_backtrace_thread.IsValid() - ? queue_backtrace_thread.GetNumFrames() + 1 - : 0)); - } - if (!frame.IsValid()) - break; - stackFrames.emplace_back(CreateStackFrame(frame)); - } - - body.try_emplace("totalFrames", totalFrames); + int64_t offset = 0; + bool reached_end_of_stack = + FillStackFrames(thread, stack_frames, offset, start_frame, + levels == 0 ? INT64_MAX : levels); + body.try_emplace("totalFrames", + start_frame + stack_frames.size() + + (reached_end_of_stack ? 0 : StackPageSize)); } - body.try_emplace("stackFrames", std::move(stackFrames)); + + body.try_emplace("stackFrames", std::move(stack_frames)); response.try_emplace("body", std::move(body)); g_dap.SendJSON(llvm::json::Value(std::move(response))); } @@ -3487,7 +3626,6 @@ void request_stepOut(const llvm::json::Object &request) { // }] // } void request_threads(const llvm::json::Object &request) { - lldb::SBProcess process = g_dap.target.GetProcess(); llvm::json::Object response; FillResponse(request, response); diff --git a/lldb/tools/lldb-dap/package.json b/lldb/tools/lldb-dap/package.json index 6c7885fa81f7c4..d35accfb6ec4e8 100644 --- a/lldb/tools/lldb-dap/package.json +++ b/lldb/tools/lldb-dap/package.json @@ -237,7 +237,12 @@ }, "exitCommands": { "type": "array", - "description": "Commands executed at the end of debugging session.", + "description": "Commands executed when the program exits.", + "default": [] + }, + "terminateCommands": { + "type": "array", + "description": "Commands executed when the debugging session ends.", "default": [] }, "runInTerminal": { @@ -254,6 +259,11 @@ "description": "Enable auto generated summaries for variables when no summaries exist for a given type. This feature can cause performance delays in large projects when viewing variables.", "default": false }, + "enableDisplayExtendedBacktrace": { + "type": "boolean", + "description": "Enable language specific extended backtraces.", + "default": false + }, "enableSyntheticChildDebugging": { "type": "boolean", "description": "If a variable is displayed using a synthetic children, also display the actual contents of the variable at the end under a [raw] entry. This is useful when creating sythetic child plug-ins as it lets you see the actual contents of the variable.", @@ -342,7 +352,12 @@ }, "exitCommands": { "type": "array", - "description": "Commands executed at the end of debugging session.", + "description": "Commands executed when the program exits.", + "default": [] + }, + "terminateCommands": { + "type": "array", + "description": "Commands executed when the debugging session ends.", "default": [] }, "coreFile": { @@ -369,6 +384,11 @@ "description": "Enable auto generated summaries for variables when no summaries exist for a given type. This feature can cause performance delays in large projects when viewing variables.", "default": false }, + "enableDisplayExtendedBacktrace": { + "type": "boolean", + "description": "Enable language specific extended backtraces.", + "default": false + }, "enableSyntheticChildDebugging": { "type": "boolean", "description": "If a variable is displayed using a synthetic children, also display the actual contents of the variable at the end under a [raw] entry. This is useful when creating sythetic child plug-ins as it lets you see the actual contents of the variable.", _______________________________________________ lldb-commits mailing list lldb-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits