Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[clang][modules][deps] Add mutex as an alternative to file lock #129751

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

jansvoboda11
Copy link
Contributor

@jansvoboda11 jansvoboda11 commented Mar 4, 2025

The dependency scanner uses implicitly-built Clang modules under the hood. This system was originally designed to handle multiple concurrent processes working on the same module cache, and mutual exclusion was implemented using file locks. The scanner, however, runs within single process, making file locks unnecessary. This patch virtualizes the interface for module cache locking and provides an implementation based on std::shared_mutex. This reduces clang-scan-deps runtime by ~17% on my benchmark.

Note that even when multiple processes run a scan on the same module cache (and therefore don't coordinate efficiently), this should still be correct due to the strict context hash, the write-through InMemoryModuleCache and the logic for rebuilding out-of-date or incompatible modules.

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:modules C++20 modules and Clang Header Modules labels Mar 4, 2025
@llvmbot
Copy link
Member

llvmbot commented Mar 4, 2025

@llvm/pr-subscribers-clang-modules

@llvm/pr-subscribers-clang

Author: Jan Svoboda (jansvoboda11)

Changes

The dependency scanner uses implicitly-built Clang modules under the hood. This system was originally designed to handle multiple concurrent processes working on the same module cache, and mutual exclusion was implemented using file locks. The scanner, however, runs within single process, making file locks unnecessary. This patch virtualizes the interface for module cache locking and provides an implementation based on std::mutex. This reduces clang-scan-deps runtime by ~17% on my benchmark.

Note that even when multiple processes run a scan on the same module cache (and therefore don't coordinate efficiently), this should still be correct due to the strict context hash, the write-through InMemoryModuleCache and the logic for rebuilding out-of-date or incompatible modules.


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

10 Files Affected:

  • (modified) clang/include/clang/Frontend/CompilerInstance.h (+11-2)
  • (added) clang/include/clang/Serialization/ModuleCacheLock.h (+31)
  • (modified) clang/include/clang/Tooling/DependencyScanning/DependencyScanningService.h (+7)
  • (added) clang/include/clang/Tooling/DependencyScanning/ModuleCacheMutexLock.h (+31)
  • (modified) clang/lib/Frontend/CompilerInstance.cpp (+25-22)
  • (modified) clang/lib/Serialization/CMakeLists.txt (+1)
  • (added) clang/lib/Serialization/ModuleCacheLock.cpp (+60)
  • (modified) clang/lib/Tooling/DependencyScanning/CMakeLists.txt (+1)
  • (modified) clang/lib/Tooling/DependencyScanning/DependencyScanningWorker.cpp (+6-1)
  • (added) clang/lib/Tooling/DependencyScanning/ModuleCacheMutexLock.cpp (+81)
diff --git a/clang/include/clang/Frontend/CompilerInstance.h b/clang/include/clang/Frontend/CompilerInstance.h
index 8b539dfc92960..bede160991443 100644
--- a/clang/include/clang/Frontend/CompilerInstance.h
+++ b/clang/include/clang/Frontend/CompilerInstance.h
@@ -53,6 +53,7 @@ class FileManager;
 class FrontendAction;
 class InMemoryModuleCache;
 class Module;
+class ModuleCacheLock;
 class Preprocessor;
 class Sema;
 class SourceManager;
@@ -96,9 +97,12 @@ class CompilerInstance : public ModuleLoader {
   /// The source manager.
   IntrusiveRefCntPtr<SourceManager> SourceMgr;
 
-  /// The cache of PCM files.
+  /// The local in-memory cache of PCM files.
   IntrusiveRefCntPtr<InMemoryModuleCache> ModuleCache;
 
+  /// The lock managing the global cache of PCM files.
+  std::shared_ptr<ModuleCacheLock> ModuleCacheLck;
+
   /// The preprocessor.
   std::shared_ptr<Preprocessor> PP;
 
@@ -209,7 +213,8 @@ class CompilerInstance : public ModuleLoader {
   explicit CompilerInstance(
       std::shared_ptr<PCHContainerOperations> PCHContainerOps =
           std::make_shared<PCHContainerOperations>(),
-      InMemoryModuleCache *SharedModuleCache = nullptr);
+      InMemoryModuleCache *SharedModuleCache = nullptr,
+      std::shared_ptr<ModuleCacheLock> ModuleCacheLck = nullptr);
   ~CompilerInstance() override;
 
   /// @name High-Level Operations
@@ -897,6 +902,10 @@ class CompilerInstance : public ModuleLoader {
   void setExternalSemaSource(IntrusiveRefCntPtr<ExternalSemaSource> ESS);
 
   InMemoryModuleCache &getModuleCache() const { return *ModuleCache; }
+
+  std::shared_ptr<ModuleCacheLock> getModuleCacheLockPtr() const {
+    return ModuleCacheLck;
+  }
 };
 
 } // end namespace clang
diff --git a/clang/include/clang/Serialization/ModuleCacheLock.h b/clang/include/clang/Serialization/ModuleCacheLock.h
new file mode 100644
index 0000000000000..9435735b1a1bd
--- /dev/null
+++ b/clang/include/clang/Serialization/ModuleCacheLock.h
@@ -0,0 +1,31 @@
+#ifndef LLVM_CLANG_SERIALIZATION_MODULECACHELOCK_H
+#define LLVM_CLANG_SERIALIZATION_MODULECACHELOCK_H
+
+#include "clang/Basic/LLVM.h"
+#include "llvm/Support/LockFileManager.h"
+
+namespace clang {
+enum class LockResult { Owned, Shared, Error };
+enum class WaitForUnlockResult { Success, OwnerDied, Timeout };
+
+class ModuleCacheLockManager {
+public:
+  virtual operator LockResult() const = 0;
+  virtual WaitForUnlockResult waitForUnlock() = 0;
+  virtual void unsafeRemoveLock() = 0;
+  virtual std::string getErrorMessage() const = 0;
+  virtual ~ModuleCacheLockManager() = default;
+};
+
+class ModuleCacheLock {
+public:
+  virtual void prepareLock(StringRef ModuleFilename) = 0;
+  virtual std::unique_ptr<ModuleCacheLockManager>
+  tryLock(StringRef ModuleFilename) = 0;
+  virtual ~ModuleCacheLock() = default;
+};
+
+std::shared_ptr<ModuleCacheLock> getModuleCacheFileLock();
+} // namespace clang
+
+#endif
diff --git a/clang/include/clang/Tooling/DependencyScanning/DependencyScanningService.h b/clang/include/clang/Tooling/DependencyScanning/DependencyScanningService.h
index f002f8645d3f6..f6914b90ac67e 100644
--- a/clang/include/clang/Tooling/DependencyScanning/DependencyScanningService.h
+++ b/clang/include/clang/Tooling/DependencyScanning/DependencyScanningService.h
@@ -9,6 +9,7 @@
 #ifndef LLVM_CLANG_TOOLING_DEPENDENCYSCANNING_DEPENDENCYSCANNINGSERVICE_H
 #define LLVM_CLANG_TOOLING_DEPENDENCYSCANNING_DEPENDENCYSCANNINGSERVICE_H
 
+#include "clang/Tooling/DependencyScanning/ModuleCacheMutexLock.h"
 #include "clang/Tooling/DependencyScanning/DependencyScanningFilesystem.h"
 #include "llvm/ADT/BitmaskEnum.h"
 
@@ -95,6 +96,10 @@ class DependencyScanningService {
     return SharedCache;
   }
 
+  ModuleCacheMutexes &getSharedModuleCacheMutexes() {
+    return ModuleCacheMutexes;
+  }
+
 private:
   const ScanningMode Mode;
   const ScanningOutputFormat Format;
@@ -106,6 +111,8 @@ class DependencyScanningService {
   const bool TraceVFS;
   /// The global file system cache.
   DependencyScanningFilesystemSharedCache SharedCache;
+  /// The global module cache mutexes.
+  ModuleCacheMutexes ModuleCacheMutexes;
 };
 
 } // end namespace dependencies
diff --git a/clang/include/clang/Tooling/DependencyScanning/ModuleCacheMutexLock.h b/clang/include/clang/Tooling/DependencyScanning/ModuleCacheMutexLock.h
new file mode 100644
index 0000000000000..312972b26702c
--- /dev/null
+++ b/clang/include/clang/Tooling/DependencyScanning/ModuleCacheMutexLock.h
@@ -0,0 +1,31 @@
+#ifndef LLVM_CLANG_TOOLING_DEPENDENCYSCANNING_MODULECACHEMUTEXLOCK_H
+#define LLVM_CLANG_TOOLING_DEPENDENCYSCANNING_MODULECACHEMUTEXLOCK_H
+
+#include "clang/Serialization/ModuleCacheLock.h"
+#include "llvm/ADT/StringMap.h"
+
+#include <condition_variable>
+
+namespace clang {
+namespace tooling {
+namespace dependencies {
+struct ModuleCacheMutexWrapper {
+  std::mutex Mutex;
+  std::condition_variable CondVar;
+  bool Done = false;
+
+  ModuleCacheMutexWrapper() = default;
+};
+
+struct ModuleCacheMutexes {
+  std::mutex Mutex;
+  llvm::StringMap<std::shared_ptr<ModuleCacheMutexWrapper>> Map;
+};
+
+std::shared_ptr<ModuleCacheLock>
+getModuleCacheMutexLock(ModuleCacheMutexes &Mutexes);
+} // namespace dependencies
+} // namespace tooling
+} // namespace clang
+
+#endif
diff --git a/clang/lib/Frontend/CompilerInstance.cpp b/clang/lib/Frontend/CompilerInstance.cpp
index c11c857ea0606..3d02d42032e21 100644
--- a/clang/lib/Frontend/CompilerInstance.cpp
+++ b/clang/lib/Frontend/CompilerInstance.cpp
@@ -39,6 +39,7 @@
 #include "clang/Serialization/ASTReader.h"
 #include "clang/Serialization/GlobalModuleIndex.h"
 #include "clang/Serialization/InMemoryModuleCache.h"
+#include "clang/Serialization/ModuleCacheLock.h"
 #include "llvm/ADT/IntrusiveRefCntPtr.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/ScopeExit.h"
@@ -66,11 +67,14 @@ using namespace clang;
 
 CompilerInstance::CompilerInstance(
     std::shared_ptr<PCHContainerOperations> PCHContainerOps,
-    InMemoryModuleCache *SharedModuleCache)
+    InMemoryModuleCache *SharedModuleCache,
+    std::shared_ptr<ModuleCacheLock> ModuleCacheLck)
     : ModuleLoader(/* BuildingModule = */ SharedModuleCache),
       Invocation(new CompilerInvocation()),
       ModuleCache(SharedModuleCache ? SharedModuleCache
                                     : new InMemoryModuleCache),
+      ModuleCacheLck(ModuleCacheLck ? ModuleCacheLck
+                                    : getModuleCacheFileLock()),
       ThePCHContainerOperations(std::move(PCHContainerOps)) {}
 
 CompilerInstance::~CompilerInstance() {
@@ -1227,7 +1231,8 @@ compileModuleImpl(CompilerInstance &ImportingInstance, SourceLocation ImportLoc,
   // CompilerInstance::CompilerInstance is responsible for finalizing the
   // buffers to prevent use-after-frees.
   CompilerInstance Instance(ImportingInstance.getPCHContainerOperations(),
-                            &ImportingInstance.getModuleCache());
+                            &ImportingInstance.getModuleCache(),
+                            ImportingInstance.getModuleCacheLockPtr());
   auto &Inv = *Invocation;
   Instance.setInvocation(std::move(Invocation));
 
@@ -1471,47 +1476,45 @@ static bool compileModuleAndReadASTBehindLock(
   Diags.Report(ModuleNameLoc, diag::remark_module_lock)
       << ModuleFileName << Module->Name;
 
-  // FIXME: have LockFileManager return an error_code so that we can
-  // avoid the mkdir when the directory already exists.
-  StringRef Dir = llvm::sys::path::parent_path(ModuleFileName);
-  llvm::sys::fs::create_directories(Dir);
+  auto Lock = ImportingInstance.getModuleCacheLockPtr();
+  Lock->prepareLock(ModuleFileName);
 
   while (true) {
-    llvm::LockFileManager Locked(ModuleFileName);
-    switch (Locked) {
-    case llvm::LockFileManager::LFS_Error:
+    auto Locked = Lock->tryLock(ModuleFileName);
+    switch (*Locked) {
+    case LockResult::Error:
       // ModuleCache takes care of correctness and locks are only necessary for
       // performance. Fallback to building the module in case of any lock
       // related errors.
       Diags.Report(ModuleNameLoc, diag::remark_module_lock_failure)
-          << Module->Name << Locked.getErrorMessage();
+          << Module->Name << Locked->getErrorMessage();
       // Clear out any potential leftover.
-      Locked.unsafeRemoveLockFile();
+      Locked->unsafeRemoveLock();
       [[fallthrough]];
-    case llvm::LockFileManager::LFS_Owned:
+    case LockResult::Owned:
       // We're responsible for building the module ourselves.
       return compileModuleAndReadASTImpl(ImportingInstance, ImportLoc,
                                          ModuleNameLoc, Module, ModuleFileName);
 
-    case llvm::LockFileManager::LFS_Shared:
+    case LockResult::Shared:
       break; // The interesting case.
     }
 
     // Someone else is responsible for building the module. Wait for them to
     // finish.
-    switch (Locked.waitForUnlock()) {
-    case llvm::LockFileManager::Res_Success:
+    switch (Locked->waitForUnlock()) {
+    case WaitForUnlockResult::Success:
       break; // The interesting case.
-    case llvm::LockFileManager::Res_OwnerDied:
+    case WaitForUnlockResult::OwnerDied:
       continue; // try again to get the lock.
-    case llvm::LockFileManager::Res_Timeout:
-      // Since ModuleCache takes care of correctness, we try waiting for
-      // another process to complete the build so clang does not do it done
-      // twice. If case of timeout, build it ourselves.
+    case WaitForUnlockResult::Timeout:
+      // Since the InMemoryModuleCache takes care of correctness, we try waiting
+      // for someone else to complete the build so that it does not happen
+      // twice. In case of timeout, build it ourselves.
       Diags.Report(ModuleNameLoc, diag::remark_module_lock_timeout)
           << Module->Name;
-      // Clear the lock file so that future invocations can make progress.
-      Locked.unsafeRemoveLockFile();
+      // Remove the lock so that future invocations can make progress.
+      Locked->unsafeRemoveLock();
       continue;
     }
 
diff --git a/clang/lib/Serialization/CMakeLists.txt b/clang/lib/Serialization/CMakeLists.txt
index b1fc0345047f2..acbd8669caaed 100644
--- a/clang/lib/Serialization/CMakeLists.txt
+++ b/clang/lib/Serialization/CMakeLists.txt
@@ -18,6 +18,7 @@ add_clang_library(clangSerialization
   GeneratePCH.cpp
   GlobalModuleIndex.cpp
   InMemoryModuleCache.cpp
+  ModuleCacheLock.cpp
   ModuleFile.cpp
   ModuleFileExtension.cpp
   ModuleManager.cpp
diff --git a/clang/lib/Serialization/ModuleCacheLock.cpp b/clang/lib/Serialization/ModuleCacheLock.cpp
new file mode 100644
index 0000000000000..0c59ed7f28b98
--- /dev/null
+++ b/clang/lib/Serialization/ModuleCacheLock.cpp
@@ -0,0 +1,60 @@
+#include "clang/Serialization/ModuleCacheLock.h"
+
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/Path.h"
+
+using namespace clang;
+
+namespace {
+struct ModuleCacheFileLockManager : ModuleCacheLockManager {
+  llvm::LockFileManager Lock;
+
+  ModuleCacheFileLockManager(StringRef ModuleFilename) : Lock(ModuleFilename) {}
+
+  operator LockResult() const override {
+    switch (Lock) {
+    case llvm::LockFileManager::LFS_Owned:
+      return LockResult::Owned;
+    case llvm::LockFileManager::LFS_Shared:
+      return LockResult::Shared;
+    case llvm::LockFileManager::LFS_Error:
+      return LockResult::Error;
+    }
+  }
+
+  WaitForUnlockResult waitForUnlock() override {
+    switch (Lock.waitForUnlock()) {
+    case llvm::LockFileManager::Res_Success:
+      return WaitForUnlockResult::Success;
+    case llvm::LockFileManager::Res_OwnerDied:
+      return WaitForUnlockResult::OwnerDied;
+    case llvm::LockFileManager::Res_Timeout:
+      return WaitForUnlockResult::Timeout;
+    }
+  }
+
+  void unsafeRemoveLock() override { Lock.unsafeRemoveLockFile(); }
+
+  std::string getErrorMessage() const override {
+    return Lock.getErrorMessage();
+  }
+};
+
+struct ModuleCacheFileLock : ModuleCacheLock {
+  void prepareLock(StringRef ModuleFilename) override {
+    // FIXME: have LockFileManager return an error_code so that we can
+    // avoid the mkdir when the directory already exists.
+    StringRef Dir = llvm::sys::path::parent_path(ModuleFilename);
+    llvm::sys::fs::create_directories(Dir);
+  }
+
+  std::unique_ptr<ModuleCacheLockManager>
+  tryLock(StringRef ModuleFilename) override {
+    return std::make_unique<ModuleCacheFileLockManager>(ModuleFilename);
+  }
+};
+} // namespace
+
+std::shared_ptr<ModuleCacheLock> clang::getModuleCacheFileLock() {
+  return std::make_unique<ModuleCacheFileLock>();
+}
diff --git a/clang/lib/Tooling/DependencyScanning/CMakeLists.txt b/clang/lib/Tooling/DependencyScanning/CMakeLists.txt
index 66795b0be0baa..7ae35e69d7000 100644
--- a/clang/lib/Tooling/DependencyScanning/CMakeLists.txt
+++ b/clang/lib/Tooling/DependencyScanning/CMakeLists.txt
@@ -11,6 +11,7 @@ add_clang_library(clangDependencyScanning
   DependencyScanningService.cpp
   DependencyScanningWorker.cpp
   DependencyScanningTool.cpp
+  ModuleCacheMutexLock.cpp
   ModuleDepCollector.cpp
 
   DEPENDS
diff --git a/clang/lib/Tooling/DependencyScanning/DependencyScanningWorker.cpp b/clang/lib/Tooling/DependencyScanning/DependencyScanningWorker.cpp
index 697f26ee5d12f..fdc86d1764eea 100644
--- a/clang/lib/Tooling/DependencyScanning/DependencyScanningWorker.cpp
+++ b/clang/lib/Tooling/DependencyScanning/DependencyScanningWorker.cpp
@@ -22,6 +22,7 @@
 #include "clang/Lex/PreprocessorOptions.h"
 #include "clang/Serialization/ObjectFilePCHContainerReader.h"
 #include "clang/Tooling/DependencyScanning/DependencyScanningService.h"
+#include "clang/Tooling/DependencyScanning/ModuleCacheMutexLock.h"
 #include "clang/Tooling/DependencyScanning/ModuleDepCollector.h"
 #include "clang/Tooling/Tooling.h"
 #include "llvm/ADT/IntrusiveRefCntPtr.h"
@@ -315,7 +316,10 @@ class DependencyScanningAction : public tooling::ToolAction {
     Scanned = true;
 
     // Create a compiler instance to handle the actual work.
-    ScanInstanceStorage.emplace(std::move(PCHContainerOps));
+    ModuleCacheLck =
+        getModuleCacheMutexLock(Service.getSharedModuleCacheMutexes());
+    ScanInstanceStorage.emplace(std::move(PCHContainerOps), nullptr,
+                                ModuleCacheLck);
     CompilerInstance &ScanInstance = *ScanInstanceStorage;
     ScanInstance.setInvocation(std::move(Invocation));
 
@@ -479,6 +483,7 @@ class DependencyScanningAction : public tooling::ToolAction {
   llvm::IntrusiveRefCntPtr<DependencyScanningWorkerFilesystem> DepFS;
   bool DisableFree;
   std::optional<StringRef> ModuleName;
+  std::shared_ptr<ModuleCacheLock> ModuleCacheLck;
   std::optional<CompilerInstance> ScanInstanceStorage;
   std::shared_ptr<ModuleDepCollector> MDC;
   std::vector<std::string> LastCC1Arguments;
diff --git a/clang/lib/Tooling/DependencyScanning/ModuleCacheMutexLock.cpp b/clang/lib/Tooling/DependencyScanning/ModuleCacheMutexLock.cpp
new file mode 100644
index 0000000000000..162b5a4b213f5
--- /dev/null
+++ b/clang/lib/Tooling/DependencyScanning/ModuleCacheMutexLock.cpp
@@ -0,0 +1,81 @@
+#include "clang/Tooling/DependencyScanning/ModuleCacheMutexLock.h"
+
+using namespace clang;
+using namespace tooling;
+using namespace dependencies;
+
+namespace {
+struct ModuleCacheMutexLockManager : ModuleCacheLockManager {
+  ModuleCacheMutexes &Mutexes;
+  StringRef Filename;
+
+  std::shared_ptr<ModuleCacheMutexWrapper> MutexWrapper;
+  bool Owning;
+
+  ModuleCacheMutexLockManager(ModuleCacheMutexes &Mutexes, StringRef Filename)
+      : Mutexes(Mutexes), Filename(Filename) {
+    Owning = false;
+    {
+      std::lock_guard Lock(Mutexes.Mutex);
+      auto &MutexWrapperInMap = Mutexes.Map[Filename];
+      if (!MutexWrapperInMap) {
+        MutexWrapperInMap = std::make_shared<ModuleCacheMutexWrapper>();
+        Owning = true;
+      }
+      // Increment the reference count of the mutex here in the critical section
+      // to guarantee that it's kept alive even when another thread removes it
+      // via \c unsafeRemoveLock().
+      MutexWrapper = MutexWrapperInMap;
+    }
+
+    if (Owning)
+      MutexWrapper->Mutex.lock();
+  }
+
+  ~ModuleCacheMutexLockManager() override {
+    if (Owning) {
+      MutexWrapper->Done = true;
+      MutexWrapper->CondVar.notify_all();
+      MutexWrapper->Mutex.unlock();
+    }
+  }
+
+  operator LockResult() const override {
+    return Owning ? LockResult::Owned : LockResult::Shared;
+  }
+
+  WaitForUnlockResult waitForUnlock() override {
+    assert(!Owning);
+    std::unique_lock Lock(MutexWrapper->Mutex);
+    bool Done = MutexWrapper->CondVar.wait_for(
+        Lock, std::chrono::seconds(90), [&] { return MutexWrapper->Done; });
+    return Done ? WaitForUnlockResult::Success : WaitForUnlockResult::Timeout;
+  }
+
+  void unsafeRemoveLock() override {
+    std::lock_guard Lock(Mutexes.Mutex);
+    Mutexes.Map[Filename].reset();
+  }
+
+  std::string getErrorMessage() const override {
+    llvm_unreachable("ModuleCacheMutexLockManager cannot fail");
+  }
+};
+
+struct ModuleCacheMutexLock : ModuleCacheLock {
+  ModuleCacheMutexes &Mutexes;
+
+  ModuleCacheMutexLock(ModuleCacheMutexes &Mutexes) : Mutexes(Mutexes) {}
+
+  void prepareLock(StringRef Filename) override {}
+
+  std::unique_ptr<ModuleCacheLockManager> tryLock(StringRef Filename) override {
+    return std::make_unique<ModuleCacheMutexLockManager>(Mutexes, Filename);
+  }
+};
+} // namespace
+
+std::shared_ptr<ModuleCacheLock>
+dependencies::getModuleCacheMutexLock(ModuleCacheMutexes &Mutexes) {
+  return std::make_shared<ModuleCacheMutexLock>(Mutexes);
+}

Comment on lines +8 to +9
enum class LockResult { Owned, Shared, Error };
enum class WaitForUnlockResult { Success, OwnerDied, Timeout };
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure if this is better than just using the LockFileManager enums... Any thoughts?

Comment on lines +11 to +26
class ModuleCacheLockManager {
public:
virtual operator LockResult() const = 0;
virtual WaitForUnlockResult waitForUnlock() = 0;
virtual void unsafeRemoveLock() = 0;
virtual std::string getErrorMessage() const = 0;
virtual ~ModuleCacheLockManager() = default;
};

class ModuleCacheLock {
public:
virtual void prepareLock(StringRef ModuleFilename) = 0;
virtual std::unique_ptr<ModuleCacheLockManager>
tryLock(StringRef ModuleFilename) = 0;
virtual ~ModuleCacheLock() = default;
};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure if it'd be neater to add these interfaces into LLVMSupport and have LockFileManager implement them right then and there. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, ModuleCacheLockManager is basically the same interface, and the enums are the same. I do think it makes sense to have that part in llvm/Support. I think the ModuleCacheLock should stay in Clang though.

It would also be nice to modernize the interface (particularly std::string getErrorMessage()), but I think that can happen separately.

Copy link

github-actions bot commented Mar 4, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@@ -95,6 +96,10 @@ class DependencyScanningService {
return SharedCache;
}

ModuleCacheMutexes &getSharedModuleCacheMutexes() {
Copy link
Contributor Author

@jansvoboda11 jansvoboda11 Mar 4, 2025

Choose a reason for hiding this comment

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

Not sure whether the service should hand out ModuleCacheMutexes & (the same thing we do for the shared caching filesystem) or whether it'd be simpler to just hand out ready thread-safe std::shared_ptr<ModuleCacheLock>. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we need shared_ptr here, the lifetime of workers is already bound to the service.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We don't need it, but we currently call std::make_shared<ModuleCacheMutexLock> for each TU, which is unnecessary. We could just do that on service construction and avoid future allocations.

Comment on lines +19 to +34
class ModuleCacheLockManager {
public:
virtual operator LockResult() const = 0;
virtual WaitForUnlockResult waitForUnlock() = 0;
virtual void unsafeRemoveLock() = 0;
virtual std::string getErrorMessage() const = 0;
virtual ~ModuleCacheLockManager() = default;
};

class ModuleCacheLock {
public:
virtual void prepareLock(StringRef ModuleFilename) = 0;
virtual std::unique_ptr<ModuleCacheLockManager>
tryLock(StringRef ModuleFilename) = 0;
virtual ~ModuleCacheLock() = default;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

These two names are a bit confusing, and seem reversed. I would expect a LockManager to handle multiple locks, and just Lock to be the actual lock.

Comment on lines +11 to +26
class ModuleCacheLockManager {
public:
virtual operator LockResult() const = 0;
virtual WaitForUnlockResult waitForUnlock() = 0;
virtual void unsafeRemoveLock() = 0;
virtual std::string getErrorMessage() const = 0;
virtual ~ModuleCacheLockManager() = default;
};

class ModuleCacheLock {
public:
virtual void prepareLock(StringRef ModuleFilename) = 0;
virtual std::unique_ptr<ModuleCacheLockManager>
tryLock(StringRef ModuleFilename) = 0;
virtual ~ModuleCacheLock() = default;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, ModuleCacheLockManager is basically the same interface, and the enums are the same. I do think it makes sense to have that part in llvm/Support. I think the ModuleCacheLock should stay in Clang though.

It would also be nice to modernize the interface (particularly std::string getErrorMessage()), but I think that can happen separately.

class ModuleCacheLockManager {
public:
virtual operator LockResult() const = 0;
virtual WaitForUnlockResult waitForUnlock() = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

Looking more at how this is structured I think this should still have a timeout because of the LockFile implementation. And then it doesn't really hurt to actually do the timeout for the mutex impl, even though actually hitting that has other issues.

@@ -95,6 +96,10 @@ class DependencyScanningService {
return SharedCache;
}

ModuleCacheMutexes &getSharedModuleCacheMutexes() {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we need shared_ptr here, the lifetime of workers is already bound to the service.

Comment on lines +51 to +57
auto &Mutex = [&]() -> std::shared_mutex & {
std::lock_guard Lock(Mutexes.Mutex);
auto &Mutex = Mutexes.Map[Filename];
if (!Mutex)
Mutex = std::make_unique<std::shared_mutex>();
return *Mutex;
}();
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think you need a lambda here, the only difference in lifetime without it is that the return value will be constructed before unlock.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that's the reason I did this - I wanted to avoid an unnecessary allocation in the critical section.


namespace {
struct ModuleCacheMutexLockManager : ModuleCacheLockManager {
std::unique_lock<std::shared_mutex> OwningLock;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a comment here saying something like:

TODO: Consider using std::atomic::{wait, notify_all} when LLVM moves to C++20.

This is the actual operation we want to use, which lowers to futex/ulock.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:modules C++20 modules and Clang Header Modules clang Clang issues not falling into any other category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants