[Lldb-commits] [lldb] [lldb] Add --provider option to thread backtrace (PR #181071)

Med Ismail Bennani via lldb-commits lldb-commits at lists.llvm.org
Tue Mar 31 21:25:26 PDT 2026


https://github.com/medismailben updated https://github.com/llvm/llvm-project/pull/181071

>From e4b299251822ac244d20763b81dec053a29e1f23 Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Tue, 31 Mar 2026 21:25:07 -0700
Subject: [PATCH] [lldb/Trace] Add --provider option to thread backtrace

This patch adds a `--provider/-p` option to `thread backtrace` to enable
viewing frame provider outputs individually. Users can specify single
provider IDs (e.g., `--provider 2`) or ranges (e.g., `--provider 0-2`,
`--provider 0 to 2`) using the same syntax as breakpoint ID ranges,
or `'*'`/`all` to show every provider.

Provider ID 0 represents the base unwinder, while IDs 1+ correspond to
synthetic frame providers. When the option is specified, the command
displays sequential views for each provider in the range, showing provider
metadata (name, priority, description) followed by the backtrace produced
by that provider.

A `sys_bt` alias is added for `thread backtrace --provider 0` to quickly
view the base unwinder frames without provider transformations.

The implementation adds a new `eArgTypeFrameProviderIDRange` argument type
with completion support, extends `CommandObjectThreadBacktrace::Options`
with provider filtering state (`m_provider_start_id`, `m_provider_end_id`,
`m_use_provider_filter`), and implements range parsing that supports `-`,
`to`, `To`, and `TO` as range specifiers.

The backtrace rendering logic calls `Thread::GetFrameListByIdentifier()`
for each provider ID in the range and displays provider metadata by looking
up the provider descriptor in the chain. A new `Thread::GetProviderChainIds()`
accessor exposes the provider chain metadata for display.

This enables debugging workflows where users need to inspect individual
provider transformations in a multi-provider chain (e.g., comparing base
unwinder output vs. coroutine frame injection vs. async task unwrapping).

Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
---
 .../Interpreter/CommandOptionArgumentTable.h  |   1 +
 lldb/include/lldb/Target/Target.h             |  10 +-
 lldb/include/lldb/Target/Thread.h             |  19 +
 lldb/include/lldb/lldb-enumerations.h         |   1 +
 lldb/source/Commands/CommandObjectTarget.cpp  |   2 +-
 lldb/source/Commands/CommandObjectThread.cpp  | 193 +++++++-
 lldb/source/Commands/Options.td               |   9 +
 .../source/Interpreter/CommandInterpreter.cpp |   9 +
 lldb/source/Target/Target.cpp                 |   2 +-
 lldb/source/Target/Thread.cpp                 |  51 ++-
 .../TestFrameProviderPassThroughPrefix.py     | 414 +++++++++++++++++-
 .../pass_through_prefix/frame_provider.py     |  73 ++-
 .../thread_filter/Makefile                    |   3 +
 .../TestFrameProviderThreadFilter.py          |  96 ++++
 .../thread_filter/frame_provider.py           | 110 +++++
 .../thread_filter/main.cpp                    |  18 +
 16 files changed, 962 insertions(+), 49 deletions(-)
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/thread_filter/Makefile
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/thread_filter/TestFrameProviderThreadFilter.py
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/thread_filter/frame_provider.py
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/thread_filter/main.cpp

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 77b8f04a4b3b8..ff9fbb7493360 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 {
@@ -797,7 +798,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:
@@ -1805,9 +1806,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 dccb7256b1cb7..61e647546a646 100644
--- a/lldb/source/Commands/CommandObjectTarget.cpp
+++ b/lldb/source/Commands/CommandObjectTarget.cpp
@@ -5508,7 +5508,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 2a3832225f9b7..1765f7ca34c98 100644
--- a/lldb/source/Target/Target.cpp
+++ b/lldb/source/Target/Target.cpp
@@ -3771,7 +3771,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 c8e50db2316ad..3fd092c1373b1 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..daeb4df7d787d
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/thread_filter/TestFrameProviderThreadFilter.py
@@ -0,0 +1,96 @@
+"""
+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