Skip to content

Conversation

@asavonic
Copy link
Collaborator

@asavonic asavonic commented Jul 7, 2025

Shared modules are stored in a global ModuleList cache, and it is intentionally leaked to avoid doing cleanup when lldb exits.

However, when lldb is used as a library, we need a way to manage opened modules to avoid problems with file locks (on some systems) for modules that we no longer need.
It is possible to use SBDebugger::MemoryPressureDetected (which calls ModuleList::RemoveOrphanSharedModules) to clear all modules with use_count == 1, but this has to be done after SBDebugger::Destroy, because it may keep pointers to some modules.

The patch adds the following:

  • If enabled, SBDebugger::Destroy now calls ModuleList::RemoveOrphanSharedModules after all its pointers are released.
    Note that if the program keeps pointers to modules elsewhere, they will not be released, and they will not be removed from the cache.
    When a complete tear down is required, the program must ensure that all pointers are released before calling SBDebugger::Destroy.

  • SBDebugger::SetClearSharedModules(bool) API to enable this behavior.

  • settings set clear-shared-modules true command line option that is equivalent to SetClearSharedModules(true).

This new behaviour is turned off by default: the debugger does not release these shared modules until the process exits, or until SBDebugger::MemoryPressureDetected.

The option provides a clear way for library users to communicate that complete cleanup is required, without relying on side-effects of MemoryPressureDetected.
It also allows to evaluate impact of clearing modules on performance of standalone debuggers.

@asavonic asavonic requested a review from clayborg July 7, 2025 12:38
@asavonic asavonic requested a review from JDevlieghere as a code owner July 7, 2025 12:38
@llvmbot llvmbot added the lldb label Jul 7, 2025
@llvmbot
Copy link
Member

llvmbot commented Jul 7, 2025

@llvm/pr-subscribers-lldb

Author: Andrew Savonichev (asavonic)

Changes

Shared modules are stored in a global ModuleList cache, and it is intentionally leaked to avoid doing cleanup when lldb exits.

However, when lldb is used as a library, we need a way to manage opened modules to avoid problems with file locks (on some systems) for modules that we no longer need.

It should be possible to record all loaded modules and use ModuleList::RemoveSharedModule and RemoveOrphanSharedModules functions to clear the cache, but these functions are not available in the API. This approach is also way too complicated when we just need to cleanup the library.

The patch adds the following:

  • ModuleList::ClearSharedModules function to clear all shared modules.

  • SBDebugger::SetClearSharedModules(bool) API to enable clearing during SBDebugger::Clear().

  • settings set clear-shared-modules true command line option that is equivalent to SetClearSharedModules(true).

This new behaviour is turned off by default: the debugger does not release these shared modules until the process exits.


Full diff: https://github.com/llvm/llvm-project/pull/147289.diff

10 Files Affected:

  • (modified) lldb/include/lldb/API/SBDebugger.h (+2)
  • (modified) lldb/include/lldb/Core/Debugger.h (+4)
  • (modified) lldb/include/lldb/Core/ModuleList.h (+3)
  • (modified) lldb/source/API/SBDebugger.cpp (+6)
  • (modified) lldb/source/Core/CoreProperties.td (+4)
  • (modified) lldb/source/Core/Debugger.cpp (+13)
  • (modified) lldb/source/Core/ModuleList.cpp (+7)
  • (modified) lldb/test/API/python_api/debugger/TestDebuggerAPI.py (+31)
  • (modified) lldb/unittests/Target/CMakeLists.txt (+1)
  • (added) lldb/unittests/Target/SharedModuleListTest.cpp (+209)
diff --git a/lldb/include/lldb/API/SBDebugger.h b/lldb/include/lldb/API/SBDebugger.h
index 192fbee9c0c6d..6a2f76f2d5685 100644
--- a/lldb/include/lldb/API/SBDebugger.h
+++ b/lldb/include/lldb/API/SBDebugger.h
@@ -319,6 +319,8 @@ class LLDB_API SBDebugger {
 
   bool SetShowInlineDiagnostics(bool);
 
+  bool SetClearSharedModules(bool);
+
   bool SetUseSourceCache(bool use_source_cache);
 
   bool GetUseSourceCache() const;
diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h
index 504f936fe317a..4cf7fc75bafd4 100644
--- a/lldb/include/lldb/Core/Debugger.h
+++ b/lldb/include/lldb/Core/Debugger.h
@@ -373,6 +373,10 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
 
   bool SetShowInlineDiagnostics(bool);
 
+  bool GetClearSharedModules() const;
+
+  bool SetClearSharedModules(bool);
+
   bool LoadPlugin(const FileSpec &spec, Status &error);
 
   void RunIOHandlers();
diff --git a/lldb/include/lldb/Core/ModuleList.h b/lldb/include/lldb/Core/ModuleList.h
index 909ee08f9ba62..587843dd05a4d 100644
--- a/lldb/include/lldb/Core/ModuleList.h
+++ b/lldb/include/lldb/Core/ModuleList.h
@@ -482,6 +482,9 @@ class ModuleList {
 
   static bool RemoveSharedModuleIfOrphaned(const Module *module_ptr);
 
+  /// Empty global cache of modules to release memory, file locks, etc.
+  static void ClearSharedModules();
+
   /// Applies 'callback' to each module in this ModuleList.
   /// If 'callback' returns false, iteration terminates.
   /// The 'module_sp' passed to 'callback' is guaranteed to
diff --git a/lldb/source/API/SBDebugger.cpp b/lldb/source/API/SBDebugger.cpp
index 603e306497841..221c02cfe66ed 100644
--- a/lldb/source/API/SBDebugger.cpp
+++ b/lldb/source/API/SBDebugger.cpp
@@ -1466,6 +1466,12 @@ bool SBDebugger::SetShowInlineDiagnostics(bool value) {
   return (m_opaque_sp ? m_opaque_sp->SetShowInlineDiagnostics(value) : false);
 }
 
+bool SBDebugger::SetClearSharedModules(bool value) {
+  LLDB_INSTRUMENT_VA(this, value);
+
+  return (m_opaque_sp ? m_opaque_sp->SetClearSharedModules(value) : false);
+}
+
 bool SBDebugger::SetUseSourceCache(bool value) {
   LLDB_INSTRUMENT_VA(this, value);
 
diff --git a/lldb/source/Core/CoreProperties.td b/lldb/source/Core/CoreProperties.td
index 53dd333f045c9..1a6ba1a9af84e 100644
--- a/lldb/source/Core/CoreProperties.td
+++ b/lldb/source/Core/CoreProperties.td
@@ -268,4 +268,8 @@ let Definition = "debugger" in {
     Global,
     DefaultFalse,
     Desc<"Controls whether diagnostics can refer directly to the command input, drawing arrows to it. If false, diagnostics will echo the input.">;
+  def ClearSharedModules: Property<"clear-shared-modules", "Boolean">,
+    Global,
+    DefaultFalse,
+    Desc<"Controls whether the debugger clears internal shared modules as it exits.">;
 }
diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp
index ed674ee1275c7..fbd2f37960e19 100644
--- a/lldb/source/Core/Debugger.cpp
+++ b/lldb/source/Core/Debugger.cpp
@@ -700,6 +700,17 @@ bool Debugger::SetShowInlineDiagnostics(bool b) {
   return SetPropertyAtIndex(idx, b);
 }
 
+bool Debugger::GetClearSharedModules() const {
+  const uint32_t idx = ePropertyClearSharedModules;
+  return GetPropertyAtIndexAs<bool>(
+      idx, g_debugger_properties[idx].default_uint_value);
+}
+
+bool Debugger::SetClearSharedModules(bool b) {
+  const uint32_t idx = ePropertyClearSharedModules;
+  return SetPropertyAtIndex(idx, b);
+}
+
 #pragma mark Debugger
 
 // const DebuggerPropertiesSP &
@@ -1092,6 +1103,8 @@ void Debugger::Clear() {
     StopIOHandlerThread();
     StopEventHandlerThread();
     m_listener_sp->Clear();
+    if (GetClearSharedModules())
+      ModuleList::ClearSharedModules();
     for (TargetSP target_sp : m_target_list.Targets()) {
       if (target_sp) {
         if (ProcessSP process_sp = target_sp->GetProcessSP())
diff --git a/lldb/source/Core/ModuleList.cpp b/lldb/source/Core/ModuleList.cpp
index d5ddf6e846112..81a63b7ec541e 100644
--- a/lldb/source/Core/ModuleList.cpp
+++ b/lldb/source/Core/ModuleList.cpp
@@ -1040,6 +1040,13 @@ bool ModuleList::RemoveSharedModuleIfOrphaned(const Module *module_ptr) {
   return GetSharedModuleList().RemoveIfOrphaned(module_ptr);
 }
 
+void ModuleList::ClearSharedModules() {
+  GetSharedModuleList().Clear();
+  Log *log = GetLog(LLDBLog::Modules);
+  if (log != nullptr)
+    LLDB_LOGF(log, "cleared shared modules");
+}
+
 bool ModuleList::LoadScriptingResourcesInTarget(Target *target,
                                                 std::list<Status> &errors,
                                                 Stream &feedback_stream,
diff --git a/lldb/test/API/python_api/debugger/TestDebuggerAPI.py b/lldb/test/API/python_api/debugger/TestDebuggerAPI.py
index 43f45f330ee2a..7ff88eb252967 100644
--- a/lldb/test/API/python_api/debugger/TestDebuggerAPI.py
+++ b/lldb/test/API/python_api/debugger/TestDebuggerAPI.py
@@ -294,3 +294,34 @@ def test_version(self):
 
         self.assertEqual(instance_str, class_str)
         self.assertEqual(class_str, property_str)
+
+    def test_default_SetClearSharedModules(self):
+        """Check that that SBDebugger does not clear shared modules by
+        default.
+        """
+        messages = list()
+        self.dbg.SetLoggingCallback(messages.append)
+        self.runCmd("log enable lldb module")
+        self.dbg.Destroy(self.dbg)
+        self.assertFalse(any("cleared shared modules cache" in msg for msg in messages))
+
+    def test_enable_SetClearSharedModules(self):
+        """Check that SetClearSharedModule(true) clears shared module cache.
+        """
+        messages = list()
+        self.dbg.SetLoggingCallback(messages.append)
+        self.dbg.SetClearSharedModules(True)
+        self.runCmd("log enable lldb module")
+        self.dbg.Destroy(self.dbg)
+        self.assertTrue(any("cleared shared modules" in msg for msg in messages))
+
+    def test_enable_clear_shared_modules(self):
+        """Check that clear-shared-module setting is equivalent
+        to SetClearSharedModules(true).
+        """
+        messages = list()
+        self.dbg.SetLoggingCallback(messages.append)
+        self.runCmd("settings set clear-shared-modules true")
+        self.runCmd("log enable lldb module")
+        self.dbg.Destroy(self.dbg)
+        self.assertTrue(any("cleared shared modules" in msg for msg in messages))
diff --git a/lldb/unittests/Target/CMakeLists.txt b/lldb/unittests/Target/CMakeLists.txt
index 3169339ec699f..aeb552e22ac3f 100644
--- a/lldb/unittests/Target/CMakeLists.txt
+++ b/lldb/unittests/Target/CMakeLists.txt
@@ -12,6 +12,7 @@ add_lldb_unittest(TargetTests
   RemoteAwarePlatformTest.cpp
   StackFrameRecognizerTest.cpp
   SummaryStatisticsTest.cpp
+  SharedModuleListTest.cpp
   FindFileTest.cpp
 
   LINK_COMPONENTS
diff --git a/lldb/unittests/Target/SharedModuleListTest.cpp b/lldb/unittests/Target/SharedModuleListTest.cpp
new file mode 100644
index 0000000000000..837f934f439c3
--- /dev/null
+++ b/lldb/unittests/Target/SharedModuleListTest.cpp
@@ -0,0 +1,209 @@
+//===---------- SharedModuleList.cpp --------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "Plugins/ObjectFile/Breakpad/ObjectFileBreakpad.h"
+#include "Plugins/ObjectFile/ELF/ObjectFileELF.h"
+#include "Plugins/Platform/Android/PlatformAndroid.h"
+#include "Plugins/SymbolFile/Breakpad/SymbolFileBreakpad.h"
+#include "Plugins/SymbolFile/Symtab/SymbolFileSymtab.h"
+#include "TestingSupport/SubsystemRAII.h"
+#include "TestingSupport/TestUtilities.h"
+#include "lldb/Core/Debugger.h"
+#include "lldb/Core/Module.h"
+#include "lldb/Core/PluginManager.h"
+#include "lldb/Host/HostInfo.h"
+#include "lldb/Target/Target.h"
+
+using namespace lldb;
+using namespace lldb_private;
+using namespace lldb_private::platform_android;
+using namespace lldb_private::platform_linux;
+using namespace lldb_private::breakpad;
+using namespace testing;
+
+namespace {
+
+constexpr llvm::StringLiteral k_process_plugin("mock-process-plugin");
+constexpr llvm::StringLiteral k_platform_dir("remote-android");
+constexpr llvm::StringLiteral k_cache_dir(".cache");
+constexpr llvm::StringLiteral k_module_file("AndroidModule.so");
+constexpr llvm::StringLiteral k_arch("aarch64-none-linux");
+constexpr llvm::StringLiteral
+    k_module_uuid("80008338-82A0-51E5-5922-C905D23890DA-BDDEFECC");
+const size_t k_module_size = 3784;
+
+FileSpec GetTestDir() {
+  const auto *info = UnitTest::GetInstance()->current_test_info();
+  FileSpec test_dir = HostInfo::GetProcessTempDir();
+  test_dir.AppendPathComponent(std::string(info->test_case_name()) + "-" +
+                               info->name());
+  std::error_code ec = llvm::sys::fs::create_directory(test_dir.GetPath());
+  EXPECT_FALSE(ec);
+  return test_dir;
+}
+
+FileSpec GetRemotePath() {
+  FileSpec fs("/", FileSpec::Style::posix);
+  fs.AppendPathComponent("bin");
+  fs.AppendPathComponent(k_module_file);
+  return fs;
+}
+
+FileSpec GetUuidView(FileSpec spec) {
+  spec.AppendPathComponent(k_platform_dir);
+  spec.AppendPathComponent(k_cache_dir);
+  spec.AppendPathComponent(k_module_uuid);
+  spec.AppendPathComponent(k_module_file);
+  return spec;
+}
+
+FileSpec BuildCacheDir(const FileSpec &test_dir) {
+  FileSpec uuid_view = GetUuidView(test_dir);
+  std::error_code ec =
+      llvm::sys::fs::create_directories(uuid_view.GetDirectory().GetCString());
+  EXPECT_FALSE(ec);
+  ec = llvm::sys::fs::copy_file(GetInputFilePath(k_module_file),
+                                uuid_view.GetPath().c_str());
+  EXPECT_FALSE(ec);
+  return uuid_view;
+}
+
+ModuleSpec GetTestModuleSpec() {
+  ModuleSpec module_spec(GetRemotePath(), ArchSpec(k_arch));
+  module_spec.GetUUID().SetFromStringRef(k_module_uuid);
+  module_spec.SetObjectSize(k_module_size);
+  return module_spec;
+}
+
+void CheckModule(const ModuleSP &module_sp) {
+  ASSERT_TRUE(module_sp);
+  ASSERT_EQ(module_sp->GetUUID().GetAsString(), k_module_uuid);
+  ASSERT_EQ(module_sp->GetObjectOffset(), 0U);
+  ASSERT_EQ(module_sp->GetPlatformFileSpec(), GetRemotePath());
+}
+
+class MockProcess : public Process {
+public:
+  MockProcess(TargetSP target_sp, ListenerSP listener_sp)
+      : Process(target_sp, listener_sp) {}
+
+  llvm::StringRef GetPluginName() override { return k_process_plugin; };
+
+  bool CanDebug(TargetSP target, bool plugin_specified_by_name) override {
+    return true;
+  }
+
+  Status DoDestroy() override { return Status(); }
+
+  void RefreshStateAfterStop() override {}
+
+  bool DoUpdateThreadList(ThreadList &old_thread_list,
+                          ThreadList &new_thread_list) override {
+    return false;
+  }
+
+  size_t DoReadMemory(addr_t vm_addr, void *buf, size_t size,
+                      Status &error) override {
+    return 0;
+  }
+
+  bool GetModuleSpec(const FileSpec &module_file_spec, const ArchSpec &arch,
+                     ModuleSpec &module_spec) override {
+    module_spec = GetTestModuleSpec();
+    return true;
+  }
+};
+
+ProcessSP MockProcessCreateInstance(TargetSP target_sp, ListenerSP listener_sp,
+                                    const FileSpec *crash_file_path,
+                                    bool can_connect) {
+  return std::make_shared<MockProcess>(target_sp, listener_sp);
+}
+
+class SharedModuleListTest : public testing::Test {
+  SubsystemRAII<FileSystem, HostInfo, ObjectFileBreakpad, ObjectFileELF,
+                PlatformAndroid, PlatformLinux, SymbolFileBreakpad,
+                SymbolFileSymtab>
+      subsystems;
+
+public:
+  void SetUp() override {
+    m_test_dir = GetTestDir();
+
+    // Set module cache directory for PlatformAndroid.
+    PlatformAndroid::GetGlobalPlatformProperties().SetModuleCacheDirectory(
+        m_test_dir);
+
+    // Create Debugger.
+    ArchSpec host_arch("i386-pc-linux");
+    Platform::SetHostPlatform(
+        platform_linux::PlatformLinux::CreateInstance(true, &host_arch));
+    m_debugger_sp = Debugger::CreateInstance();
+    EXPECT_TRUE(m_debugger_sp);
+
+    // Create PlatformAndroid.
+    ArchSpec arch(k_arch);
+    m_platform_sp = PlatformAndroid::CreateInstance(true, &arch);
+    EXPECT_TRUE(m_platform_sp);
+
+    // Create Target.
+    m_debugger_sp->GetTargetList().CreateTarget(*m_debugger_sp, "", arch,
+                                                eLoadDependentsNo,
+                                                m_platform_sp, m_target_sp);
+    EXPECT_TRUE(m_target_sp);
+
+    // Create MockProcess.
+    PluginManager::RegisterPlugin(k_process_plugin, "",
+                                  MockProcessCreateInstance);
+    m_process_sp =
+        m_target_sp->CreateProcess(Listener::MakeListener("test-listener"),
+                                   k_process_plugin, /*crash_file=*/nullptr,
+                                   /*can_connect=*/true);
+    EXPECT_TRUE(m_process_sp);
+
+    m_module_spec = GetTestModuleSpec();
+    m_module_spec_without_uuid = ModuleSpec(GetRemotePath(), ArchSpec(k_arch));
+  }
+
+  void TearDown() override {
+    if (m_module_sp)
+      ModuleList::RemoveSharedModule(m_module_sp);
+  }
+
+protected:
+  FileSpec m_test_dir;
+  DebuggerSP m_debugger_sp;
+  PlatformSP m_platform_sp;
+  TargetSP m_target_sp;
+  ProcessSP m_process_sp;
+  ModuleSpec m_module_spec;
+  ModuleSpec m_module_spec_without_uuid;
+  ModuleSP m_module_sp;
+  int m_callback_call_count = 0;
+};
+
+} // namespace
+
+TEST_F(SharedModuleListTest, TestClear) {
+  FileSpec uuid_view = BuildCacheDir(m_test_dir);
+
+  m_module_sp = m_target_sp->GetOrCreateModule(m_module_spec, /*notify=*/false);
+  CheckModule(m_module_sp);
+  ASSERT_EQ(m_module_sp->GetFileSpec(), uuid_view);
+  ASSERT_FALSE(m_module_sp->GetSymbolFileFileSpec());
+
+  UUID uuid = m_module_sp->GetUUID();
+
+  // Check if the module is cached
+  ASSERT_TRUE(ModuleList::FindSharedModule(uuid));
+
+  // Clear cache and check that it is gone
+  ModuleList::ClearSharedModules();
+  ASSERT_FALSE(ModuleList::FindSharedModule(uuid));
+  m_module_sp = nullptr;
+}

@github-actions
Copy link

github-actions bot commented Jul 7, 2025

✅ With the latest revision this PR passed the Python code formatter.

Shared modules are stored in a global ModuleList cache, and it is
intentionally leaked to avoid doing cleanup when lldb exits.

However, when lldb is used as a library, we need a way to manage
opened modules to avoid problems with file locks (on some systems) for
modules that we no longer need.

It should be possible to record all loaded modules and use
ModuleList::RemoveSharedModule and RemoveOrphanSharedModules functions
to clear the cache, but these functions are not available in the API.
This approach is also way too complicated when we just need to cleanup
the library.

The patch adds the following:

  - ModuleList::ClearSharedModules function to clear all shared
    modules.

  - SBDebugger::SetClearSharedModules(bool) API to enable clearing
    during SBDebugger::Clear().

  - `settings set clear-shared-modules true` command line option
    that is equivalent to SetClearSharedModules(true).

This new behaviour is turned off by default: the debugger does not
release these shared modules until the process exits.
@asavonic asavonic force-pushed the lldb/shared-module-cache branch from 0737ec0 to a01eb19 Compare July 7, 2025 12:46
@asavonic
Copy link
Collaborator Author

asavonic commented Jul 7, 2025

Python code formatter, darker found issues in your code.

Fixed.

@JDevlieghere JDevlieghere requested a review from jimingham July 7, 2025 16:25
@JDevlieghere
Copy link
Member

However, when lldb is used as a library, we need a way to manage opened modules to avoid problems with file locks (on some systems) for modules that we no longer need.

Can you give a concrete example? This seems like its own issue that should be solved rather than worked around. I wouldn't expect us to need to keep files open for as long as a module exists.

It should be possible to record all loaded modules and use ModuleList::RemoveSharedModule and RemoveOrphanSharedModules functions to clear the cache, but these functions are not available in the API. This approach is also way too complicated when we just need to cleanup the library.

This is exposed from the SB API through SBDebugger::MemoryPressureDetected. Not sure what's "complicated" about calling his function, but I do agree that this shouldn't necessary be an explicit operation and would make a good default. The natural place to do this is in SBDebugger::Destroy. You can have many debuggers and it seems like a bad idea to clear the modules when you clear one of them.

@asavonic
Copy link
Collaborator Author

asavonic commented Jul 7, 2025

However, when lldb is used as a library, we need a way to manage opened modules to avoid problems with file locks (on some systems) for modules that we no longer need.

Can you give a concrete example? This seems like its own issue that should be solved rather than worked around. I wouldn't expect us to need to keep files open for as long as a module exists.

Right, it is a separate issue. I'll follow up later.

It should be possible to record all loaded modules and use ModuleList::RemoveSharedModule and RemoveOrphanSharedModules functions to clear the cache, but these functions are not available in the API. This approach is also way too complicated when we just need to cleanup the library.

This is exposed from the SB API through SBDebugger::MemoryPressureDetected. Not sure what's "complicated" about calling his function, but I do agree that this shouldn't necessary be an explicit operation and would make a good default. The natural place to do this is in SBDebugger::Destroy. You can have many debuggers and it seems like a bad idea to clear the modules when you clear one of them.

How should a proper debugger shutdown sequence look like?

  1. Release all SBModule pointers.
  2. Call SBDebugger::MemoryPressureDetected.
  3. Call SBDebugger::Destroy.

SBDebugger::MemoryPressureDetected only removes shared pointers to modules with use_count == 1. Assuming that the calling program releases all its modules in (1), we need to make sure that the debugger does not keep any pointers elsewhere before (2). Can we rely on this?

If any module survives MemoryPressureDetected, they will not be released by SBDebugger::Destroy.

@asavonic
Copy link
Collaborator Author

asavonic commented Jul 7, 2025

The natural place to do this is in SBDebugger::Destroy. You can have many debuggers and it seems like a bad idea to clear the modules when you clear one of them.

Oh, right. If we call MemoryPressureDetected from SBDebugger::Destroy, we can make sure that all modules are released before MemoryPressureDetected.

@JDevlieghere
Copy link
Member

JDevlieghere commented Jul 7, 2025

How should a proper debugger shutdown sequence look like?

  1. Release all SBModule pointers.
  2. Call SBDebugger::MemoryPressureDetected.
  3. Call SBDebugger::Destroy.

Ideally, SBDebugger::Destroy would clean up all the modules in the module cache. You say we don't do that and that we leak them intentionally, which suggests that we don't do that on purpose. It'd be interesting to understand why, but I think that's fundamentally at odds with what you're trying to achieve. If there's a "good" reason and changing that is not an option, SBDebugger::MemoryPressureDetected could be an escape hatch.

SBDebugger::MemoryPressureDetected only removes shared pointers to modules with use_count == 1. Assuming that the calling program releases all its modules in (1), we need to make sure that the debugger does not keep any pointers elsewhere before (2). Can we rely on this?

Yes, if you destroyed all the debuggers and don't hold onto modules yourself, calling MemoryPressureDetected will release all the modules (barring a bug). We actually test for this after every test in our test suite so I'm fairly confident this works correctly.

@JDevlieghere
Copy link
Member

Oh, right. If we call MemoryPressureDetected from SBDebugger::Destroy, we can make sure that all modules are released before MemoryPressureDetected.

Aha, so it already kind-of works the way I was suggesting. Instead of forcefully deleting the modules, we ask nicely to release the ones that have no uses anymore. That seems very reasonable. I still wonder why we can't just boot the whole module cache but I'm sure there is a reason...

@asavonic
Copy link
Collaborator Author

asavonic commented Jul 8, 2025

You say we don't do that and that we leak them intentionally, which suggests that we don't do that on purpose. It'd be interesting to understand why, but I think that's fundamentally at odds with what you're trying to achieve.

The cache was added in 6f4d8af back in 2012. I guess It makes sense for a standalone debugger.

I'll update the patch to use RemoveOrphanSharedModules instead of ClearSharedModules.
SetClearSharedModules option should probably stay to avoid performance regressions for standalone debuggers.

@jimingham
Copy link
Collaborator

No one SBDebugger should force-clear the Shared Module Cache. That's a resource shared among all the SBDebuggers, and no SBDebugger can know what the other debugger's intentions are w.r.t. it. That seems wrong to me.

The most an SBDebugger should do on Destroy or Clear is call MemoryPressureDetected or a similar lower-level API to clear all the modules that it is the only owner of. It should not clear the cache out from under other users.

Even in its current reduced form I don't think this patch is a good idea.

@asavonic
Copy link
Collaborator Author

asavonic commented Jul 9, 2025

Updated the patch to use RemoveOrphanSharedModules instead of force-clearing the cache.
Changed the description as well.

No one SBDebugger should force-clear the Shared Module Cache. That's a resource shared among all the SBDebuggers, and no SBDebugger can know what the other debugger's intentions are w.r.t. it. That seems wrong to me.

Agree.

The most an SBDebugger should do on Destroy or Clear is call MemoryPressureDetected or a similar lower-level API to clear all the modules that it is the only owner of. It should not clear the cache out from under other users.

The last revision should do that. The downside is that the program must release all shared pointers to modules before calling Destroy, or they leak again.

Copy link
Member

@JDevlieghere JDevlieghere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This patch is still trying attempting to clear the module list when clearing the debugger. As Jim said, that doesn't make sense. The module list is global and spans multiple debuggers.

Maybe I misunderstand the problem, but AFAIK all you need to achieve what you're trying to do is:

diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp
index ed674ee1275c..c6390e90f726 100644
--- a/lldb/source/Core/Debugger.cpp
+++ b/lldb/source/Core/Debugger.cpp
@@ -742,6 +742,8 @@ void Debugger::Terminate() {
       g_debugger_list_ptr->clear();
     }
   }
+
+  ModuleList::RemoveOrphanSharedModules(/*mandatory=*/true);
 }
 
 void Debugger::SettingsInitialize() { Target::SettingsInitialize(); }

This is going to remove all the orphaned modules (= all modules if you're not holding on to any shared pointers, which is by design) when you terminate LLDB (i.e. the last thing you do with LLDB).

@asavonic
Copy link
Collaborator Author

Maybe I misunderstand the problem, but AFAIK all you need to achieve what you're trying to do is:

diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp
index ed674ee1275c..c6390e90f726 100644
--- a/lldb/source/Core/Debugger.cpp
+++ b/lldb/source/Core/Debugger.cpp
@@ -742,6 +742,8 @@ void Debugger::Terminate() {
       g_debugger_list_ptr->clear();
     }
   }
+
+  ModuleList::RemoveOrphanSharedModules(/*mandatory=*/true);
 }
 
 void Debugger::SettingsInitialize() { Target::SettingsInitialize(); }

Do we need to tie this to any option, or we can clear these modules unconditionally?
The current patch uses a property, but I don't see how to do this with static Debugger::Terminate.

@JDevlieghere
Copy link
Member

Do we need to tie this to any option, or we can clear these modules unconditionally? The current patch uses a property, but I don't see how to do this with static Debugger::Terminate.

I don't think this needs to be an option if we go with the approach of only removing orphaned modules.

@jimingham
Copy link
Collaborator

jimingham commented Jul 14, 2025

Any Debugger that wants to can call MemoryPressureDetected or RemoveOrphanedSharedModules. That's always been true. But no Debugger should delete modules out from under any other. And appropriately there are no API's that actually do that.

When Debugger::Terminate is called we should go through and close anything remaining on the module list. Note, we should have closed down all the SBDebuggers by the time we get to clearing the global module list in Terminate, so there really shouldn't be anything with a non-zero reference count on it. We test that in the test suite at the end of each test, and except for a few bugs, that's generally true. So the normal RemoveOrphanedSharedModules in Terminate should do the job, but since we're tearing down the library at that point, force removing the rest should be fine, and doesn't need to be controlled by a setting.

Note however that the mandatory in RemoveOrphanedSharedModules is a bit of a misleading parameter name. It isn't a "force these modules to be deleted" parameter, it only governs whether getting the module list lock is mandatory for doing the clearing, or if we should just do a try_lock. When we get to deleting modules, we still obey the reference count.

So if you want to really force that on exit there are NO modules in the shared module cache, I'd suggest adding another API like RemoveAllSharedModules that ignores reference count - but that API should only ever be called from Debugger::Terminate. And it would be nice to stage this so we can first remove all the ones with reference count == 0, then flag a signal that the test suite can pick up if there are any left after that. THEN force remove the remaining ones.

@asavonic
Copy link
Collaborator Author

asavonic commented Sep 2, 2025

@jimingham, thanks a lot for the explanation! I think MemoryPressureDetected is a better solution than what I was trying to do originally.

I'd suggest adding another API like RemoveAllSharedModules that ignores reference count - but that API should only ever be called from Debugger::Terminate.

I almost prepared a patch to add that, but tests highlighted another problem - there seems to be no way to recover from Debugger::Terminate and re-initialize. Initialization fails because Terminate leaves global pointers as non-zero and we get Debugger::Initialize called more than once!. Without a way to re-initialize, this dance around modules in Terminate makes even less sense. I think we can drop this patch, I don't see a practical reason to do it.

Thank you @jimingham and @JDevlieghere for your guidance here.

@asavonic asavonic closed this Sep 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants