[lldb] add a marker around hidden frames#181143
Conversation
**This patch adds a marker to make hidden frames more explicit.** --- Hidden frames can be confusing for some users, who see that the indexes of the frames in a backtrace are not contiguous. This patch aims to lessen the confusion by adding a delimiter for the first and last non hidden frame, i.e the boundaries. IDE's like Xcode and VSCode represent those in the UI by having the hidden frames either greyed out or collapsed. It's not possible to do this in the CLI, therefore, this patch makes use of 2 unicode characters to mark the beginning and end of the hidden frames range. This patch depends on: - llvm#168603 # Examples In the example below, frame `llvm#2` to `llvm#7` are is hidden, and therefore, frame `llvm#1` is the first non hidden frame of the range while frame `llvm#8` is the last non hidden frame: <img width="488" height="112" alt="Screenshot 2025-11-18 at 18 41 11" src="https://github.com/user-attachments/assets/a21431da-9729-4cf0-a6bc-024aa306fc45" /> If the selected frame is one of the 2 boundary frames, we replace the delimiter character with the select character (`*`). <img width="487" height="111" alt="Screenshot 2025-11-18 at 18 41 03" src="https://github.com/user-attachments/assets/5616fa81-6db6-457d-9d1e-bbe46e710c26" /> <img width="488" height="111" alt="Screenshot 2025-11-18 at 18 40 55" src="https://github.com/user-attachments/assets/93dfa6cf-0956-4718-b31c-f965ec72b56d" />
|
@llvm/pr-subscribers-lldb Author: Charles Zablit (charles-zablit) ChangesThis is a reland of #167550. Instead of relying on libcpp for testing, we emulate our own hidden frames. This was originally causing tests failures on Windows. Full diff: https://github.com/llvm/llvm-project/pull/181143.diff 12 Files Affected:
diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h
index 87a57f7f1a538..a38caa7ac594e 100644
--- a/lldb/include/lldb/Core/Debugger.h
+++ b/lldb/include/lldb/Core/Debugger.h
@@ -347,6 +347,8 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
bool SetUseSourceCache(bool use_source_cache);
+ bool GetMarkHiddenFrames() const;
+
bool GetHighlightSource() const;
lldb::StopShowColumn GetStopShowColumn() const;
diff --git a/lldb/include/lldb/Target/StackFrame.h b/lldb/include/lldb/Target/StackFrame.h
index 5cba9afe2a7e8..17563ee6ada98 100644
--- a/lldb/include/lldb/Target/StackFrame.h
+++ b/lldb/include/lldb/Target/StackFrame.h
@@ -363,7 +363,7 @@ class StackFrame : public ExecutionContextScope,
/// \param [in] frame_marker
/// Optional string that will be prepended to the frame output description.
virtual void DumpUsingSettingsFormat(Stream *strm, bool show_unique = false,
- const char *frame_marker = nullptr);
+ const llvm::StringRef frame_marker = "");
/// Print a description for this frame using a default format.
///
@@ -400,7 +400,7 @@ class StackFrame : public ExecutionContextScope,
/// Returns true if successful.
virtual bool GetStatus(Stream &strm, bool show_frame_info, bool show_source,
bool show_unique = false,
- const char *frame_marker = nullptr);
+ const llvm::StringRef frame_marker = "");
/// Query whether this frame is a concrete frame on the call stack, or if it
/// is an inlined frame derived from the debug information and presented by
diff --git a/lldb/include/lldb/Target/StackFrameList.h b/lldb/include/lldb/Target/StackFrameList.h
index 715781abb83a3..223aa3ddab731 100644
--- a/lldb/include/lldb/Target/StackFrameList.h
+++ b/lldb/include/lldb/Target/StackFrameList.h
@@ -50,6 +50,22 @@ class StackFrameList : public std::enable_shared_from_this<StackFrameList> {
/// Resets the selected frame index of this object.
void ClearSelectedFrameIndex();
+ /// Returns \p true if the next frame is hidden.
+ bool IsNextFrameHidden(lldb_private::StackFrame &frame);
+
+ /// Returns \p true if the previous frame is hidden.
+ bool IsPreviousFrameHidden(lldb_private::StackFrame &frame);
+
+ /// Returns the stack frame marker depending on if \p frame_sp:
+ /// @li is selected: *
+ /// @li is the first non hidden frame: ﹍
+ /// @li is the last non hidden frame: ﹉
+ ///
+ /// If the terminal does not support Unicode rendering, the hidden frame
+ /// markers are replaced with whitespaces.
+ std::string FrameMarker(lldb::StackFrameSP frame_sp,
+ lldb::StackFrameSP selected_frame_sp);
+
/// Get the currently selected frame index.
/// We should only call SelectMostRelevantFrame if (a) the user hasn't already
/// selected a frame, and (b) if this really is a user facing
@@ -97,7 +113,8 @@ class StackFrameList : public std::enable_shared_from_this<StackFrameList> {
size_t GetStatus(Stream &strm, uint32_t first_frame, uint32_t num_frames,
bool show_frame_info, uint32_t num_frames_with_source,
bool show_unique = false, bool show_hidden = false,
- const char *frame_marker = nullptr);
+ bool show_hidden_marker = true,
+ bool show_selected_frame = false);
/// Returns whether we have currently fetched all the frames of a stack.
bool WereAllFramesFetched() const;
diff --git a/lldb/packages/Python/lldbsuite/test/decorators.py b/lldb/packages/Python/lldbsuite/test/decorators.py
index df0b2cba4c573..b4658b149af90 100644
--- a/lldb/packages/Python/lldbsuite/test/decorators.py
+++ b/lldb/packages/Python/lldbsuite/test/decorators.py
@@ -442,6 +442,35 @@ def impl(func):
return impl
+def unicode_test(func):
+ """Decorate the item as a test which requires Unicode to be enabled.
+
+ lldb checks the value of the `LANG` environment variable for the substring "utf-8"
+ to determine if the terminal supports Unicode (except on Windows, were we assume
+ it's always supported).
+ This decorator sets LANG to `utf-8` before running the test and resets it to its
+ previous value afterwards.
+ """
+
+ def unicode_wrapped(*args, **kwargs):
+ import os
+
+ previous_lang = os.environ.get("LANG", None)
+ os.environ["LANG"] = "en_US.UTF-8"
+ try:
+ func(*args, **kwargs)
+ except Exception as err:
+ raise err
+ finally:
+ # Reset the value, whether the test failed or not.
+ if previous_lang is not None:
+ os.environ["LANG"] = previous_lang
+ else:
+ del os.environ["LANG"]
+
+ return unicode_wrapped
+
+
def no_debug_info_test(func):
"""Decorate the item as a test what don't use any debug info. If this annotation is specified
then the test runner won't generate a separate test for each debug info format."""
diff --git a/lldb/source/Core/CoreProperties.td b/lldb/source/Core/CoreProperties.td
index 383834eea22a5..e029da6467fce 100644
--- a/lldb/source/Core/CoreProperties.td
+++ b/lldb/source/Core/CoreProperties.td
@@ -128,6 +128,10 @@ let Definition = "debugger", Path = "" in {
Global,
DefaultTrue,
Desc<"If true, LLDB will highlight the displayed source code.">;
+ def MarkHiddenFrames: Property<"mark-hidden-frames", "Boolean">,
+ Global,
+ DefaultTrue,
+ Desc<"If true, LLDB will add a marker to delimit hidden frames in backtraces.">;
def StopShowColumn: Property<"stop-show-column", "Enum">,
DefaultEnumValue<"eStopShowColumnAnsiOrCaret">,
EnumValues<"OptionEnumValues(s_stop_show_column_values)">,
diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp
index 00dea7da3497e..12f8039da947e 100644
--- a/lldb/source/Core/Debugger.cpp
+++ b/lldb/source/Core/Debugger.cpp
@@ -612,6 +612,13 @@ bool Debugger::SetUseSourceCache(bool b) {
}
return ret;
}
+
+bool Debugger::GetMarkHiddenFrames() const {
+ const uint32_t idx = ePropertyMarkHiddenFrames;
+ return GetPropertyAtIndexAs<bool>(
+ idx, g_debugger_properties[idx].default_uint_value != 0);
+}
+
bool Debugger::GetHighlightSource() const {
const uint32_t idx = ePropertyHighlightSource;
return GetPropertyAtIndexAs<bool>(
diff --git a/lldb/source/Target/StackFrame.cpp b/lldb/source/Target/StackFrame.cpp
index 340607e14abed..3ef96a46517c9 100644
--- a/lldb/source/Target/StackFrame.cpp
+++ b/lldb/source/Target/StackFrame.cpp
@@ -1945,7 +1945,7 @@ bool StackFrame::DumpUsingFormat(Stream &strm,
}
void StackFrame::DumpUsingSettingsFormat(Stream *strm, bool show_unique,
- const char *frame_marker) {
+ const llvm::StringRef frame_marker) {
if (strm == nullptr)
return;
@@ -2044,7 +2044,8 @@ bool StackFrame::HasCachedData() const {
}
bool StackFrame::GetStatus(Stream &strm, bool show_frame_info, bool show_source,
- bool show_unique, const char *frame_marker) {
+ bool show_unique,
+ const llvm::StringRef frame_marker) {
if (show_frame_info) {
strm.Indent();
DumpUsingSettingsFormat(&strm, show_unique, frame_marker);
diff --git a/lldb/source/Target/StackFrameList.cpp b/lldb/source/Target/StackFrameList.cpp
index 4f4b06f30460b..d573d23c318fb 100644
--- a/lldb/source/Target/StackFrameList.cpp
+++ b/lldb/source/Target/StackFrameList.cpp
@@ -27,6 +27,7 @@
#include "lldb/Utility/LLDBLog.h"
#include "lldb/Utility/Log.h"
#include "llvm/ADT/SmallPtrSet.h"
+#include "llvm/Support/ConvertUTF.h"
#include <memory>
@@ -947,11 +948,43 @@ StackFrameList::GetStackFrameSPForStackFramePtr(StackFrame *stack_frame_ptr) {
return ret_sp;
}
+bool StackFrameList::IsNextFrameHidden(lldb_private::StackFrame &frame) {
+ uint32_t frame_idx = frame.GetFrameIndex();
+ StackFrameSP frame_sp = GetFrameAtIndex(frame_idx + 1);
+ if (!frame_sp)
+ return false;
+ return frame_sp->IsHidden();
+}
+
+bool StackFrameList::IsPreviousFrameHidden(lldb_private::StackFrame &frame) {
+ uint32_t frame_idx = frame.GetFrameIndex();
+ if (frame_idx == 0)
+ return false;
+ StackFrameSP frame_sp = GetFrameAtIndex(frame_idx - 1);
+ if (!frame_sp)
+ return false;
+ return frame_sp->IsHidden();
+}
+
+std::string StackFrameList::FrameMarker(lldb::StackFrameSP frame_sp,
+ lldb::StackFrameSP selected_frame_sp) {
+ if (frame_sp == selected_frame_sp)
+ return Terminal::SupportsUnicode() ? u8" * " : u8"* ";
+ else if (!Terminal::SupportsUnicode())
+ return u8" ";
+ else if (IsPreviousFrameHidden(*frame_sp))
+ return u8"﹉ ";
+ else if (IsNextFrameHidden(*frame_sp))
+ return u8"﹍ ";
+ return u8" ";
+}
+
size_t StackFrameList::GetStatus(Stream &strm, uint32_t first_frame,
uint32_t num_frames, bool show_frame_info,
uint32_t num_frames_with_source,
bool show_unique, bool show_hidden,
- const char *selected_frame_marker) {
+ bool show_hidden_marker,
+ bool show_selected_frame) {
size_t num_frames_displayed = 0;
if (num_frames == 0)
@@ -969,25 +1002,17 @@ size_t StackFrameList::GetStatus(Stream &strm, uint32_t first_frame,
StackFrameSP selected_frame_sp =
m_thread.GetSelectedFrame(DoNoSelectMostRelevantFrame);
- const char *unselected_marker = nullptr;
std::string buffer;
- if (selected_frame_marker) {
- size_t len = strlen(selected_frame_marker);
- buffer.insert(buffer.begin(), len, ' ');
- unselected_marker = buffer.c_str();
- }
- const char *marker = nullptr;
+ std::string marker;
for (frame_idx = first_frame; frame_idx < last_frame; ++frame_idx) {
frame_sp = GetFrameAtIndex(frame_idx);
if (!frame_sp)
break;
- if (selected_frame_marker != nullptr) {
- if (frame_sp == selected_frame_sp)
- marker = selected_frame_marker;
- else
- marker = unselected_marker;
- }
+ if (show_selected_frame)
+ marker = FrameMarker(frame_sp, selected_frame_sp);
+ else
+ marker = FrameMarker(frame_sp, nullptr);
// Hide uninteresting frames unless it's the selected frame.
if (!show_hidden && frame_sp != selected_frame_sp && frame_sp->IsHidden())
diff --git a/lldb/source/Target/Thread.cpp b/lldb/source/Target/Thread.cpp
index 44b664ac70d29..aa005a2b9b3db 100644
--- a/lldb/source/Target/Thread.cpp
+++ b/lldb/source/Target/Thread.cpp
@@ -1961,9 +1961,9 @@ size_t Thread::GetStatus(Stream &strm, uint32_t start_frame,
uint32_t num_frames, uint32_t num_frames_with_source,
bool stop_format, bool show_hidden, bool only_stacks) {
+ ExecutionContext exe_ctx(shared_from_this());
+ Target *target = exe_ctx.GetTargetPtr();
if (!only_stacks) {
- ExecutionContext exe_ctx(shared_from_this());
- Target *target = exe_ctx.GetTargetPtr();
Process *process = exe_ctx.GetProcessPtr();
strm.Indent();
bool is_selected = false;
@@ -1997,16 +1997,19 @@ size_t Thread::GetStatus(Stream &strm, uint32_t start_frame,
const bool show_frame_info = true;
const bool show_frame_unique = only_stacks;
- const char *selected_frame_marker = nullptr;
+ bool show_selected_frame = false;
if (num_frames == 1 || only_stacks ||
(GetID() != GetProcess()->GetThreadList().GetSelectedThread()->GetID()))
strm.IndentMore();
else
- selected_frame_marker = "* ";
+ show_selected_frame = true;
+ bool show_hidden_marker =
+ target && target->GetDebugger().GetMarkHiddenFrames();
num_frames_shown = GetStackFrameList()->GetStatus(
strm, start_frame, num_frames, show_frame_info, num_frames_with_source,
- show_frame_unique, show_hidden, selected_frame_marker);
+ show_frame_unique, show_hidden, show_hidden_marker,
+ show_selected_frame);
if (num_frames == 1)
strm.IndentLess();
strm.IndentLess();
@@ -2106,9 +2109,13 @@ size_t Thread::GetStackFrameStatus(Stream &strm, uint32_t first_frame,
uint32_t num_frames, bool show_frame_info,
uint32_t num_frames_with_source,
bool show_hidden) {
- return GetStackFrameList()->GetStatus(strm, first_frame, num_frames,
- show_frame_info, num_frames_with_source,
- /*show_unique*/ false, show_hidden);
+ ExecutionContext exe_ctx(shared_from_this());
+ Target *target = exe_ctx.GetTargetPtr();
+ bool show_hidden_marker =
+ target && target->GetDebugger().GetMarkHiddenFrames();
+ return GetStackFrameList()->GetStatus(
+ strm, first_frame, num_frames, show_frame_info, num_frames_with_source,
+ /*show_unique*/ false, show_hidden, show_hidden_marker);
}
Unwind &Thread::GetUnwinder() {
diff --git a/lldb/test/API/terminal/hidden_frame_markers/Makefile b/lldb/test/API/terminal/hidden_frame_markers/Makefile
new file mode 100644
index 0000000000000..99998b20bcb05
--- /dev/null
+++ b/lldb/test/API/terminal/hidden_frame_markers/Makefile
@@ -0,0 +1,3 @@
+CXX_SOURCES := main.cpp
+
+include Makefile.rules
diff --git a/lldb/test/API/terminal/hidden_frame_markers/TestHiddenFrameMarkers.py b/lldb/test/API/terminal/hidden_frame_markers/TestHiddenFrameMarkers.py
new file mode 100644
index 0000000000000..178d97fce17c2
--- /dev/null
+++ b/lldb/test/API/terminal/hidden_frame_markers/TestHiddenFrameMarkers.py
@@ -0,0 +1,97 @@
+"""
+Test that hidden frames are delimited with markers.
+"""
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class HiddenFrameMarkerTest(TestBase):
+ @unicode_test
+ def test_hidden_frame_markers(self):
+ """Test that hidden frame markers are rendered in backtraces"""
+ self.build()
+ lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.cpp")
+ )
+ self.expect(
+ "bt",
+ substrs=[
+ " * frame #0:",
+ " ﹍ frame #1:",
+ " ﹉ frame #7:",
+ " frame #8:",
+ " frame #9:",
+ ],
+ )
+
+ self.runCmd("f 1")
+ self.expect(
+ "bt",
+ substrs=[
+ " frame #0:",
+ " * frame #1:",
+ " ﹉ frame #7:",
+ " frame #8:",
+ " frame #9:",
+ ],
+ )
+
+ self.runCmd("f 7")
+ self.expect(
+ "bt",
+ substrs=[
+ " frame #0:",
+ " ﹍ frame #1:",
+ " * frame #7:",
+ " frame #8:",
+ " frame #9:",
+ ],
+ )
+
+ def test_hidden_frame_markers(self):
+ """
+ Test that hidden frame markers are not rendered in backtraces when
+ mark-hidden-frames is set to false
+ """
+ self.build()
+ self.runCmd("settings set mark-hidden-frames 0")
+ lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.cpp")
+ )
+ self.expect(
+ "bt",
+ substrs=[
+ " * frame #0:",
+ " frame #1:",
+ " frame #7:",
+ " frame #8:",
+ " frame #9:",
+ ],
+ )
+
+ self.runCmd("f 1")
+ self.expect(
+ "bt",
+ substrs=[
+ " frame #0:",
+ " * frame #1:",
+ " frame #7:",
+ " frame #8:",
+ " frame #9:",
+ ],
+ )
+
+ self.runCmd("f 7")
+ self.expect(
+ "bt",
+ substrs=[
+ " frame #0:",
+ " frame #1:",
+ " * frame #7:",
+ " frame #8:",
+ " frame #9:",
+ ],
+ )
diff --git a/lldb/test/API/terminal/hidden_frame_markers/main.cpp b/lldb/test/API/terminal/hidden_frame_markers/main.cpp
new file mode 100644
index 0000000000000..c0b7e0884538a
--- /dev/null
+++ b/lldb/test/API/terminal/hidden_frame_markers/main.cpp
@@ -0,0 +1,12 @@
+#include <functional>
+#include <iostream>
+
+static void target() {
+ int a = 0; // break here
+}
+
+int main() {
+ std::function<void()> fn = [] { target(); };
+ fn();
+ return 0;
+}
|
| lldb::StackFrameSP selected_frame_sp) { | ||
| if (frame_sp == selected_frame_sp) | ||
| return Terminal::SupportsUnicode() ? u8" * " : u8"* "; | ||
| else if (!Terminal::SupportsUnicode()) |
| /// | ||
| /// If the terminal does not support Unicode rendering, the hidden frame | ||
| /// markers are replaced with whitespaces. | ||
| std::string FrameMarker(lldb::StackFrameSP frame_sp, |
There was a problem hiding this comment.
nit: GetFrameMarker maybe ?
| return frame_sp->IsHidden(); | ||
| } | ||
|
|
||
| std::string StackFrameList::FrameMarker(lldb::StackFrameSP frame_sp, |
|
Also I'd rename this PR because IIUC the marker is around hidden frames not before |
| @@ -0,0 +1,12 @@ | |||
| #include <functional> | |||
There was a problem hiding this comment.
we're still relying on libc++ headers here. Is there going to be a follow-up commit? or am i missing something?
There was a problem hiding this comment.
I forgot to push the latest commit. The updated version does not rely on libc++ anymore.
| def test_hidden_frame_markers(self): | ||
| """Test that hidden frame markers are rendered in backtraces""" | ||
| self.build() | ||
| lldbutil.run_to_source_breakpoint( |
There was a problem hiding this comment.
One additional test-case we'd want is a backtrace with 2 sets of hidden frames. E.g., the target() method calling into another std::function
There was a problem hiding this comment.
Added a new test case which has 2 hidden frames sections in the backtrace.
|
✅ With the latest revision this PR passed the C/C++ code formatter. |
| ], | ||
| ) | ||
|
|
||
| def test_hidden_frame_markers(self): |
There was a problem hiding this comment.
You'll have to rename this method. Otherwise it's going to shadow the other one and only 1 of them will be run. It's a foot-gun i'm trying to implement a github action for as we speak :)
There was a problem hiding this comment.
Fixed, thanks!
🐧 Linux x64 Test Results
✅ The build succeeded and all tests passed. |
|
LLVM Buildbot has detected a new failure on builder Full details are available at: https://lab.llvm.org/buildbot/#/builders/211/builds/6339 Here is the relevant piece of the build log for the reference |
|
LLVM Buildbot has detected a new failure on builder Full details are available at: https://lab.llvm.org/buildbot/#/builders/141/builds/15481 Here is the relevant piece of the build log for the reference |
#181143 introduced a regression in Windows build bots, which is due to some Unicode characters not being rendered properly. The proper fix is to be able to configure the characters that are rendered, and to force them to be ascii characters.
llvm/llvm-project#181143 introduced a regression in Windows build bots, which is due to some Unicode characters not being rendered properly. The proper fix is to be able to configure the characters that are rendered, and to force them to be ascii characters.
This is a reland of llvm#167550. Instead of relying on libcpp for testing, we emulate our own hidden frames. This was originally causing tests failures on Windows.
llvm#181143 introduced a regression in Windows build bots, which is due to some Unicode characters not being rendered properly. The proper fix is to be able to configure the characters that are rendered, and to force them to be ascii characters.
This is a reland of llvm#167550. Instead of relying on libcpp for testing, we emulate our own hidden frames. This was originally causing tests failures on Windows.
llvm#181143 introduced a regression in Windows build bots, which is due to some Unicode characters not being rendered properly. The proper fix is to be able to configure the characters that are rendered, and to force them to be ascii characters.
This is a reland of #167550. Instead of relying on libcpp for testing, we emulate our own hidden frames. This was originally causing tests failures on Windows.