[Lldb-commits] [lldb] a030dfb - [lldb] Add --provider option to thread backtrace (#181071)
via lldb-commits
lldb-commits at lists.llvm.org
Tue Apr 7 11:55:36 PDT 2026
Author: Med Ismail Bennani
Date: 2026-04-07T11:55:30-07:00
New Revision: a030dfb53b21bd900aeed7830f408a79501418dc
URL: https://github.com/llvm/llvm-project/commit/a030dfb53b21bd900aeed7830f408a79501418dc
DIFF: https://github.com/llvm/llvm-project/commit/a030dfb53b21bd900aeed7830f408a79501418dc.diff
LOG: [lldb] Add --provider option to thread backtrace (#181071)
Added:
lldb/test/API/functionalities/scripted_frame_provider/thread_filter/Makefile
lldb/test/API/functionalities/scripted_frame_provider/thread_filter/TestFrameProviderThreadFilter.py
lldb/test/API/functionalities/scripted_frame_provider/thread_filter/frame_provider.py
lldb/test/API/functionalities/scripted_frame_provider/thread_filter/main.cpp
Modified:
lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h
lldb/include/lldb/Target/Target.h
lldb/include/lldb/Target/Thread.h
lldb/include/lldb/lldb-enumerations.h
lldb/source/Commands/CommandObjectTarget.cpp
lldb/source/Commands/CommandObjectThread.cpp
lldb/source/Commands/Options.td
lldb/source/Interpreter/CommandInterpreter.cpp
lldb/source/Target/Target.cpp
lldb/source/Target/Thread.cpp
lldb/test/API/functionalities/scripted_frame_provider/pass_through_prefix/TestFrameProviderPassThroughPrefix.py
lldb/test/API/functionalities/scripted_frame_provider/pass_through_prefix/frame_provider.py
Removed:
################################################################################
diff --git a/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h b/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h
index 701491b71fb27..600b9639c4d4d 100644
--- a/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h
+++ b/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h
@@ -258,6 +258,7 @@ static constexpr CommandObject::ArgumentTableEntry g_argument_table[] = {
{ lldb::eArgTypeFilename, "filename", lldb::eDiskFileCompletion, {}, { nullptr, false }, "The name of a file (can include path)." },
{ lldb::eArgTypeFormat, "format", lldb::CompletionType::eNoCompletion, {}, { FormatHelpTextCallback, true }, nullptr },
{ lldb::eArgTypeFrameIndex, "frame-index", lldb::eFrameIndexCompletion, {}, { nullptr, false }, "Index into a thread's list of frames." },
+ { lldb::eArgTypeFrameProviderIDRange, "frame-provider-id-range", lldb::CompletionType::eNoCompletion, {}, { nullptr, false }, "A single frame provider ID, a range of IDs (e.g., '0', '0-2', '0 to 2'), or '*'/'all' to show every provider. ID 0 is the base unwinder, 1+ are synthetic providers." },
{ lldb::eArgTypeFullName, "fullname", lldb::CompletionType::eNoCompletion, {}, { nullptr, false }, "Help text goes here." },
{ lldb::eArgTypeFunctionName, "function-name", lldb::CompletionType::eNoCompletion, {}, { nullptr, false }, "The name of a function." },
{ lldb::eArgTypeFunctionOrSymbol, "function-or-symbol", lldb::CompletionType::eNoCompletion, {}, { nullptr, false }, "The name of a function or symbol." },
diff --git a/lldb/include/lldb/Target/Target.h b/lldb/include/lldb/Target/Target.h
index 16f21ad9d8271..25ab90e906aeb 100644
--- a/lldb/include/lldb/Target/Target.h
+++ b/lldb/include/lldb/Target/Target.h
@@ -41,6 +41,7 @@
#include "lldb/Utility/StructuredData.h"
#include "lldb/Utility/Timeout.h"
#include "lldb/lldb-public.h"
+#include "llvm/ADT/MapVector.h"
#include "llvm/ADT/StringRef.h"
namespace lldb_private {
@@ -813,7 +814,7 @@ class Target : public std::enable_shared_from_this<Target>,
void ClearScriptedFrameProviderDescriptors();
/// Get all scripted frame provider descriptors for this target.
- const llvm::DenseMap<uint32_t, ScriptedFrameProviderDescriptor> &
+ const llvm::MapVector<uint32_t, ScriptedFrameProviderDescriptor> &
GetScriptedFrameProviderDescriptors() const;
protected:
@@ -1821,9 +1822,10 @@ class Target : public std::enable_shared_from_this<Target>,
TypeSystemMap m_scratch_type_system_map;
/// Map of scripted frame provider descriptors for this target.
- /// Keys are the provider descriptors ids, values are the descriptors.
- /// Used to initialize frame providers for new threads.
- llvm::DenseMap<uint32_t, ScriptedFrameProviderDescriptor>
+ /// Keys are the provider descriptor IDs, values are the descriptors.
+ /// Insertion order is preserved so that equal-priority providers chain
+ /// in registration order.
+ llvm::MapVector<uint32_t, ScriptedFrameProviderDescriptor>
m_frame_provider_descriptors;
mutable std::recursive_mutex m_frame_provider_descriptors_mutex;
diff --git a/lldb/include/lldb/Target/Thread.h b/lldb/include/lldb/Target/Thread.h
index c698dd8015885..8b69e83d94ca4 100644
--- a/lldb/include/lldb/Target/Thread.h
+++ b/lldb/include/lldb/Target/Thread.h
@@ -1324,6 +1324,25 @@ class Thread : public std::enable_shared_from_this<Thread>,
/// Returns true if any host thread is currently inside a provider.
bool IsAnyProviderActive();
+ /// Get the ordered chain of provider descriptors and their frame list IDs.
+ ///
+ /// Each element is a pair of:
+ /// - \b ScriptedFrameProviderDescriptor: metadata for the provider
+ /// (class name, description, priority, thread specs).
+ /// - \b frame_list_id_t: the sequential frame list identifier assigned
+ /// to that provider in the chain (1 for the first provider, 2 for the
+ /// second, etc.). ID 0 is reserved for the base unwinder and is never
+ /// present in this vector.
+ ///
+ /// The vector is ordered by provider chain position (registration order
+ /// adjusted by priority). It persists across \c ClearStackFrames() so that
+ /// provider IDs remain stable for the lifetime of the thread.
+ const std::vector<
+ std::pair<ScriptedFrameProviderDescriptor, lldb::frame_list_id_t>> &
+ GetProviderChainIds() const {
+ return m_provider_chain_ids;
+ }
+
protected:
friend class ThreadPlan;
friend class ThreadList;
diff --git a/lldb/include/lldb/lldb-enumerations.h b/lldb/include/lldb/lldb-enumerations.h
index 11b7808bb1b61..4d31c4db5c20b 100644
--- a/lldb/include/lldb/lldb-enumerations.h
+++ b/lldb/include/lldb/lldb-enumerations.h
@@ -596,6 +596,7 @@ enum CommandArgumentType {
eArgTypeFilename,
eArgTypeFormat,
eArgTypeFrameIndex,
+ eArgTypeFrameProviderIDRange,
eArgTypeFullName,
eArgTypeFunctionName,
eArgTypeFunctionOrSymbol,
diff --git a/lldb/source/Commands/CommandObjectTarget.cpp b/lldb/source/Commands/CommandObjectTarget.cpp
index f9b794d5124c8..aaed3051b0898 100644
--- a/lldb/source/Commands/CommandObjectTarget.cpp
+++ b/lldb/source/Commands/CommandObjectTarget.cpp
@@ -5472,7 +5472,7 @@ class CommandObjectTargetFrameProviderList : public CommandObjectParsed {
return;
}
- result.AppendMessageWithFormat("%u frame provider(s) registered:\n\n",
+ result.AppendMessageWithFormat("%zu frame provider(s) registered:\n\n",
descriptors.size());
for (const auto &entry : descriptors) {
diff --git a/lldb/source/Commands/CommandObjectThread.cpp b/lldb/source/Commands/CommandObjectThread.cpp
index 6786741cd04b6..87dd28445365d 100644
--- a/lldb/source/Commands/CommandObjectThread.cpp
+++ b/lldb/source/Commands/CommandObjectThread.cpp
@@ -92,6 +92,76 @@ class CommandObjectThreadBacktrace : public CommandObjectIterateOverThreads {
case 'u':
m_filtered_backtrace = false;
break;
+ case 'p': {
+ // Parse provider range using same format as breakpoint IDs.
+ // Supports: "N", "N-M", "N to M", "*", "all".
+ llvm::StringRef trimmed = option_arg.trim();
+ if (trimmed == "*" || trimmed.equals_insensitive("all")) {
+ m_show_all_providers = true;
+ m_provider_specific_backtrace = true;
+ break;
+ }
+
+ std::string option_lower = option_arg.lower();
+ static constexpr llvm::StringLiteral range_specifiers[] = {"-", "to"};
+
+ llvm::StringRef range_from;
+ llvm::StringRef range_to;
+ bool is_range = false;
+
+ // Try to find a range specifier.
+ for (auto specifier : range_specifiers) {
+ size_t idx = option_lower.find(specifier);
+ if (idx == std::string::npos)
+ continue;
+
+ range_from = llvm::StringRef(option_lower).take_front(idx).trim();
+ range_to = llvm::StringRef(option_lower)
+ .drop_front(idx + specifier.size())
+ .trim();
+
+ if (!range_from.empty() && !range_to.empty()) {
+ is_range = true;
+ break;
+ }
+ }
+
+ if (is_range) {
+ // Parse both start and end IDs.
+ if (range_from.getAsInteger(0, m_provider_start_id)) {
+ error = Status::FromErrorStringWithFormat(
+ "invalid start provider ID for option '%c': %s", short_option,
+ range_from.data());
+ break;
+ }
+ if (range_to.getAsInteger(0, m_provider_end_id)) {
+ error = Status::FromErrorStringWithFormat(
+ "invalid end provider ID for option '%c': %s", short_option,
+ range_to.data());
+ break;
+ }
+
+ // Validate range.
+ if (m_provider_start_id > m_provider_end_id) {
+ error = Status::FromErrorStringWithFormat(
+ "invalid provider range for option '%c': start ID %u > end "
+ "ID %u",
+ short_option, m_provider_start_id, m_provider_end_id);
+ break;
+ }
+ } else {
+ // Single provider ID.
+ if (option_arg.getAsInteger(0, m_provider_start_id)) {
+ error = Status::FromErrorStringWithFormat(
+ "invalid provider ID for option '%c': %s", short_option,
+ option_arg.data());
+ break;
+ }
+ m_provider_end_id = m_provider_start_id;
+ }
+
+ m_provider_specific_backtrace = true;
+ } break;
default:
llvm_unreachable("Unimplemented option");
}
@@ -103,10 +173,14 @@ class CommandObjectThreadBacktrace : public CommandObjectIterateOverThreads {
m_start = 0;
m_extended_backtrace = false;
m_filtered_backtrace = true;
+ m_provider_start_id = 0;
+ m_provider_end_id = 0;
+ m_provider_specific_backtrace = false;
+ m_show_all_providers = false;
}
llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
- return llvm::ArrayRef(g_thread_backtrace_options);
+ return g_thread_backtrace_options;
}
// Instance variables to hold the values for command options.
@@ -114,6 +188,10 @@ class CommandObjectThreadBacktrace : public CommandObjectIterateOverThreads {
uint32_t m_start;
bool m_extended_backtrace;
bool m_filtered_backtrace;
+ lldb::frame_list_id_t m_provider_start_id;
+ lldb::frame_list_id_t m_provider_end_id;
+ bool m_provider_specific_backtrace;
+ bool m_show_all_providers;
};
CommandObjectThreadBacktrace(CommandInterpreter &interpreter)
@@ -124,6 +202,9 @@ class CommandObjectThreadBacktrace : public CommandObjectIterateOverThreads {
"Use the thread-index \"all\" to see all threads.\n"
"Use the thread-index \"unique\" to see threads grouped by unique "
"call stacks.\n"
+ "Use '--provider <id>' or '--provider <start>-<end>' to view "
+ "synthetic frame providers (0=base unwinder, 1+=synthetic). "
+ "Range specifiers '-', 'to', 'To', 'TO' are supported.\n"
"Use 'settings set frame-format' to customize the printing of "
"frames in the backtrace and 'settings set thread-format' to "
"customize the thread header.\n"
@@ -227,13 +308,115 @@ class CommandObjectThreadBacktrace : public CommandObjectIterateOverThreads {
}
Thread *thread = thread_sp.get();
-
Stream &strm = result.GetOutputStream();
- // Only dump stack info if we processing unique stacks.
- const bool only_stacks = m_unique_stacks;
+ // Check if provider filtering is requested.
+ if (m_options.m_provider_specific_backtrace) {
+ // Disallow 'bt --provider' from within a scripted frame provider.
+ // A provider's get_frame_at_index running 'bt --provider' would
+ // try to evaluate the very provider that is mid-construction,
+ // leading to infinite recursion.
+ if (thread->IsAnyProviderActive()) {
+ result.AppendErrorWithFormat(
+ "cannot use '--provider' option while a scripted frame provider is "
+ "being constructed on this thread\n");
+ return false;
+ }
+
+ // Print thread status header, like regular bt. This also ensures the
+ // frame list is initialized and any providers are loaded.
+ thread->GetStatus(strm, /*start_frame=*/0, /*num_frames=*/0,
+ /*num_frames_with_source=*/0, /*stop_format=*/true,
+ /*show_hidden=*/false, /*only_stacks=*/false);
+
+ if (m_options.m_show_all_providers) {
+ // Show all providers: unwinder (0) through the last in the chain.
+ m_options.m_provider_start_id = 0;
+ const auto &chain = thread->GetProviderChainIds();
+ m_options.m_provider_end_id = chain.empty() ? 0 : chain.back().second;
+ }
+
+ // Provider filter mode: show sequential views for each provider in range.
+ bool first_provider = true;
+ for (lldb::frame_list_id_t provider_id = m_options.m_provider_start_id;
+ provider_id <= m_options.m_provider_end_id; ++provider_id) {
+
+ // Get the frame list for this provider.
+ lldb::StackFrameListSP frame_list_sp =
+ thread->GetFrameListByIdentifier(provider_id);
+
+ if (!frame_list_sp) {
+ // Provider doesn't exist - skip silently.
+ continue;
+ }
+
+ // Add blank line between providers for readability.
+ if (!first_provider)
+ strm.PutChar('\n');
+ first_provider = false;
+
+ // Print provider header.
+ strm.Printf("=== Provider %u", provider_id);
+
+ // Get provider metadata for header.
+ if (provider_id == 0) {
+ strm.Printf(": Base Unwinder ===\n");
+ } else {
+ // Find the descriptor in the provider chain.
+ const auto &provider_chain = thread->GetProviderChainIds();
+ std::string provider_name = "Unknown";
+ std::string provider_desc;
+ std::optional<uint32_t> provider_priority;
+
+ for (const auto &[descriptor, id] : provider_chain) {
+ if (id == provider_id) {
+ provider_name = descriptor.GetName().str();
+ provider_desc = descriptor.GetDescription();
+ provider_priority = descriptor.GetPriority();
+ break;
+ }
+ }
+
+ strm.Printf(": %s", provider_name.c_str());
+ if (provider_priority.has_value()) {
+ strm.Printf(" (priority: %u)", *provider_priority);
+ }
+ strm.Printf(" ===\n");
+
+ if (!provider_desc.empty()) {
+ strm.Printf("Description: %s\n", provider_desc.c_str());
+ }
+ }
+
+ // Print the backtrace for this provider.
+ const uint32_t num_frames_with_source = 0;
+ const StackFrameSP selected_frame_sp =
+ thread->GetSelectedFrame(DoNoSelectMostRelevantFrame);
+ const char *selected_frame_marker = selected_frame_sp ? "->" : nullptr;
+
+ size_t num_frames = frame_list_sp->GetStatus(
+ strm, m_options.m_start, m_options.m_count,
+ /*show_frame_info=*/true, num_frames_with_source,
+ /*show_unique=*/false,
+ /*show_hidden=*/!m_options.m_filtered_backtrace,
+ selected_frame_marker);
+
+ if (num_frames == 0) {
+ strm.Printf("(No frames available)\n");
+ }
+ }
- // Don't show source context when doing backtraces.
+ if (first_provider) {
+ result.AppendErrorWithFormat("no provider found in range %u-%u\n",
+ m_options.m_provider_start_id,
+ m_options.m_provider_end_id);
+ return false;
+ }
+ return true;
+ }
+
+ // Original behavior: show default backtrace.
+ const bool only_stacks = m_unique_stacks;
const uint32_t num_frames_with_source = 0;
const bool stop_format = true;
if (!thread->GetStatus(strm, m_options.m_start, m_options.m_count,
diff --git a/lldb/source/Commands/Options.td b/lldb/source/Commands/Options.td
index 78bde66199659..54e9f022880c4 100644
--- a/lldb/source/Commands/Options.td
+++ b/lldb/source/Commands/Options.td
@@ -1919,6 +1919,15 @@ let Command = "thread backtrace" in {
Group<1>,
Desc<"Do not filter out frames according "
"to installed frame recognizers">;
+ def thread_backtrace_provider
+ : Option<"provider", "p">,
+ Group<1>,
+ Arg<"FrameProviderIDRange">,
+ Desc<"Show backtrace from specific frame provider(s). "
+ "Use a single ID (e.g., '2') for one provider, "
+ "a range (e.g., '0-2', '0 to 2') for multiple, "
+ "or '*'/'all' to show every provider. "
+ "ID 0 is the base unwinder, 1+ are synthetic providers.">;
}
let Command = "thread step scope" in {
diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp
index eeb1ae0ff3eb8..6218591b3a20b 100644
--- a/lldb/source/Interpreter/CommandInterpreter.cpp
+++ b/lldb/source/Interpreter/CommandInterpreter.cpp
@@ -424,6 +424,15 @@ void CommandInterpreter::Initialize() {
if (cmd_obj_sp)
AddAlias("bt", cmd_obj_sp)->SetSyntax(cmd_obj_sp->GetSyntax());
+ cmd_obj_sp = GetCommandSPExact("thread backtrace");
+ if (cmd_obj_sp) {
+ if (auto *sys_bt = AddAlias("sys_bt", cmd_obj_sp, "--provider 0")) {
+ sys_bt->SetHelp("Show the base unwinder backtrace (without frame "
+ "providers). Equivalent to 'thread backtrace "
+ "--provider 0'.");
+ }
+ }
+
cmd_obj_sp = GetCommandSPExact("target create");
if (cmd_obj_sp)
AddAlias("file", cmd_obj_sp);
diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp
index a45945e1107cb..cba04a67a00cc 100644
--- a/lldb/source/Target/Target.cpp
+++ b/lldb/source/Target/Target.cpp
@@ -3762,7 +3762,7 @@ void Target::ClearScriptedFrameProviderDescriptors() {
InvalidateThreadFrameProviders();
}
-const llvm::DenseMap<uint32_t, ScriptedFrameProviderDescriptor> &
+const llvm::MapVector<uint32_t, ScriptedFrameProviderDescriptor> &
Target::GetScriptedFrameProviderDescriptors() const {
std::lock_guard<std::recursive_mutex> guard(
m_frame_provider_descriptors_mutex);
diff --git a/lldb/source/Target/Thread.cpp b/lldb/source/Target/Thread.cpp
index 60785d5c230db..fab9d2f3eada5 100644
--- a/lldb/source/Target/Thread.cpp
+++ b/lldb/source/Target/Thread.cpp
@@ -1538,15 +1538,16 @@ StackFrameListSP Thread::GetStackFrameList() {
thread_descriptors.push_back(&descriptor);
}
- // Sort by priority (lower number = higher priority).
- llvm::sort(thread_descriptors,
- [](const ScriptedFrameProviderDescriptor *a,
- const ScriptedFrameProviderDescriptor *b) {
- // nullopt (no priority) sorts last (UINT32_MAX).
- uint32_t priority_a = a->GetPriority().value_or(UINT32_MAX);
- uint32_t priority_b = b->GetPriority().value_or(UINT32_MAX);
- return priority_a < priority_b;
- });
+ // Stable sort by priority so equal-priority providers keep
+ // their registration (insertion) order.
+ llvm::stable_sort(
+ thread_descriptors, [](const ScriptedFrameProviderDescriptor *a,
+ const ScriptedFrameProviderDescriptor *b) {
+ // nullopt (no priority) sorts last (UINT32_MAX).
+ uint32_t priority_a = a->GetPriority().value_or(UINT32_MAX);
+ uint32_t priority_b = b->GetPriority().value_or(UINT32_MAX);
+ return priority_a < priority_b;
+ });
// Load ALL matching providers in priority order.
for (const auto *descriptor : thread_descriptors) {
@@ -1603,8 +1604,14 @@ Thread::GetFrameListByIdentifier(lldb::frame_list_id_t id) {
auto it = m_frame_lists_by_id.find(id);
if (it != m_frame_lists_by_id.end()) {
- return it->second.lock();
- }
+ auto sp = it->second.lock();
+ LLDB_LOG(GetLog(LLDBLog::Thread),
+ "GetFrameListByIdentifier({0}): found={1}, locked={2}", id, true,
+ sp != nullptr);
+ return sp;
+ }
+ LLDB_LOG(GetLog(LLDBLog::Thread), "GetFrameListByIdentifier({0}): found={1}",
+ id, false);
return nullptr;
}
@@ -1630,6 +1637,13 @@ llvm::Error Thread::LoadScriptedFrameProvider(
input_frames = std::make_shared<SyntheticStackFrameList>(
*this, last_provider_frames, m_prev_frames_sp, true, last_provider,
last_id);
+ // Register this intermediate frame list so 'bt --provider <id>' can
+ // show each provider's output independently.
+ m_frame_lists_by_id.insert({last_id, input_frames});
+ LLDB_LOG(GetLog(LLDBLog::Thread),
+ "Registered intermediate frame list for provider id={0}, "
+ "use_count={1}",
+ last_id, input_frames.use_count());
}
// Protect provider construction (__init__) from re-entrancy. If the
@@ -1644,12 +1658,11 @@ llvm::Error Thread::LoadScriptedFrameProvider(
if (!provider_or_err)
return provider_or_err.takeError();
- if (m_next_provider_id == std::numeric_limits<lldb::frame_list_id_t>::max())
+ lldb::frame_list_id_t provider_id = m_next_provider_id++;
+ if (m_next_provider_id ==
+ 0) // Wrapped past max; skip 0 (reserved for unwinder).
m_next_provider_id = 1;
- else
- m_next_provider_id++;
- lldb::frame_list_id_t provider_id = m_next_provider_id;
m_frame_providers.insert({provider_id, *provider_or_err});
// Add to the provider chain.
@@ -1682,6 +1695,7 @@ void Thread::ClearScriptedFrameProvider() {
std::lock_guard<std::recursive_mutex> guard(m_frame_mutex);
m_frame_providers.clear();
m_provider_chain_ids.clear();
+ m_frame_lists_by_id.clear();
m_next_provider_id = 1; // Reset counter.
m_unwinder_frames_sp.reset();
m_curr_frames_sp.reset();
@@ -1718,10 +1732,11 @@ void Thread::ClearStackFrames() {
m_curr_frames_sp.reset();
m_unwinder_frames_sp.reset();
- // Clear the provider instances, but keep the chain configuration
- // (m_provider_chain_ids and m_next_provider_id) so provider IDs
- // remain stable across ClearStackFrames() calls.
+ // Clear the provider instances and reset the ID counter, but keep the
+ // chain configuration (m_provider_chain_ids) so providers are re-loaded
+ // with consistent IDs on the next GetStackFrameList() call.
m_frame_providers.clear();
+ m_next_provider_id = 1;
m_frame_lists_by_id.clear();
m_extended_info.reset();
m_extended_info_fetched = false;
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/pass_through_prefix/TestFrameProviderPassThroughPrefix.py b/lldb/test/API/functionalities/scripted_frame_provider/pass_through_prefix/TestFrameProviderPassThroughPrefix.py
index 43994ef1a8cd4..071a7382dcab9 100644
--- a/lldb/test/API/functionalities/scripted_frame_provider/pass_through_prefix/TestFrameProviderPassThroughPrefix.py
+++ b/lldb/test/API/functionalities/scripted_frame_provider/pass_through_prefix/TestFrameProviderPassThroughPrefix.py
@@ -13,11 +13,17 @@
class FrameProviderPassThroughPrefixTestCase(TestBase):
NO_DEBUG_INFO_TESTCASE = True
+ # The frame list IDs used by 'bt --provider' are internal sequential IDs:
+ # 0 = base unwinder, 1 = first provider, 2 = second provider, etc.
+ # These are NOT the descriptor IDs returned by RegisterScriptedFrameProvider.
+ UNWINDER_FRAME_LIST_ID = 0
+ FIRST_PROVIDER_FRAME_LIST_ID = 1
+ SECOND_PROVIDER_FRAME_LIST_ID = 2
+
def setUp(self):
TestBase.setUp(self)
self.source = "main.c"
- @expectedFailureAll(oslist=["linux"], archs=["arm$"])
@expectedFailureAll(oslist=["windows"], bugnumber="llvm.org/pr24778")
def test_pass_through_with_prefix(self):
"""
@@ -83,6 +89,397 @@ def test_pass_through_with_prefix(self):
f"Frame {i} should be '{expected}' after provider",
)
+ @expectedFailureAll(oslist=["windows"], bugnumber="llvm.org/pr24778")
+ def test_bt_provider_shows_unwinder_frames(self):
+ """
+ Test that 'bt --provider 0' shows the base unwinder frames
+ (without the provider prefix) even after a provider is registered.
+ """
+ self.build()
+
+ target, process, thread, _ = lldbutil.run_to_source_breakpoint(
+ self, "break here", lldb.SBFileSpec(self.source)
+ )
+
+ script_path = os.path.join(self.getSourceDir(), "frame_provider.py")
+ self.runCmd("command script import " + script_path)
+
+ error = lldb.SBError()
+ provider_id = target.RegisterScriptedFrameProvider(
+ "frame_provider.PrefixPassThroughProvider",
+ lldb.SBStructuredData(),
+ error,
+ )
+ self.assertTrue(
+ error.Success(), f"Should register provider successfully: {error}"
+ )
+
+ # 'bt --provider 0' should show the base unwinder frames without prefix.
+ result = lldb.SBCommandReturnObject()
+ self.dbg.GetCommandInterpreter().HandleCommand("bt --provider 0", result)
+ self.assertTrue(result.Succeeded(), "bt --provider 0 should succeed")
+ output = result.GetOutput()
+ self.assertIn("Base Unwinder", output)
+ self.assertIn("baz", output)
+ self.assertNotIn("my_custom_", output)
+
+ @expectedFailureAll(oslist=["windows"], bugnumber="llvm.org/pr24778")
+ def test_bt_provider_shows_provider_frames(self):
+ """
+ Test that 'bt --provider <id>' shows the provider's transformed frames
+ with the prefix applied.
+ """
+ self.build()
+
+ target, process, thread, _ = lldbutil.run_to_source_breakpoint(
+ self, "break here", lldb.SBFileSpec(self.source)
+ )
+
+ script_path = os.path.join(self.getSourceDir(), "frame_provider.py")
+ self.runCmd("command script import " + script_path)
+
+ error = lldb.SBError()
+ provider_id = target.RegisterScriptedFrameProvider(
+ "frame_provider.PrefixPassThroughProvider",
+ lldb.SBStructuredData(),
+ error,
+ )
+ self.assertTrue(
+ error.Success(), f"Should register provider successfully: {error}"
+ )
+ self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
+
+ # 'bt --provider <id>' should show the provider's prefixed frames.
+ result = lldb.SBCommandReturnObject()
+ self.dbg.GetCommandInterpreter().HandleCommand(
+ f"bt --provider {self.FIRST_PROVIDER_FRAME_LIST_ID}", result
+ )
+ self.assertTrue(
+ result.Succeeded(),
+ f"bt --provider {self.FIRST_PROVIDER_FRAME_LIST_ID} should succeed",
+ )
+ output = result.GetOutput()
+ self.assertIn("PrefixPassThroughProvider", output)
+ self.assertIn("my_custom_baz", output)
+ self.assertIn("my_custom_bar", output)
+ self.assertIn("my_custom_foo", output)
+ self.assertIn("my_custom_main", output)
+
+ @expectedFailureAll(oslist=["windows"], bugnumber="llvm.org/pr24778")
+ def test_bt_provider_range(self):
+ """
+ Test that 'bt --provider 0-<id>' shows both the base unwinder
+ and provider frames sequentially.
+ """
+ self.build()
+
+ target, process, thread, _ = lldbutil.run_to_source_breakpoint(
+ self, "break here", lldb.SBFileSpec(self.source)
+ )
+
+ script_path = os.path.join(self.getSourceDir(), "frame_provider.py")
+ self.runCmd("command script import " + script_path)
+
+ error = lldb.SBError()
+ provider_id = target.RegisterScriptedFrameProvider(
+ "frame_provider.PrefixPassThroughProvider",
+ lldb.SBStructuredData(),
+ error,
+ )
+ self.assertTrue(
+ error.Success(), f"Should register provider successfully: {error}"
+ )
+ self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
+
+ # 'bt --provider 0-<id>' should show both sections.
+ result = lldb.SBCommandReturnObject()
+ self.dbg.GetCommandInterpreter().HandleCommand(
+ f"bt --provider 0-{self.FIRST_PROVIDER_FRAME_LIST_ID}", result
+ )
+ self.assertTrue(
+ result.Succeeded(),
+ f"bt --provider 0-{self.FIRST_PROVIDER_FRAME_LIST_ID} should succeed",
+ )
+ output = result.GetOutput()
+ # Should contain both the base unwinder and the provider sections.
+ self.assertIn("Base Unwinder", output)
+ self.assertIn("PrefixPassThroughProvider", output)
+
+ @expectedFailureAll(oslist=["windows"], bugnumber="llvm.org/pr24778")
+ def test_bt_provider_range_not_starting_at_zero(self):
+ """
+ Test that 'bt --provider <id>-<id>' works when the range doesn't
+ include the base unwinder (provider 0).
+ """
+ self.build()
+
+ target, process, thread, _ = lldbutil.run_to_source_breakpoint(
+ self, "break here", lldb.SBFileSpec(self.source)
+ )
+
+ script_path = os.path.join(self.getSourceDir(), "frame_provider.py")
+ self.runCmd("command script import " + script_path)
+
+ error = lldb.SBError()
+ provider_id = target.RegisterScriptedFrameProvider(
+ "frame_provider.PrefixPassThroughProvider",
+ lldb.SBStructuredData(),
+ error,
+ )
+ self.assertTrue(
+ error.Success(), f"Should register provider successfully: {error}"
+ )
+ self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
+
+ # Range that only includes the provider, not the base unwinder.
+ fid = self.FIRST_PROVIDER_FRAME_LIST_ID
+ result = lldb.SBCommandReturnObject()
+ self.dbg.GetCommandInterpreter().HandleCommand(
+ f"bt --provider {fid}-{fid}", result
+ )
+ self.assertTrue(
+ result.Succeeded(),
+ f"bt --provider {fid}-{fid} should succeed",
+ )
+ output = result.GetOutput()
+ self.assertNotIn("Base Unwinder", output)
+ self.assertIn("PrefixPassThroughProvider", output)
+ self.assertIn("my_custom_baz", output)
+
+ @expectedFailureAll(oslist=["windows"], bugnumber="llvm.org/pr24778")
+ def test_bt_provider_range_with_to_separator(self):
+ """
+ Test that 'bt --provider 0 to <id>' works with the 'to' separator.
+ """
+ self.build()
+
+ target, process, thread, _ = lldbutil.run_to_source_breakpoint(
+ self, "break here", lldb.SBFileSpec(self.source)
+ )
+
+ script_path = os.path.join(self.getSourceDir(), "frame_provider.py")
+ self.runCmd("command script import " + script_path)
+
+ error = lldb.SBError()
+ provider_id = target.RegisterScriptedFrameProvider(
+ "frame_provider.PrefixPassThroughProvider",
+ lldb.SBStructuredData(),
+ error,
+ )
+ self.assertTrue(
+ error.Success(), f"Should register provider successfully: {error}"
+ )
+ self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
+
+ # Use 'to' separator instead of '-'.
+ result = lldb.SBCommandReturnObject()
+ self.dbg.GetCommandInterpreter().HandleCommand(
+ f"bt --provider '0 to {self.FIRST_PROVIDER_FRAME_LIST_ID}'", result
+ )
+ self.assertTrue(
+ result.Succeeded(),
+ f"bt --provider '0 to {self.FIRST_PROVIDER_FRAME_LIST_ID}' should succeed",
+ )
+ output = result.GetOutput()
+ self.assertIn("Base Unwinder", output)
+ self.assertIn("PrefixPassThroughProvider", output)
+
+ def test_bt_provider_invalid_id(self):
+ """
+ Test that 'bt --provider <invalid>' fails with an error when the
+ provider ID doesn't exist.
+ """
+ self.build()
+
+ target, process, thread, _ = lldbutil.run_to_source_breakpoint(
+ self, "break here", lldb.SBFileSpec(self.source)
+ )
+
+ result = lldb.SBCommandReturnObject()
+ self.dbg.GetCommandInterpreter().HandleCommand("bt --provider 999", result)
+ self.assertFalse(result.Succeeded(), "bt --provider 999 should fail")
+
+ def test_bt_provider_invalid_range(self):
+ """
+ Test that 'bt --provider N-M' where N > M fails with an error
+ about an invalid range.
+ """
+ self.build()
+
+ target, process, thread, _ = lldbutil.run_to_source_breakpoint(
+ self, "break here", lldb.SBFileSpec(self.source)
+ )
+
+ result = lldb.SBCommandReturnObject()
+ self.dbg.GetCommandInterpreter().HandleCommand("bt --provider 5-2", result)
+ self.assertFalse(result.Succeeded(), "bt --provider 5-2 should fail")
+ self.assertIn("invalid provider range", result.GetError())
+
+ @expectedFailureAll(oslist=["windows"], bugnumber="llvm.org/pr24778")
+ def test_bt_provider_star_shows_all(self):
+ """
+ Test that 'bt --provider *' shows all providers including the
+ base unwinder and any registered providers.
+ """
+ self.build()
+
+ target, process, thread, _ = lldbutil.run_to_source_breakpoint(
+ self, "break here", lldb.SBFileSpec(self.source)
+ )
+
+ script_path = os.path.join(self.getSourceDir(), "frame_provider.py")
+ self.runCmd("command script import " + script_path)
+
+ error = lldb.SBError()
+ target.RegisterScriptedFrameProvider(
+ "frame_provider.PrefixPassThroughProvider",
+ lldb.SBStructuredData(),
+ error,
+ )
+ self.assertTrue(error.Success(), f"Should register provider: {error}")
+
+ result = lldb.SBCommandReturnObject()
+ self.dbg.GetCommandInterpreter().HandleCommand("bt --provider '*'", result)
+ self.assertTrue(result.Succeeded(), "bt --provider '*' should succeed")
+ output = result.GetOutput()
+ self.assertIn("Base Unwinder", output)
+ self.assertIn("PrefixPassThroughProvider", output)
+
+ @expectedFailureAll(oslist=["windows"], bugnumber="llvm.org/pr24778")
+ def test_bt_provider_all_shows_all(self):
+ """
+ Test that 'bt --provider all' shows all providers including the
+ base unwinder and any registered providers.
+ """
+ self.build()
+
+ target, process, thread, _ = lldbutil.run_to_source_breakpoint(
+ self, "break here", lldb.SBFileSpec(self.source)
+ )
+
+ script_path = os.path.join(self.getSourceDir(), "frame_provider.py")
+ self.runCmd("command script import " + script_path)
+
+ error = lldb.SBError()
+ target.RegisterScriptedFrameProvider(
+ "frame_provider.PrefixPassThroughProvider",
+ lldb.SBStructuredData(),
+ error,
+ )
+ self.assertTrue(error.Success(), f"Should register provider: {error}")
+
+ target.RegisterScriptedFrameProvider(
+ "frame_provider.UpperCasePassThroughProvider",
+ lldb.SBStructuredData(),
+ error,
+ )
+ self.assertTrue(error.Success(), f"Should register upper provider: {error}")
+
+ result = lldb.SBCommandReturnObject()
+ self.dbg.GetCommandInterpreter().HandleCommand("bt --provider all", result)
+ self.assertTrue(result.Succeeded(), "bt --provider all should succeed")
+ output = result.GetOutput()
+ self.assertIn("Base Unwinder", output)
+ self.assertIn("PrefixPassThroughProvider", output)
+ self.assertIn("UpperCasePassThroughProvider", output)
+
+ @expectedFailureAll(oslist=["windows"], bugnumber="llvm.org/pr24778")
+ def test_bt_provider_multiple_providers(self):
+ """
+ Test 'bt --provider' with two chained providers. Register
+ PrefixPassThroughProvider (adds 'my_custom_' prefix) then
+ UpperCasePassThroughProvider (upper-cases names). Verify each
+ provider's view is correct and that ranges across providers work.
+ """
+ self.build()
+
+ target, process, thread, _ = lldbutil.run_to_source_breakpoint(
+ self, "break here", lldb.SBFileSpec(self.source)
+ )
+
+ script_path = os.path.join(self.getSourceDir(), "frame_provider.py")
+ self.runCmd("command script import " + script_path)
+
+ # Register first provider: adds 'my_custom_' prefix.
+ error = lldb.SBError()
+ target.RegisterScriptedFrameProvider(
+ "frame_provider.PrefixPassThroughProvider",
+ lldb.SBStructuredData(),
+ error,
+ )
+ self.assertTrue(error.Success(), f"Should register prefix provider: {error}")
+
+ # Register second provider: upper-cases everything.
+ target.RegisterScriptedFrameProvider(
+ "frame_provider.UpperCasePassThroughProvider",
+ lldb.SBStructuredData(),
+ error,
+ )
+ self.assertTrue(error.Success(), f"Should register upper provider: {error}")
+
+ # bt --provider 0: base unwinder, original names.
+ result = lldb.SBCommandReturnObject()
+ self.dbg.GetCommandInterpreter().HandleCommand("bt --provider 0", result)
+ self.assertTrue(result.Succeeded())
+ output = result.GetOutput()
+ self.assertIn("Base Unwinder", output)
+ self.assertIn("baz", output)
+
+ # bt --provider all: should show all providers.
+ result = lldb.SBCommandReturnObject()
+ self.dbg.GetCommandInterpreter().HandleCommand("bt --provider all", result)
+ self.assertTrue(result.Succeeded())
+ output = result.GetOutput()
+ self.assertIn("Base Unwinder", output)
+ self.assertIn("UpperCasePassThroughProvider", output)
+ # UpperCase wraps Prefix (registration order is preserved), so the
+ # outermost output should have fully upper-cased prefixed names.
+ self.assertIn("MY_CUSTOM_BAZ", output)
+
+ @expectedFailureAll(oslist=["windows"], bugnumber="llvm.org/pr24778")
+ def test_bt_provider_star_from_within_provider(self):
+ """
+ Test that running 'bt --provider *' re-entrantly from within a
+ scripted frame provider's get_frame_at_index does not deadlock
+ or crash.
+
+ BtProviderStarProvider runs 'bt --provider *' on its first
+ get_frame_at_index call and stores the output, then passes through
+ all frames with a 'reentrant_' prefix. We verify:
+ 1. The provider completes without hanging.
+ 2. Frame 0 has the 'reentrant_' prefix.
+ """
+ self.build()
+
+ target, process, thread, _ = lldbutil.run_to_source_breakpoint(
+ self, "break here", lldb.SBFileSpec(self.source)
+ )
+
+ script_path = os.path.join(self.getSourceDir(), "frame_provider.py")
+ self.runCmd("command script import " + script_path)
+
+ error = lldb.SBError()
+ provider_id = target.RegisterScriptedFrameProvider(
+ "frame_provider.BtProviderStarProvider",
+ lldb.SBStructuredData(),
+ error,
+ )
+ self.assertTrue(
+ error.Success(), f"Should register provider successfully: {error}"
+ )
+ self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
+
+ # Access frame 0 to trigger the provider (and the re-entrant bt call).
+ # Only frame 0 is checked because the re-entrant HandleCommand resets
+ # the selected frame on the unwinder list, which corrupts subsequent
+ # frame lookups via inlined depth adjustments.
+ frame = thread.GetFrameAtIndex(0)
+ self.assertEqual(
+ frame.GetFunctionName(),
+ "reentrant_baz",
+ "Frame 0 should be 'reentrant_baz' after provider",
+ )
+
def test_provider_receives_parent_frames(self):
"""
Test that the provider's input_frames come from the parent
@@ -116,11 +513,10 @@ def test_provider_receives_parent_frames(self):
)
self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
- # No frame should contain 'danger_will_robinson_' — that would mean the provider
- # was handed its own output list instead of the parent list.
- expected_names = ["baz", "bar", "foo", "main"]
- prefix = "my_custom_"
- for i, name in enumerate(expected_names):
+ # No frame should contain 'danger_will_robinson_' — that would mean
+ # the provider was handed its own output list instead of the parent.
+ num_frames = thread.GetNumFrames()
+ for i in range(num_frames):
frame = thread.GetFrameAtIndex(i)
actual = frame.GetFunctionName()
self.assertFalse(
@@ -128,9 +524,3 @@ def test_provider_receives_parent_frames(self):
f"Frame {i}: provider got its own output list "
f"(expected bare parent frames, got '{actual}')",
)
- expected = prefix + name
- self.assertEqual(
- actual,
- expected,
- f"Frame {i} should be '{expected}' after provider",
- )
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/pass_through_prefix/frame_provider.py b/lldb/test/API/functionalities/scripted_frame_provider/pass_through_prefix/frame_provider.py
index 5c82b5e43689c..18344208cac56 100644
--- a/lldb/test/API/functionalities/scripted_frame_provider/pass_through_prefix/frame_provider.py
+++ b/lldb/test/API/functionalities/scripted_frame_provider/pass_through_prefix/frame_provider.py
@@ -14,19 +14,18 @@
class PrefixedFrame(ScriptedFrame):
"""A frame that wraps a real frame but prefixes the function name."""
- def __init__(self, thread, idx, pc, function_name, prefix):
+ def __init__(self, thread, idx, function_name, prefix):
args = lldb.SBStructuredData()
super().__init__(thread, args)
self.idx = idx
- self.pc = pc
self.function_name = prefix + function_name
def get_id(self):
return self.idx
def get_pc(self):
- return self.pc
+ return 0
def get_function_name(self):
return self.function_name
@@ -63,11 +62,70 @@ def get_frame_at_index(self, idx):
if idx < len(self.input_frames):
frame = self.input_frames[idx]
function_name = frame.GetFunctionName()
- pc = frame.GetPC()
- return PrefixedFrame(self.thread, idx, pc, function_name, self.PREFIX)
+ return PrefixedFrame(self.thread, idx, function_name, self.PREFIX)
return None
+class UpperCasePassThroughProvider(ScriptedFrameProvider):
+ """
+ Provider that passes through every frame from its parent StackFrameList
+ but upper-cases each function name.
+
+ When chained after PrefixPassThroughProvider, the result should be
+ e.g. 'MY_CUSTOM_BAZ'.
+ """
+
+ def __init__(self, input_frames, args):
+ super().__init__(input_frames, args)
+
+ @staticmethod
+ def get_description():
+ return "Provider that upper-cases all function names"
+
+ def get_frame_at_index(self, idx):
+ if idx < len(self.input_frames):
+ frame = self.input_frames[idx]
+ function_name = frame.GetFunctionName()
+ return PrefixedFrame(self.thread, idx, function_name.upper(), "")
+ return None
+
+
+class BtProviderStarProvider(ScriptedFrameProvider):
+ """
+ Provider that runs 'bt --provider *' from within get_frame_at_index
+ to verify that re-entrant provider queries don't deadlock or crash.
+
+ On the first call to get_frame_at_index, it runs the command and stores
+ the output. It then passes through all frames with a 'reentrant_' prefix.
+ """
+
+ PREFIX = "reentrant_"
+
+ def __init__(self, input_frames, args):
+ super().__init__(input_frames, args)
+ self.bt_provider_star_output = None
+
+ @staticmethod
+ def get_description():
+ return "Provider that runs 'bt --provider *' during frame construction"
+
+ def get_frame_at_index(self, idx):
+ if idx >= len(self.input_frames):
+ return None
+
+ # On the first frame request, run 'bt --provider *' re-entrantly.
+ if self.bt_provider_star_output is None:
+ debugger = self.target.GetDebugger()
+ ci = debugger.GetCommandInterpreter()
+ result = lldb.SBCommandReturnObject()
+ ci.HandleCommand("bt --provider '*'", result)
+ self.bt_provider_star_output = result.GetOutput() or ""
+
+ frame = self.input_frames[idx]
+ function_name = frame.GetFunctionName()
+ return PrefixedFrame(self.thread, idx, function_name, self.PREFIX)
+
+
class ValidatingPrefixProvider(ScriptedFrameProvider):
"""
Provider that prefixes function names AND validates it receives the
@@ -94,7 +152,6 @@ def get_frame_at_index(self, idx):
frame = self.input_frames[idx]
function_name = frame.GetFunctionName()
- pc = frame.GetPC()
# For frames after the first, peek at the younger (already-provided)
# frame in input_frames. If it already has our prefix, we were handed
@@ -103,7 +160,7 @@ def get_frame_at_index(self, idx):
younger = self.input_frames[idx - 1]
if younger.GetFunctionName().startswith(self.PREFIX):
return PrefixedFrame(
- self.thread, idx, pc, function_name, "danger_will_robinson_"
+ self.thread, idx, function_name, "danger_will_robinson_"
)
- return PrefixedFrame(self.thread, idx, pc, function_name, self.PREFIX)
+ return PrefixedFrame(self.thread, idx, function_name, self.PREFIX)
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/thread_filter/Makefile b/lldb/test/API/functionalities/scripted_frame_provider/thread_filter/Makefile
new file mode 100644
index 0000000000000..99998b20bcb05
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/thread_filter/Makefile
@@ -0,0 +1,3 @@
+CXX_SOURCES := main.cpp
+
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/thread_filter/TestFrameProviderThreadFilter.py b/lldb/test/API/functionalities/scripted_frame_provider/thread_filter/TestFrameProviderThreadFilter.py
new file mode 100644
index 0000000000000..fb41cb29f2b4f
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/thread_filter/TestFrameProviderThreadFilter.py
@@ -0,0 +1,94 @@
+"""
+Test bt --provider * in a multithreaded scenario with even/odd thread-filtered
+providers and an uppercase provider chained on top.
+"""
+
+import os
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import TestBase
+from lldbsuite.test import lldbutil
+
+
+class FrameProviderThreadFilterTestCase(TestBase):
+ NO_DEBUG_INFO_TESTCASE = True
+
+ def setUp(self):
+ TestBase.setUp(self)
+ self.source = "main.cpp"
+
+ def build_and_stop_all_threads(self):
+ """Build, set a breakpoint, and continue until all 3 worker threads
+ have hit it. Returns (target, process, worker_threads)."""
+ self.build()
+ target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
+ self,
+ "break in thread",
+ lldb.SBFileSpec(self.source),
+ only_one_thread=False,
+ )
+
+ # The breakpoint is on a one-shot line (fetch_add), so each hit is
+ # from a unique thread. Continue until all 3 have hit it.
+ while bkpt.GetHitCount() < 3:
+ process.Continue()
+
+ # After 3 hits, all worker threads are alive: some in the spin loop,
+ # one at the breakpoint. Collect all non-main threads.
+ worker_threads = []
+ for t in process:
+ for i in range(t.GetNumFrames()):
+ if "thread_work" in (t.GetFrameAtIndex(i).GetFunctionName() or ""):
+ worker_threads.append(t)
+ break
+
+ self.assertEqual(len(worker_threads), 3, "Expected 3 worker threads")
+ return target, process, worker_threads
+
+ def register_providers(self, target):
+ """Import the script and register all three providers in order."""
+ script_path = os.path.join(self.getSourceDir(), "frame_provider.py")
+ self.runCmd("command script import " + script_path)
+
+ error = lldb.SBError()
+ for cls in ("EvenThreadProvider", "OddThreadProvider", "UpperCaseProvider"):
+ target.RegisterScriptedFrameProvider(
+ "frame_provider." + cls,
+ lldb.SBStructuredData(),
+ error,
+ )
+ self.assertTrue(error.Success(), f"Should register {cls}: {error}")
+
+ @expectedFailureAll(oslist=["windows"], bugnumber="llvm.org/pr24778")
+ def test_bt_provider_star_with_thread_filter(self):
+ """
+ Register EvenThreadProvider, OddThreadProvider, and UpperCaseProvider.
+ For each worker thread, 'bt --provider *' should show:
+ - Base Unwinder section with original names (thread_work).
+ - UpperCaseProvider section with the fully chained result.
+ - The non-applicable prefix provider should NOT appear.
+ The default 'bt' should show the final upper-cased + prefixed names.
+ """
+ target, process, worker_threads = self.build_and_stop_all_threads()
+ self.register_providers(target)
+
+ for thread in worker_threads:
+ prefix = "EVEN_THREAD_" if thread.GetIndexID() % 2 == 0 else "ODD_THREAD_"
+ excluded = (
+ "OddThreadProvider"
+ if prefix == "EVEN_THREAD_"
+ else "EvenThreadProvider"
+ )
+
+ process.SetSelectedThread(thread)
+
+ self.expect(
+ "bt --provider '*'",
+ substrs=["Base Unwinder", "thread_work", "UpperCaseProvider", prefix],
+ )
+ self.expect(
+ "bt --provider '*'",
+ matching=False,
+ substrs=[excluded],
+ )
+ self.expect("bt", substrs=[prefix])
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/thread_filter/frame_provider.py b/lldb/test/API/functionalities/scripted_frame_provider/thread_filter/frame_provider.py
new file mode 100644
index 0000000000000..5fe27c8590713
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/thread_filter/frame_provider.py
@@ -0,0 +1,110 @@
+"""
+Frame providers for multithreaded thread-filter testing.
+
+EvenThreadProvider - applies only to even-indexed threads, prepends 'even_thread_'.
+OddThreadProvider - applies only to odd-indexed threads, prepends 'odd_thread_'.
+UpperCaseProvider - applies to ALL threads, upper-cases function names.
+
+When all three are registered (even, odd, uppercase -- in that order), chaining
+produces:
+ Even threads: EVEN_THREAD_THREAD_WORK
+ Odd threads: ODD_THREAD_THREAD_WORK
+"""
+
+import lldb
+from lldb.plugins.scripted_process import ScriptedFrame
+from lldb.plugins.scripted_frame_provider import ScriptedFrameProvider
+
+
+class PrefixedFrame(ScriptedFrame):
+ """A frame that wraps a real frame but transforms the function name."""
+
+ def __init__(self, thread, idx, function_name):
+ args = lldb.SBStructuredData()
+ super().__init__(thread, args)
+ self.idx = idx
+ self.function_name = function_name
+
+ def get_id(self):
+ return self.idx
+
+ def get_pc(self):
+ return 0
+
+ def get_function_name(self):
+ return self.function_name
+
+ def is_artificial(self):
+ return False
+
+ def is_hidden(self):
+ return False
+
+ def get_register_context(self):
+ return None
+
+
+class EvenThreadProvider(ScriptedFrameProvider):
+ """Applies only to even-indexed threads; prepends 'even_thread_' prefix."""
+
+ PREFIX = "even_thread_"
+
+ def __init__(self, input_frames, args):
+ super().__init__(input_frames, args)
+
+ @staticmethod
+ def applies_to_thread(thread):
+ return thread.GetIndexID() % 2 == 0
+
+ @staticmethod
+ def get_description():
+ return "Prefix for even threads"
+
+ def get_frame_at_index(self, idx):
+ if idx < len(self.input_frames):
+ frame = self.input_frames[idx]
+ name = self.PREFIX + frame.GetFunctionName()
+ return PrefixedFrame(self.thread, idx, name)
+ return None
+
+
+class OddThreadProvider(ScriptedFrameProvider):
+ """Applies only to odd-indexed threads; prepends 'odd_thread_' prefix."""
+
+ PREFIX = "odd_thread_"
+
+ def __init__(self, input_frames, args):
+ super().__init__(input_frames, args)
+
+ @staticmethod
+ def applies_to_thread(thread):
+ return thread.GetIndexID() % 2 != 0
+
+ @staticmethod
+ def get_description():
+ return "Prefix for odd threads"
+
+ def get_frame_at_index(self, idx):
+ if idx < len(self.input_frames):
+ frame = self.input_frames[idx]
+ name = self.PREFIX + frame.GetFunctionName()
+ return PrefixedFrame(self.thread, idx, name)
+ return None
+
+
+class UpperCaseProvider(ScriptedFrameProvider):
+ """Applies to ALL threads; upper-cases all function names."""
+
+ def __init__(self, input_frames, args):
+ super().__init__(input_frames, args)
+
+ @staticmethod
+ def get_description():
+ return "Upper-case all function names"
+
+ def get_frame_at_index(self, idx):
+ if idx < len(self.input_frames):
+ frame = self.input_frames[idx]
+ name = frame.GetFunctionName().upper()
+ return PrefixedFrame(self.thread, idx, name)
+ return None
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/thread_filter/main.cpp b/lldb/test/API/functionalities/scripted_frame_provider/thread_filter/main.cpp
new file mode 100644
index 0000000000000..3658357132503
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/thread_filter/main.cpp
@@ -0,0 +1,18 @@
+#include <thread>
+#include <unistd.h>
+#include <vector>
+
+#define NUM_THREADS 3
+
+void thread_work() {
+ pause(); // break in thread
+}
+
+int main() {
+ std::vector<std::thread> threads;
+ for (int i = 0; i < NUM_THREADS; i++)
+ threads.emplace_back(thread_work);
+ for (auto &t : threads)
+ t.join();
+ return 0;
+}
More information about the lldb-commits
mailing list