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;
+}


        
_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits

Reply via email to