diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h --- a/clang-tools-extra/clangd/ClangdServer.h +++ b/clang-tools-extra/clangd/ClangdServer.h @@ -102,6 +102,9 @@ /// thread, and callbacks are invoked before "async" functions return. unsigned AsyncThreadsCount = getDefaultAsyncThreadsCount(); + // Number of preambles of closed files to keep in the cache. + unsigned ClosedPreambleCacheSize = 1; + /// AST caching policy. The default is to keep up to 3 ASTs in memory. ASTRetentionPolicy RetentionPolicy; diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp --- a/clang-tools-extra/clangd/ClangdServer.cpp +++ b/clang-tools-extra/clangd/ClangdServer.cpp @@ -137,12 +137,14 @@ Opts.UpdateDebounce = DebouncePolicy::fixed(/*zero*/ {}); Opts.StorePreamblesInMemory = true; Opts.AsyncThreadsCount = 4; // Consistent! + Opts.ClosedPreambleCacheSize = 0; return Opts; } ClangdServer::Options::operator TUScheduler::Options() const { TUScheduler::Options Opts; Opts.AsyncThreadsCount = AsyncThreadsCount; + Opts.ClosedPreambleCacheSize = ClosedPreambleCacheSize; Opts.RetentionPolicy = RetentionPolicy; Opts.StorePreamblesInMemory = StorePreamblesInMemory; Opts.UpdateDebounce = UpdateDebounce; diff --git a/clang-tools-extra/clangd/ParsedAST.cpp b/clang-tools-extra/clangd/ParsedAST.cpp --- a/clang-tools-extra/clangd/ParsedAST.cpp +++ b/clang-tools-extra/clangd/ParsedAST.cpp @@ -242,6 +242,58 @@ std::vector MainFileTokens; }; +llvm::Expected +scanMainFileMacros(llvm::StringRef Contents, + const tooling::CompileCommand &Cmd) { + class EmptyFS : public ThreadsafeFS { + private: + llvm::IntrusiveRefCntPtr viewImpl() const override { + return new llvm::vfs::InMemoryFileSystem; + } + }; + EmptyFS FS; + // Build and run Preprocessor over the preamble. + ParseInputs PI; + PI.Contents = Contents.str(); + PI.TFS = &FS; + PI.CompileCommand = Cmd; + IgnoringDiagConsumer IgnoreDiags; + auto CI = buildCompilerInvocation(PI, IgnoreDiags); + if (!CI) + return error("failed to create compiler invocation"); + CI->getDiagnosticOpts().IgnoreWarnings = true; + auto ContentsBuffer = llvm::MemoryBuffer::getMemBuffer(Contents); + // This means we're scanning (though not preprocessing) the preamble section + // twice. However, it's important to precisely follow the preamble bounds used + // elsewhere. + auto Bounds = ComputePreambleBounds(*CI->getLangOpts(), *ContentsBuffer, 0); + auto PreambleContents = + llvm::MemoryBuffer::getMemBufferCopy(Contents.substr(0, Bounds.Size)); + auto Clang = prepareCompilerInstance( + std::move(CI), nullptr, std::move(PreambleContents), + // Provide an empty FS to prevent preprocessor from performing IO. This + // also implies missing resolved paths for includes. + FS.view(llvm::None), IgnoreDiags); + if (Clang->getFrontendOpts().Inputs.empty()) + return error("compiler instance had no inputs"); + // We are only interested in main file includes. + Clang->getPreprocessorOpts().SingleFileParseMode = true; + PreprocessOnlyAction Action; + if (!Action.BeginSourceFile(*Clang, Clang->getFrontendOpts().Inputs[0])) + return error("failed BeginSourceFile"); + + MainFileMacros Macros; + Clang->getPreprocessor().addPPCallbacks( + std::make_unique(Clang->getSourceManager(), + Macros)); + + if (llvm::Error Err = Action.Execute()) + return std::move(Err); + Action.EndSourceFile(); + + return Macros; +} + } // namespace llvm::Optional @@ -443,8 +495,17 @@ // Copy over the macros in the preamble region of the main file, and combine // with non-preamble macros below. MainFileMacros Macros; - if (Preamble) + if (Preamble && Preamble->CompileCommand.Filename == Filename) { Macros = Preamble->Macros; + } + else if (Preamble) { + auto EM = scanMainFileMacros(Inputs.Contents, Inputs.CompileCommand); + if (!EM) { + elog("Failed to scan macros of {0}: {1}", Filename, EM.takeError()); + } else { + Macros = std::move(*EM); + } + } Clang->getPreprocessor().addPPCallbacks( std::make_unique(Clang->getSourceManager(), Macros)); diff --git a/clang-tools-extra/clangd/TUScheduler.h b/clang-tools-extra/clangd/TUScheduler.h --- a/clang-tools-extra/clangd/TUScheduler.h +++ b/clang-tools-extra/clangd/TUScheduler.h @@ -192,6 +192,9 @@ /// If 0, executes actions synchronously on the calling thread. unsigned AsyncThreadsCount = getDefaultAsyncThreadsCount(); + // Number of preambles of closed files to keep in the cache. + unsigned ClosedPreambleCacheSize = 1; + /// Cache (large) preamble data in RAM rather than temporary files on disk. bool StorePreamblesInMemory = false; @@ -315,6 +318,8 @@ class ASTCache; /// Tracks headers included by open files, to get known-good compile commands. class HeaderIncluderCache; + /// Store all known preambles + class PreambleStore; // The file being built/processed in the current thread. This is a hack in // order to get the file name into the index implementations. Do not depend on @@ -338,6 +343,7 @@ llvm::StringMap> Files; std::unique_ptr IdleASTs; std::unique_ptr HeaderIncluders; + std::unique_ptr Preambles; // None when running tasks synchronously and non-None when running tasks // asynchronously. llvm::Optional PreambleTasks; diff --git a/clang-tools-extra/clangd/TUScheduler.cpp b/clang-tools-extra/clangd/TUScheduler.cpp --- a/clang-tools-extra/clangd/TUScheduler.cpp +++ b/clang-tools-extra/clangd/TUScheduler.cpp @@ -96,6 +96,62 @@ namespace { class ASTWorker; + +bool compileCommandsAreSimilar(const tooling::CompileCommand &LHS, + const tooling::CompileCommand &RHS) { + auto LIt = LHS.CommandLine.begin(); + auto RIt = RHS.CommandLine.begin(); + + auto SkipSpecificArg = [](auto It, auto End, const std::string &Filename) { + while (It != End) { + if (*It == "-o" || *It == "-x") { + // Erase -o and its argument + // Erase -x and its argument: it would prevent using header file + // preamble for a source file + It = std::min(It + 2, End); + } else if (*It == "-c" || It->find("-W") == 0 || *It == "-pedantic") { + // We don't care about those flags + It++; + } else if (It->find(Filename) != std::string::npos) { + // The file we're compiling appears in this argument, drop it to make it + // generic + It++; + } else { + break; + } + } + return It; + }; + + // Iterate the command line, skipping arguments that are specific to the + // file being compiled and that would not influence the premable + // compatiblity. Stop when one iterate reach the or when an argument differs + auto LEnd = LHS.CommandLine.end(); + auto REnd = RHS.CommandLine.end(); + while (true) { + LIt = SkipSpecificArg(LIt, LEnd, LHS.Filename); + RIt = SkipSpecificArg(RIt, REnd, RHS.Filename); + + if (LIt == LEnd || RIt == REnd) { + break; + } + + if (*LIt != *RIt) { + break; + } + + LIt++; + RIt++; + } + + // If both iterator get to the end at the same time, the CL are compatible + bool Compatible = LIt == LEnd && RIt == REnd; + if (Compatible) { + vlog("{0} and {1} are compatible", LHS.Filename, RHS.Filename); + } + return Compatible; +} + } // namespace static clang::clangd::Key kFileBeingProcessed; @@ -301,6 +357,65 @@ } }; +class TUScheduler::PreambleStore { +public: + struct Entry { + std::shared_ptr Preamble; + size_t Score; + Path FileName; + + bool operator>(const Entry &Other) const { return Score > Other.Score; } + }; + + explicit PreambleStore(size_t ClosedPreamblesToKeep): ClosedPreamblesToKeep(ClosedPreamblesToKeep) {} + + auto getAll() { + std::unique_lock Lock(Mut); + return Store; + } + + void push(PathRef FileName, std::shared_ptr Preamble) { + std::unique_lock Lock(Mut); + auto It = llvm::find_if( + Store, [&](const auto &Item) { return Item.FileName == FileName; }); + if (It == Store.end()) { + Store.push_back(Entry{std::move(Preamble), 0, FileName.str()}); + popWorstPreambles(); + } + else { + It->Preamble = std::move(Preamble); + } + vlog("Store contains {0} preambles", Store.size()); + } + + void hit(PathRef FileName) { + std::unique_lock Lock(Mut); + auto It = llvm::find_if( + Store, [&](const auto &Item) { return Item.FileName == FileName; }); + if (It == Store.end()) { + return; + } + It->Score++; + } + +private: + void popWorstPreambles() { + auto Begin = llvm::partition( + Store, [](const auto &Item) { return Item.Preamble.use_count() > 1; }); + if (static_cast(std::distance(Begin, Store.end())) <= + ClosedPreamblesToKeep) { + return; + } + auto Nth = Begin + ClosedPreamblesToKeep; + std::nth_element(Begin, Nth, Store.end(), std::greater()); + Store.erase(Nth, Store.end()); + } + + std::mutex Mut; + std::vector Store; + size_t ClosedPreamblesToKeep; +}; + namespace { bool isReliable(const tooling::CompileCommand &Cmd) { @@ -500,7 +615,8 @@ ASTWorker(PathRef FileName, const GlobalCompilationDatabase &CDB, TUScheduler::ASTCache &LRUCache, TUScheduler::HeaderIncluderCache &HeaderIncluders, - Semaphore &Barrier, bool RunSync, const TUScheduler::Options &Opts, + TUScheduler::PreambleStore &Preambles, Semaphore &Barrier, + bool RunSync, const TUScheduler::Options &Opts, ParsingCallbacks &Callbacks); public: @@ -513,8 +629,9 @@ create(PathRef FileName, const GlobalCompilationDatabase &CDB, TUScheduler::ASTCache &IdleASTs, TUScheduler::HeaderIncluderCache &HeaderIncluders, - AsyncTaskRunner *Tasks, Semaphore &Barrier, - const TUScheduler::Options &Opts, ParsingCallbacks &Callbacks); + TUScheduler::PreambleStore &Preambles, AsyncTaskRunner *Tasks, + Semaphore &Barrier, const TUScheduler::Options &Opts, + ParsingCallbacks &Callbacks); ~ASTWorker(); void update(ParseInputs Inputs, WantDiagnostics, bool ContentChanged); @@ -526,6 +643,8 @@ std::shared_ptr getPossiblyStalePreamble( std::shared_ptr *ASTSignals = nullptr) const; + std::shared_ptr getBestPreamble( + std::shared_ptr *ASTSignals = nullptr) const; /// Used to inform ASTWorker about a new preamble build by PreambleThread. /// Diagnostics are only published through this callback. This ensures they @@ -592,6 +711,9 @@ /// Should the first task in the queue be skipped instead of run? bool shouldSkipHeadLocked() const; + /// Get all preambles that may be used to accelerate the AST build + std::vector getCompatiblePreambles() const; + struct Request { llvm::unique_function Action; std::string Name; @@ -606,6 +728,7 @@ /// Handles retention of ASTs. TUScheduler::ASTCache &IdleASTs; TUScheduler::HeaderIncluderCache &HeaderIncluders; + TUScheduler::PreambleStore &Preambles; const bool RunSync; /// Time to wait after an update to see whether another update obsoletes it. const DebouncePolicy UpdateDebounce; @@ -708,12 +831,12 @@ ASTWorker::create(PathRef FileName, const GlobalCompilationDatabase &CDB, TUScheduler::ASTCache &IdleASTs, TUScheduler::HeaderIncluderCache &HeaderIncluders, - AsyncTaskRunner *Tasks, Semaphore &Barrier, - const TUScheduler::Options &Opts, + TUScheduler::PreambleStore &Preambles, AsyncTaskRunner *Tasks, + Semaphore &Barrier, const TUScheduler::Options &Opts, ParsingCallbacks &Callbacks) { - std::shared_ptr Worker( - new ASTWorker(FileName, CDB, IdleASTs, HeaderIncluders, Barrier, - /*RunSync=*/!Tasks, Opts, Callbacks)); + std::shared_ptr Worker(new ASTWorker( + FileName, CDB, IdleASTs, HeaderIncluders, Preambles, Barrier, + /*RunSync=*/!Tasks, Opts, Callbacks)); if (Tasks) { Tasks->runAsync("ASTWorker:" + llvm::sys::path::filename(FileName), [Worker]() { Worker->run(); }); @@ -727,10 +850,11 @@ ASTWorker::ASTWorker(PathRef FileName, const GlobalCompilationDatabase &CDB, TUScheduler::ASTCache &LRUCache, TUScheduler::HeaderIncluderCache &HeaderIncluders, - Semaphore &Barrier, bool RunSync, - const TUScheduler::Options &Opts, + TUScheduler::PreambleStore &Preambles, Semaphore &Barrier, + bool RunSync, const TUScheduler::Options &Opts, ParsingCallbacks &Callbacks) - : IdleASTs(LRUCache), HeaderIncluders(HeaderIncluders), RunSync(RunSync), + : IdleASTs(LRUCache), HeaderIncluders(HeaderIncluders), + Preambles(Preambles), RunSync(RunSync), UpdateDebounce(Opts.UpdateDebounce), FileName(FileName), ContextProvider(Opts.ContextProvider), CDB(CDB), Callbacks(Callbacks), Barrier(Barrier), Done(false), Status(FileName, Callbacks), @@ -839,14 +963,18 @@ PreamblePeer.update(std::move(Invocation), std::move(Inputs), std::move(CompilerInvocationDiags), WantDiags); + std::unique_lock Lock(Mutex); PreambleCV.wait(Lock, [this] { // Block until we reiceve a preamble request, unless a preamble already // exists, as patching an empty preamble would imply rebuilding it from // scratch. + // It is also acceptable to unblock is we have compatible preambles + // that may be used to accelerate the AST build. // We block here instead of the consumer to prevent any deadlocks. Since // LatestPreamble is only populated by ASTWorker thread. - return LatestPreamble || !PreambleRequests.empty() || Done; + return LatestPreamble || !PreambleRequests.empty() || + !getCompatiblePreambles().empty() || Done; }); return; }; @@ -874,13 +1002,13 @@ vlog("ASTWorker rebuilding evicted AST to run {0}: {1} version {2}", Name, FileName, FileInputs.Version); // FIXME: We might need to build a patched ast once preamble thread starts - // running async. Currently getPossiblyStalePreamble below will always + // running async. Currently getBestPreamble below will always // return a compatible preamble as ASTWorker::update blocks. llvm::Optional NewAST; if (Invocation) { NewAST = ParsedAST::build(FileName, FileInputs, std::move(Invocation), CompilerInvocationDiagConsumer.take(), - getPossiblyStalePreamble()); + getBestPreamble()); ++ASTBuildCount; } AST = NewAST ? std::make_unique(std::move(*NewAST)) : nullptr; @@ -963,6 +1091,7 @@ else LatestPreamble = std::move(Preamble); } + Preambles.push(FileName, *LatestPreamble); // Notify anyone waiting for a preamble. PreambleCV.notify_all(); // Give up our ownership to old preamble before starting expensive AST @@ -1105,6 +1234,51 @@ return LatestPreamble ? *LatestPreamble : nullptr; } +std::vector +ASTWorker::getCompatiblePreambles() const { + auto AllPreamblesEntries = Preambles.getAll(); + auto End = llvm::remove_if(AllPreamblesEntries, [&](const auto &Item) { + return !compileCommandsAreSimilar(FileInputs.CompileCommand, + Item.Preamble->CompileCommand) || + // Using the preamble of a file that include the file we're + // processing will only yield partial results Unless we can tackle + // this issue, just ignore this preamble + Item.Preamble->Includes.includeDepth(Item.FileName).count(FileName); + }); + AllPreamblesEntries.erase(End, AllPreamblesEntries.end()); + return AllPreamblesEntries; +} + +std::shared_ptr ASTWorker::getBestPreamble( + std::shared_ptr *ASTSignals) const { + if (auto Preamble = getPossiblyStalePreamble(ASTSignals)) { + vlog("Best premable is the real one"); + return Preamble; + } + + auto CompatiblePreambles = getCompatiblePreambles(); + vlog("Looking for the best of {0} preambles ...", CompatiblePreambles.size()); + if (CompatiblePreambles.empty()) { + vlog("No compatible preamble"); + return nullptr; + } + + auto Best = CompatiblePreambles.begin(); + auto BestPatch = PreamblePatch::create(FileName, FileInputs, *Best->Preamble); + + for (auto It = Best + 1; It != CompatiblePreambles.end(); ++It) { + auto Patch = PreamblePatch::create(FileName, FileInputs, *It->Preamble); + if (Patch.preambleIncludes().size() > BestPatch.preambleIncludes().size()) { + BestPatch = std::move(Patch); + Best = It; + } + } + + vlog("Using preamble from: {0}", Best->FileName); + Preambles.hit(Best->FileName); + return Best->Preamble; +} + void ASTWorker::waitForFirstPreamble() const { std::unique_lock Lock(Mutex); PreambleCV.wait(Lock, [this] { return LatestPreamble.hasValue() || Done; }); @@ -1455,7 +1629,8 @@ Barrier(Opts.AsyncThreadsCount), QuickRunBarrier(Opts.AsyncThreadsCount), IdleASTs( std::make_unique(Opts.RetentionPolicy.MaxRetainedASTs)), - HeaderIncluders(std::make_unique()) { + HeaderIncluders(std::make_unique()), + Preambles(std::make_unique(Opts.ClosedPreambleCacheSize)) { // Avoid null checks everywhere. if (!Opts.ContextProvider) { this->Opts.ContextProvider = [](llvm::StringRef) { @@ -1497,7 +1672,7 @@ if (!FD) { // Create a new worker to process the AST-related tasks. ASTWorkerHandle Worker = - ASTWorker::create(File, CDB, *IdleASTs, *HeaderIncluders, + ASTWorker::create(File, CDB, *IdleASTs, *HeaderIncluders, *Preambles, WorkerThreads ? WorkerThreads.getPointer() : nullptr, Barrier, Opts, *Callbacks); FD = std::unique_ptr( @@ -1591,7 +1766,7 @@ SPAN_ATTACH(Tracer, "file", File); std::shared_ptr Signals; std::shared_ptr Preamble = - It->second->Worker->getPossiblyStalePreamble(&Signals); + It->second->Worker->getBestPreamble(&Signals); WithContext WithProvidedContext(Opts.ContextProvider(File)); Action(InputsAndPreamble{It->second->Contents, It->second->Worker->getCurrentCompileCommand(), @@ -1614,7 +1789,7 @@ Worker->waitForFirstPreamble(); } std::shared_ptr Signals; - Preamble = Worker->getPossiblyStalePreamble(&Signals); + Preamble = Worker->getBestPreamble(&Signals); std::lock_guard BarrierLock(Barrier); WithContext Guard(std::move(Ctx)); diff --git a/clang-tools-extra/clangd/tool/ClangdMain.cpp b/clang-tools-extra/clangd/tool/ClangdMain.cpp --- a/clang-tools-extra/clangd/tool/ClangdMain.cpp +++ b/clang-tools-extra/clangd/tool/ClangdMain.cpp @@ -327,6 +327,13 @@ init(getDefaultAsyncThreadsCount()), }; +opt ClosedPreambleCacheSize{ + "closed-preamble-cache-size", + cat(Misc), + desc("Number of preambles of closed files to keep in the cache."), + init(1), +}; + opt IndexFile{ "index-file", cat(Misc), @@ -846,6 +853,7 @@ Opts.StaticIndex = PAI.get(); } Opts.AsyncThreadsCount = WorkerThreadsCount; + Opts.ClosedPreambleCacheSize = ClosedPreambleCacheSize; Opts.FoldingRanges = FoldingRanges; Opts.InlayHints = InlayHints; Opts.MemoryCleanup = getMemoryCleanupFunction();