From ba2a4f76b2175f608ac10188e0bf7558f3bab348 Mon Sep 17 00:00:00 2001 From: Jan Svoboda Date: Thu, 2 Apr 2026 13:18:02 -0700 Subject: [PATCH 1/2] [clang][deps] Keep module cache in memory --- .../DependencyScanningService.h | 8 ++ .../DependencyScanning/InProcessModuleCache.h | 18 ++++ .../InProcessModuleCache.cpp | 88 ++++++++++++------- clang/lib/Serialization/ModuleCache.cpp | 17 +++- clang/tools/clang-scan-deps/ClangScanDeps.cpp | 33 ++++--- clang/tools/clang-scan-deps/Opts.td | 2 + 6 files changed, 118 insertions(+), 48 deletions(-) diff --git a/clang/include/clang/DependencyScanning/DependencyScanningService.h b/clang/include/clang/DependencyScanning/DependencyScanningService.h index 0575dbc7cca9a..d35604a839853 100644 --- a/clang/include/clang/DependencyScanning/DependencyScanningService.h +++ b/clang/include/clang/DependencyScanning/DependencyScanningService.h @@ -101,6 +101,9 @@ struct DependencyScanningServiceOptions { bool AsyncScanModules = false; /// The build session timestamp for validate-once-per-build-session logic. std::time_t BuildSessionTimestamp; // = std::chrono::system_clock::now(); + /// Whether to automatically flush the module cache from memory to disk at the + /// end of the service lifetime. + bool FlushModuleCache = true; }; /// The dependency scanning service contains shared configuration and state that @@ -110,6 +113,11 @@ class DependencyScanningService { explicit DependencyScanningService(DependencyScanningServiceOptions Opts) : Opts(std::move(Opts)) {} + ~DependencyScanningService() { + if (Opts.FlushModuleCache) + ModCacheEntries.flush(); + } + const DependencyScanningServiceOptions &getOpts() const { return Opts; } DependencyScanningFilesystemSharedCache &getSharedCache() { diff --git a/clang/include/clang/DependencyScanning/InProcessModuleCache.h b/clang/include/clang/DependencyScanning/InProcessModuleCache.h index 8ad9811606611..de76cbe3f3ad0 100644 --- a/clang/include/clang/DependencyScanning/InProcessModuleCache.h +++ b/clang/include/clang/DependencyScanning/InProcessModuleCache.h @@ -14,8 +14,13 @@ #include #include +#include #include +namespace llvm { +class MemoryBuffer; +} // namespace llvm + namespace clang { namespace dependencies { @@ -26,11 +31,24 @@ struct ModuleCacheEntry { unsigned Generation = 0; std::atomic Timestamp = 0; + + enum { + S_Unknown, + S_Read, + S_Written, + } State = S_Unknown; + /// The buffer that we've either read from disk or written in-process. + std::unique_ptr Buffer; + /// The modification time of the entry. + time_t ModTime = 0; }; struct ModuleCacheEntries { std::mutex Mutex; llvm::StringMap> Map; + + /// Flushes all PCMs built in-process to disk. + void flush(); }; std::shared_ptr diff --git a/clang/lib/DependencyScanning/InProcessModuleCache.cpp b/clang/lib/DependencyScanning/InProcessModuleCache.cpp index 1f9a8e2f85c02..5550ee4a97021 100644 --- a/clang/lib/DependencyScanning/InProcessModuleCache.cpp +++ b/clang/lib/DependencyScanning/InProcessModuleCache.cpp @@ -12,11 +12,26 @@ #include "llvm/Support/AdvisoryLock.h" #include "llvm/Support/Chrono.h" #include "llvm/Support/Error.h" +#include "llvm/Support/FileSystem.h" #include "llvm/Support/IOSandbox.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" using namespace clang; using namespace dependencies; +void ModuleCacheEntries::flush() { + auto BypassSandbox = llvm::sys::sandbox::scopedDisable(); + for (auto &[Path, Entry] : Map) { + if (Entry->State == ModuleCacheEntry::S_Written) { + off_t Size; + time_t ModTime; + // Best-effort: ignore errors (e.g. read-only cache directory). + (void)writeImpl(Path, Entry->Buffer->getMemBufferRef(), Size, ModTime); + } + } +} + namespace { class ReaderWriterLock : public llvm::AdvisoryLock { ModuleCacheEntry &Entry; @@ -81,41 +96,31 @@ class InProcessModuleCache : public ModuleCache { // recreated for each translation unit. InMemoryModuleCache InMemory; + ModuleCacheEntry &getOrCreateEntry(StringRef Filename) { + std::lock_guard Lock(Entries.Mutex); + auto &Entry = Entries.Map[Filename]; + if (!Entry) + Entry = std::make_unique(); + return *Entry; + } + public: InProcessModuleCache(ModuleCacheEntries &Entries) : Entries(Entries) {} std::unique_ptr getLock(StringRef Filename) override { - auto &Entry = [&]() -> ModuleCacheEntry & { - std::lock_guard Lock(Entries.Mutex); - auto &Entry = Entries.Map[Filename]; - if (!Entry) - Entry = std::make_unique(); - return *Entry; - }(); + auto &Entry = getOrCreateEntry(Filename); return std::make_unique(Entry); } std::time_t getModuleTimestamp(StringRef Filename) override { - auto &Timestamp = [&]() -> std::atomic & { - std::lock_guard Lock(Entries.Mutex); - auto &Entry = Entries.Map[Filename]; - if (!Entry) - Entry = std::make_unique(); - return Entry->Timestamp; - }(); + auto &Timestamp = getOrCreateEntry(Filename).Timestamp; return Timestamp.load(); } void updateModuleTimestamp(StringRef Filename) override { // Note: This essentially replaces FS contention with mutex contention. - auto &Timestamp = [&]() -> std::atomic & { - std::lock_guard Lock(Entries.Mutex); - auto &Entry = Entries.Map[Filename]; - if (!Entry) - Entry = std::make_unique(); - return Entry->Timestamp; - }(); + auto &Timestamp = getOrCreateEntry(Filename).Timestamp; Timestamp.store(llvm::sys::toTimeT(std::chrono::system_clock::now())); } @@ -134,22 +139,39 @@ class InProcessModuleCache : public ModuleCache { std::error_code write(StringRef Path, llvm::MemoryBufferRef Buffer, off_t &Size, time_t &ModTime) override { - // This is a compiler-internal input/output, let's bypass the sandbox. - auto BypassSandbox = llvm::sys::sandbox::scopedDisable(); - - // FIXME: This could use an in-memory cache to avoid IO, and only write to - // disk at the end of the scan. - return writeImpl(Path, Buffer, Size, ModTime); + ModuleCacheEntry &Entry = getOrCreateEntry(Path); + std::lock_guard Lock(Entry.Mutex); + if (Entry.State == ModuleCacheEntry::S_Written) { + assert(Entry.Buffer && *Entry.Buffer == Buffer && + "Wrote the same PCM with different contents"); + return {}; + } + Entry.Buffer = + llvm::MemoryBuffer::getMemBufferCopy(Buffer.getBuffer(), Path); + Entry.ModTime = llvm::sys::toTimeT(std::chrono::system_clock::now()); + Entry.State = ModuleCacheEntry::S_Written; + return {}; } Expected> read(StringRef FileName, off_t &Size, time_t &ModTime) override { - // This is a compiler-internal input/output, let's bypass the sandbox. - auto BypassSandbox = llvm::sys::sandbox::scopedDisable(); - - // FIXME: This only needs to go to disk once per build, not in every - // compilation. Introduce in-memory cache. - return readImpl(FileName, Size, ModTime); + ModuleCacheEntry &Entry = getOrCreateEntry(FileName); + std::lock_guard Lock(Entry.Mutex); + if (Entry.State == ModuleCacheEntry::S_Unknown) { + // This is a compiler-internal input/output, let's bypass the sandbox. + auto BypassSandbox = llvm::sys::sandbox::scopedDisable(); + off_t ReadSize; + time_t ReadModTime; + auto ReadBuffer = readImpl(FileName, ReadSize, ReadModTime); + if (!ReadBuffer) + return ReadBuffer.takeError(); + Entry.Buffer = std::move(*ReadBuffer); + Entry.ModTime = ReadModTime; + Entry.State = ModuleCacheEntry::S_Read; + } + Size = Entry.Buffer->getBufferSize(); + ModTime = Entry.ModTime; + return llvm::MemoryBuffer::getMemBuffer(*Entry.Buffer); } }; } // namespace diff --git a/clang/lib/Serialization/ModuleCache.cpp b/clang/lib/Serialization/ModuleCache.cpp index efbaf9765546f..a61724819530a 100644 --- a/clang/lib/Serialization/ModuleCache.cpp +++ b/clang/lib/Serialization/ModuleCache.cpp @@ -24,9 +24,22 @@ const ModuleCacheDirectory *ModuleCache::getDirectoryPtr(StringRef Path) { if (!ByNameIt->second) { // This is a compiler-internal input/output, let's bypass the sandbox. auto BypassSandbox = llvm::sys::sandbox::scopedDisable(); + + // If we cannot get status of the module cache directory, try if trying to + // create it helps. llvm::sys::fs::file_status Status; - if (llvm::sys::fs::status(Path, Status)) - return nullptr; + if (std::error_code EC = llvm::sys::fs::status(Path, Status)) { + // Unless the status failed because the directory does not exist yet. + if (EC != std::errc::no_such_file_or_directory) + return nullptr; + // If we're unable to create the directory. + if (llvm::sys::fs::create_directories(Path)) + return nullptr; + // If we're unable to stat the newly created directory. + if (llvm::sys::fs::status(Path, Status)) + return nullptr; + } + llvm::sys::fs::UniqueID UID = Status.getUniqueID(); auto [ByUIDIt, ByUIDInserted] = ByUID.insert({UID, nullptr}); if (!ByUIDIt->second) diff --git a/clang/tools/clang-scan-deps/ClangScanDeps.cpp b/clang/tools/clang-scan-deps/ClangScanDeps.cpp index fd3d70f9498ee..b7ad6503c7241 100644 --- a/clang/tools/clang-scan-deps/ClangScanDeps.cpp +++ b/clang/tools/clang-scan-deps/ClangScanDeps.cpp @@ -107,6 +107,7 @@ static constexpr bool DoRoundTripDefault = false; #endif static bool RoundTripArgs = DoRoundTripDefault; +static bool NoFlushModuleCache = false; static bool VerbatimArgs = false; static void ParseArgs(int argc, char **argv) { @@ -241,6 +242,8 @@ static void ParseArgs(int argc, char **argv) { RoundTripArgs = Args.hasArg(OPT_round_trip_args); + NoFlushModuleCache = Args.hasArg(OPT_no_flush_module_cache); + VerbatimArgs = Args.hasArg(OPT_verbatim_args); if (const llvm::opt::Arg *A = Args.getLastArgNoClaim(OPT_DASH_DASH)) @@ -1137,26 +1140,30 @@ int clang_scan_deps_main(int argc, char **argv, const llvm::ToolContext &) { Opts.EagerLoadModules = EagerLoadModules; Opts.TraceVFS = Verbose; Opts.AsyncScanModules = AsyncScanModules; - DependencyScanningService Service(std::move(Opts)); + Opts.FlushModuleCache = !NoFlushModuleCache; llvm::Timer T; T.startTimer(); - if (Inputs.size() == 1) { - ScanningTask(Service); - } else { - llvm::DefaultThreadPool Pool(llvm::hardware_concurrency(NumThreads)); + { + DependencyScanningService Service(std::move(Opts)); - if (Verbose) { - llvm::outs() << "Running clang-scan-deps on " << Inputs.size() - << " files using " << Pool.getMaxConcurrency() - << " workers\n"; - } + if (Inputs.size() == 1) { + ScanningTask(Service); + } else { + llvm::DefaultThreadPool Pool(llvm::hardware_concurrency(NumThreads)); - for (unsigned I = 0; I < Pool.getMaxConcurrency(); ++I) - Pool.async([ScanningTask, &Service]() { ScanningTask(Service); }); + if (Verbose) { + llvm::outs() << "Running clang-scan-deps on " << Inputs.size() + << " files using " << Pool.getMaxConcurrency() + << " workers\n"; + } - Pool.wait(); + for (unsigned I = 0; I < Pool.getMaxConcurrency(); ++I) + Pool.async([ScanningTask, &Service]() { ScanningTask(Service); }); + + Pool.wait(); + } } T.stopTimer(); diff --git a/clang/tools/clang-scan-deps/Opts.td b/clang/tools/clang-scan-deps/Opts.td index 21d0de7aa07f2..3cab34ab87599 100644 --- a/clang/tools/clang-scan-deps/Opts.td +++ b/clang/tools/clang-scan-deps/Opts.td @@ -48,6 +48,8 @@ def async_scan_modules : F<"async-scan-modules", "Scan modules asynchronously">; def round_trip_args : F<"round-trip-args", "verify that command-line arguments are canonical by parsing and re-serializing">; +def no_flush_module_cache : F<"no-flush-module-cache", "do not flush the module cache from memory to disk at the end of the scan">; + def verbatim_args : F<"verbatim-args", "Pass commands to the scanner verbatim without adjustments">; def DASH_DASH : Option<["--"], "", KIND_REMAINING_ARGS>; From 0064fd8979d9064abb4b75cb7180bc9b83bf44d8 Mon Sep 17 00:00:00 2001 From: Jan Svoboda Date: Fri, 24 Apr 2026 12:45:00 -0700 Subject: [PATCH 2/2] Document missing modification time --- clang/lib/DependencyScanning/InProcessModuleCache.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/clang/lib/DependencyScanning/InProcessModuleCache.cpp b/clang/lib/DependencyScanning/InProcessModuleCache.cpp index 5550ee4a97021..987703b0c2b6b 100644 --- a/clang/lib/DependencyScanning/InProcessModuleCache.cpp +++ b/clang/lib/DependencyScanning/InProcessModuleCache.cpp @@ -24,6 +24,9 @@ void ModuleCacheEntries::flush() { auto BypassSandbox = llvm::sys::sandbox::scopedDisable(); for (auto &[Path, Entry] : Map) { if (Entry->State == ModuleCacheEntry::S_Written) { + // Note: We could propagate Entry->ModTime to the on-disk file, but + // implicitly-built modules (unlike explicitly-built modules) don't use + // that metadata to refer to imports, rendering this unnecessary. off_t Size; time_t ModTime; // Best-effort: ignore errors (e.g. read-only cache directory).