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 @@ -160,6 +160,7 @@ /// if compilation arguments changed on calls to forceReparse(). ClangdServer(const GlobalCompilationDatabase &CDB, const ThreadsafeFS &TFS, const Options &Opts, Callbacks *Callbacks = nullptr); + ~ClangdServer(); /// Add a \p File to the list of tracked C++ files or update the contents if /// \p File is already tracked. Also schedules parsing of the AST for it on a @@ -337,6 +338,8 @@ ArrayRef Ranges, Callback CB); + ModuleSet *Modules; + const GlobalCompilationDatabase &CDB; const ThreadsafeFS &TFS; @@ -363,10 +366,7 @@ mutable std::mutex CachedCompletionFuzzyFindRequestMutex; llvm::Optional WorkspaceRoot; - // WorkScheduler has to be the last member, because its destructor has to be - // called before all other members to stop the worker thread that references - // ClangdServer. - TUScheduler WorkScheduler; + llvm::Optional WorkScheduler; }; } // namespace clangd 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 @@ -127,18 +127,16 @@ ClangdServer::ClangdServer(const GlobalCompilationDatabase &CDB, const ThreadsafeFS &TFS, const Options &Opts, Callbacks *Callbacks) - : CDB(CDB), TFS(TFS), + : Modules(Opts.Modules), CDB(CDB), TFS(TFS), DynamicIdx(Opts.BuildDynamicSymbolIndex ? new FileIndex() : nullptr), ClangTidyProvider(Opts.ClangTidyProvider), - WorkspaceRoot(Opts.WorkspaceRoot), - // Pass a callback into `WorkScheduler` to extract symbols from a newly - // parsed file and rebuild the file index synchronously each time an AST - // is parsed. - // FIXME(ioeric): this can be slow and we may be able to index on less - // critical paths. - WorkScheduler( - CDB, TUScheduler::Options(Opts), - std::make_unique(DynamicIdx.get(), Callbacks)) { + WorkspaceRoot(Opts.WorkspaceRoot) { + // Pass a callback into `WorkScheduler` to extract symbols from a newly + // parsed file and rebuild the file index synchronously each time an AST + // is parsed. + WorkScheduler.emplace( + CDB, TUScheduler::Options(Opts), + std::make_unique(DynamicIdx.get(), Callbacks)); // Adds an index to the stack, at higher priority than existing indexes. auto AddIndex = [&](SymbolIndex *Idx) { if (this->Index != nullptr) { @@ -169,6 +167,20 @@ AddIndex(DynamicIdx.get()); } +ClangdServer::~ClangdServer() { + // Destroying TUScheduler first shuts down request threads that might + // otherwise access members concurrently. + // (Nobody can be using TUScheduler because we're on the main thread). + WorkScheduler.reset(); + // Now requests have stopped, we can shut down modules. + if (Modules) { + for (auto &Mod : *Modules) + Mod.stop(); + for (auto &Mod : *Modules) + Mod.blockUntilIdle(Deadline::infinity()); + } +} + void ClangdServer::addDocument(PathRef File, llvm::StringRef Contents, llvm::StringRef Version, WantDiagnostics WantDiags, bool ForceRebuild) { @@ -183,7 +195,7 @@ Inputs.Opts = std::move(Opts); Inputs.Index = Index; Inputs.ClangTidyProvider = ClangTidyProvider; - bool NewFile = WorkScheduler.update(File, Inputs, WantDiags); + bool NewFile = WorkScheduler->update(File, Inputs, WantDiags); // If we loaded Foo.h, we want to make sure Foo.cpp is indexed. if (NewFile && BackgroundIdx) BackgroundIdx->boostRelated(File); @@ -266,7 +278,7 @@ }; } -void ClangdServer::removeDocument(PathRef File) { WorkScheduler.remove(File); } +void ClangdServer::removeDocument(PathRef File) { WorkScheduler->remove(File); } void ClangdServer::codeComplete(PathRef File, Position Pos, const clangd::CodeCompleteOptions &Opts, @@ -320,7 +332,7 @@ }; // We use a potentially-stale preamble because latency is critical here. - WorkScheduler.runWithPreamble( + WorkScheduler->runWithPreamble( "CodeComplete", File, (Opts.RunParser == CodeCompleteOptions::AlwaysParse) ? TUScheduler::Stale @@ -346,8 +358,8 @@ }; // Unlike code completion, we wait for a preamble here. - WorkScheduler.runWithPreamble("SignatureHelp", File, TUScheduler::Stale, - std::move(Action)); + WorkScheduler->runWithPreamble("SignatureHelp", File, TUScheduler::Stale, + std::move(Action)); } void ClangdServer::formatRange(PathRef File, llvm::StringRef Code, Range Rng, @@ -389,7 +401,7 @@ Result.push_back(replacementToEdit(Code, R)); return CB(Result); }; - WorkScheduler.runQuick("FormatOnType", File, std::move(Action)); + WorkScheduler->runQuick("FormatOnType", File, std::move(Action)); } void ClangdServer::prepareRename(PathRef File, Position Pos, @@ -414,14 +426,14 @@ } return CB(*Results); }; - WorkScheduler.runWithAST("PrepareRename", File, std::move(Action)); + WorkScheduler->runWithAST("PrepareRename", File, std::move(Action)); } void ClangdServer::rename(PathRef File, Position Pos, llvm::StringRef NewName, const RenameOptions &Opts, Callback CB) { // A snapshot of all file dirty buffers. - llvm::StringMap Snapshot = WorkScheduler.getAllFileContents(); + llvm::StringMap Snapshot = WorkScheduler->getAllFileContents(); auto Action = [File = File.str(), NewName = NewName.str(), Pos, Opts, CB = std::move(CB), Snapshot = std::move(Snapshot), this](llvm::Expected InpAST) mutable { @@ -456,7 +468,7 @@ RenameFiles.record(R->GlobalChanges.size()); return CB(*R); }; - WorkScheduler.runWithAST("Rename", File, std::move(Action)); + WorkScheduler->runWithAST("Rename", File, std::move(Action)); } // May generate several candidate selections, due to SelectionTree ambiguity. @@ -512,8 +524,8 @@ CB(std::move(Res)); }; - WorkScheduler.runWithAST("EnumerateTweaks", File, std::move(Action), - TUScheduler::InvalidateOnUpdate); + WorkScheduler->runWithAST("EnumerateTweaks", File, std::move(Action), + TUScheduler::InvalidateOnUpdate); } void ClangdServer::applyTweak(PathRef File, Range Sel, StringRef TweakID, @@ -559,7 +571,7 @@ } return CB(std::move(*Effect)); }; - WorkScheduler.runWithAST("ApplyTweak", File, std::move(Action)); + WorkScheduler->runWithAST("ApplyTweak", File, std::move(Action)); } void ClangdServer::locateSymbolAt(PathRef File, Position Pos, @@ -571,7 +583,7 @@ CB(clangd::locateSymbolAt(InpAST->AST, Pos, Index)); }; - WorkScheduler.runWithAST("Definitions", File, std::move(Action)); + WorkScheduler->runWithAST("Definitions", File, std::move(Action)); } void ClangdServer::switchSourceHeader( @@ -591,7 +603,7 @@ return CB(InpAST.takeError()); CB(getCorrespondingHeaderOrSource(Path, InpAST->AST, Index)); }; - WorkScheduler.runWithAST("SwitchHeaderSource", Path, std::move(Action)); + WorkScheduler->runWithAST("SwitchHeaderSource", Path, std::move(Action)); } void ClangdServer::formatCode(PathRef File, llvm::StringRef Code, @@ -612,7 +624,7 @@ tooling::calculateRangesAfterReplacements(IncludeReplaces, Ranges), File))); }; - WorkScheduler.runQuick("Format", File, std::move(Action)); + WorkScheduler->runQuick("Format", File, std::move(Action)); } void ClangdServer::findDocumentHighlights( @@ -624,8 +636,8 @@ CB(clangd::findDocumentHighlights(InpAST->AST, Pos)); }; - WorkScheduler.runWithAST("Highlights", File, std::move(Action), - TUScheduler::InvalidateOnUpdate); + WorkScheduler->runWithAST("Highlights", File, std::move(Action), + TUScheduler::InvalidateOnUpdate); } void ClangdServer::findHover(PathRef File, Position Pos, @@ -639,8 +651,8 @@ CB(clangd::getHover(InpAST->AST, Pos, std::move(Style), Index)); }; - WorkScheduler.runWithAST("Hover", File, std::move(Action), - TUScheduler::InvalidateOnUpdate); + WorkScheduler->runWithAST("Hover", File, std::move(Action), + TUScheduler::InvalidateOnUpdate); } void ClangdServer::typeHierarchy(PathRef File, Position Pos, int Resolve, @@ -654,13 +666,13 @@ File)); }; - WorkScheduler.runWithAST("TypeHierarchy", File, std::move(Action)); + WorkScheduler->runWithAST("TypeHierarchy", File, std::move(Action)); } void ClangdServer::resolveTypeHierarchy( TypeHierarchyItem Item, int Resolve, TypeHierarchyDirection Direction, Callback> CB) { - WorkScheduler.run( + WorkScheduler->run( "Resolve Type Hierarchy", "", [=, CB = std::move(CB)]() mutable { clangd::resolveTypeHierarchy(Item, Resolve, Direction, Index); CB(Item); @@ -675,16 +687,16 @@ return CB(InpAST.takeError()); CB(clangd::prepareCallHierarchy(InpAST->AST, Pos, File)); }; - WorkScheduler.runWithAST("CallHierarchy", File, std::move(Action)); + WorkScheduler->runWithAST("CallHierarchy", File, std::move(Action)); } void ClangdServer::incomingCalls( const CallHierarchyItem &Item, Callback> CB) { - WorkScheduler.run("Incoming Calls", "", - [CB = std::move(CB), Item, this]() mutable { - CB(clangd::incomingCalls(Item, Index)); - }); + WorkScheduler->run("Incoming Calls", "", + [CB = std::move(CB), Item, this]() mutable { + CB(clangd::incomingCalls(Item, Index)); + }); } void ClangdServer::onFileEvent(const DidChangeWatchedFilesParams &Params) { @@ -695,7 +707,7 @@ void ClangdServer::workspaceSymbols( llvm::StringRef Query, int Limit, Callback> CB) { - WorkScheduler.run( + WorkScheduler->run( "getWorkspaceSymbols", /*Path=*/"", [Query = Query.str(), Limit, CB = std::move(CB), this]() mutable { CB(clangd::getWorkspaceSymbols(Query, Limit, Index, @@ -711,8 +723,8 @@ return CB(InpAST.takeError()); CB(clangd::getDocumentSymbols(InpAST->AST)); }; - WorkScheduler.runWithAST("DocumentSymbols", File, std::move(Action), - TUScheduler::InvalidateOnUpdate); + WorkScheduler->runWithAST("DocumentSymbols", File, std::move(Action), + TUScheduler::InvalidateOnUpdate); } void ClangdServer::foldingRanges(llvm::StringRef File, @@ -723,8 +735,8 @@ return CB(InpAST.takeError()); CB(clangd::getFoldingRanges(InpAST->AST)); }; - WorkScheduler.runWithAST("FoldingRanges", File, std::move(Action), - TUScheduler::InvalidateOnUpdate); + WorkScheduler->runWithAST("FoldingRanges", File, std::move(Action), + TUScheduler::InvalidateOnUpdate); } void ClangdServer::findImplementations( @@ -736,7 +748,7 @@ CB(clangd::findImplementations(InpAST->AST, Pos, Index)); }; - WorkScheduler.runWithAST("Implementations", File, std::move(Action)); + WorkScheduler->runWithAST("Implementations", File, std::move(Action)); } void ClangdServer::findReferences(PathRef File, Position Pos, uint32_t Limit, @@ -748,7 +760,7 @@ CB(clangd::findReferences(InpAST->AST, Pos, Limit, Index)); }; - WorkScheduler.runWithAST("References", File, std::move(Action)); + WorkScheduler->runWithAST("References", File, std::move(Action)); } void ClangdServer::symbolInfo(PathRef File, Position Pos, @@ -760,7 +772,7 @@ CB(clangd::getSymbolInfo(InpAST->AST, Pos)); }; - WorkScheduler.runWithAST("SymbolInfo", File, std::move(Action)); + WorkScheduler->runWithAST("SymbolInfo", File, std::move(Action)); } void ClangdServer::semanticRanges(PathRef File, @@ -779,7 +791,7 @@ } CB(std::move(Result)); }; - WorkScheduler.runWithAST("SemanticRanges", File, std::move(Action)); + WorkScheduler->runWithAST("SemanticRanges", File, std::move(Action)); } void ClangdServer::documentLinks(PathRef File, @@ -790,8 +802,8 @@ return CB(InpAST.takeError()); CB(clangd::getDocumentLinks(InpAST->AST)); }; - WorkScheduler.runWithAST("DocumentLinks", File, std::move(Action), - TUScheduler::InvalidateOnUpdate); + WorkScheduler->runWithAST("DocumentLinks", File, std::move(Action), + TUScheduler::InvalidateOnUpdate); } void ClangdServer::semanticHighlights( @@ -802,8 +814,8 @@ return CB(InpAST.takeError()); CB(clangd::getSemanticHighlightings(InpAST->AST)); }; - WorkScheduler.runWithAST("SemanticHighlights", File, std::move(Action), - TUScheduler::InvalidateOnUpdate); + WorkScheduler->runWithAST("SemanticHighlights", File, std::move(Action), + TUScheduler::InvalidateOnUpdate); } void ClangdServer::getAST(PathRef File, Range R, @@ -835,21 +847,21 @@ if (!Success) CB(llvm::None); }; - WorkScheduler.runWithAST("GetAST", File, std::move(Action)); + WorkScheduler->runWithAST("GetAST", File, std::move(Action)); } void ClangdServer::customAction(PathRef File, llvm::StringRef Name, Callback Action) { - WorkScheduler.runWithAST(Name, File, std::move(Action)); + WorkScheduler->runWithAST(Name, File, std::move(Action)); } llvm::StringMap ClangdServer::fileStats() const { - return WorkScheduler.fileStats(); + return WorkScheduler->fileStats(); } LLVM_NODISCARD bool ClangdServer::blockUntilIdleForTest(llvm::Optional TimeoutSeconds) { - return WorkScheduler.blockUntilIdle(timeoutSeconds(TimeoutSeconds)) && + return WorkScheduler->blockUntilIdle(timeoutSeconds(TimeoutSeconds)) && CDB.blockUntilIdle(timeoutSeconds(TimeoutSeconds)) && (!BackgroundIdx || BackgroundIdx->blockUntilIdleForTest(TimeoutSeconds)); @@ -860,7 +872,7 @@ DynamicIdx->profile(MT.child("dynamic_index")); if (BackgroundIdx) BackgroundIdx->profile(MT.child("background_index")); - WorkScheduler.profile(MT.child("tuscheduler")); + WorkScheduler->profile(MT.child("tuscheduler")); } } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/Module.h b/clang-tools-extra/clangd/Module.h --- a/clang-tools-extra/clangd/Module.h +++ b/clang-tools-extra/clangd/Module.h @@ -2,6 +2,7 @@ #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_MODULE_H #include "LSPBinder.h" +#include "support/Threading.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/JSON.h" #include @@ -9,6 +10,7 @@ namespace clang { namespace clangd { +class Deadline; /// A Module contributes a vertical feature to clangd. /// @@ -17,18 +19,29 @@ /// The lifetime of a module is roughly: /// - modules are created before the LSP server, in ClangdMain.cpp /// - these modules are then passed to ClangdLSPServer and ClangdServer -/// - module hooks can be called at this point. +/// - module entrypoints can be called at this point. /// FIXME: We should make some server facilities like TUScheduler and index /// available to those modules after ClangdServer is initalized. -/// - ClangdServer will not be destroyed until all the requests are done. -/// FIXME: Block server shutdown until all the modules are idle. +/// - When shutting down, ClangdServer will wait for all requests to +/// finish, call stop(), and then blockUntilIdle(). /// - modules will be destroyed after ClangdLSPServer is destroyed. /// +/// Modules are not threadsafe in general. A module's entrypoints are: +/// - method handlers registered in initializeLSP() +/// - public methods called directly via ClangdServer.getModule()->... +/// - specific overridable "hook" methods inherited from Module +/// Unless otherwise specified, these are only called on the main thread. +/// /// Conventionally, standard modules live in the `clangd` namespace, and other /// exposed details live in a sub-namespace. class Module { public: - virtual ~Module() = default; + virtual ~Module() { + /// Perform shutdown sequence on destruction in case the ClangdServer was + /// never initialized. Usually redundant, but shutdown is idempotent. + stop(); + blockUntilIdle(Deadline::infinity()); + } /// Called by the server to connect this module to LSP. /// The module should register the methods/notifications/commands it handles, @@ -39,12 +52,27 @@ virtual void initializeLSP(LSPBinder &Bind, const llvm::json::Object &ClientCaps, llvm::json::Object &ServerCaps) {} + + /// Requests that the module cancel background work and go idle soon. + /// Does not block, the caller will call blockUntilIdle() instead. + /// After a module is stop()ed, it should not receive any more requests. + /// Called by the server when shutting down. + /// May be called multiple times, should be idempotent. + virtual void stop() {} + + /// Waits until the module is idle (no background work) or a deadline expires. + /// In general all modules should eventually go idle, though it may take a + /// long time (e.g. background indexing). + /// Modules should go idle quickly if stop() has been called. + /// Called by the server when shutting down, and also by tests. + virtual bool blockUntilIdle(Deadline) { return true; } }; class ModuleSet { std::vector> Modules; public: + ModuleSet() = default; explicit ModuleSet(std::vector> Modules) : Modules(std::move(Modules)) {} @@ -55,6 +83,8 @@ iterator end() { return iterator(Modules.end()); } const_iterator begin() const { return const_iterator(Modules.begin()); } const_iterator end() const { return const_iterator(Modules.end()); } + + void add(std::unique_ptr M) { Modules.push_back(std::move(M)); } }; } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp b/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp --- a/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp +++ b/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp @@ -16,6 +16,7 @@ #include "llvm/ADT/StringRef.h" #include "llvm/Support/Error.h" #include "llvm/Support/JSON.h" +#include "llvm/Testing/Support/Error.h" #include "llvm/Testing/Support/SupportHelpers.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -23,6 +24,8 @@ namespace clang { namespace clangd { namespace { +using llvm::HasValue; +using llvm::Succeeded; MATCHER_P(DiagMessage, M, "") { if (const auto *O = arg.getAsObject()) { @@ -39,6 +42,7 @@ Base = ClangdServer::optsForTest(); // This is needed to we can test index-based operations like call hierarchy. Base.BuildDynamicSymbolIndex = true; + Base.Modules = &Modules; } LSPClient &start() { @@ -66,6 +70,7 @@ MockFS FS; ClangdLSPServer::Options Opts; + ModuleSet Modules; private: // Color logs so we can distinguish them from test output. @@ -233,10 +238,7 @@ void get(const std::nullptr_t &, Callback Reply) { Reply(Value); } int Value = 0; }; - std::vector> Mods; - Mods.push_back(std::make_unique()); - ModuleSet ModSet(std::move(Mods)); - Opts.Modules = &ModSet; + Modules.add(std::make_unique()); auto &Client = start(); Client.notify("add", 2); @@ -244,6 +246,118 @@ EXPECT_EQ(10, Client.call("get", nullptr).takeValue()); } +// Creates a Callback that writes its received value into an Optional. +template +llvm::unique_function)> +capture(llvm::Optional> &Out) { + Out.reset(); + return [&Out](llvm::Expected V) { Out.emplace(std::move(V)); }; +} + +TEST_F(LSPTest, ModulesThreadingTest) { + // A module that does its work on a background thread, and so exercises the + // block/shutdown protocol. + class AsyncCounter : public Module { + bool ShouldStop; + int State = 0; + std::deque> Queue; // null = increment, non-null = read. + std::condition_variable CV; + std::mutex Mu; + std::thread Thread; + + void run() { + std::unique_lock Lock(Mu); + while (true) { + CV.wait(Lock, [&] { return ShouldStop || !Queue.empty(); }); + if (ShouldStop) { + Queue.clear(); + CV.notify_all(); + return; + } + Callback &Task = Queue.front(); + if (Task) + Task(State); + else + ++State; + Queue.pop_front(); + CV.notify_all(); + } + } + + bool blockUntilIdle(Deadline D) override { + std::unique_lock Lock(Mu); + return clangd::wait(Lock, CV, D, [this] { return Queue.empty(); }); + } + + void stop() override { + { + std::lock_guard Lock(Mu); + ShouldStop = true; + } + CV.notify_all(); + } + + public: + AsyncCounter() : Thread([this] { run(); }) {} + ~AsyncCounter() { + // Verify shutdown sequence was performed. + // Real modules would not do this, to be robust to no ClangdServer. + EXPECT_TRUE(ShouldStop) << "ClangdServer should request shutdown"; + EXPECT_EQ(Queue.size(), 0u) << "ClangdServer should block until idle"; + Thread.join(); + } + + // Get the current value, bypassing the queue. + // Used to verify that sync->blockUntilIdle avoids races in tests. + int getSync() { + std::lock_guard Lock(Mu); + return State; + } + + // Get the current value asynchronously. + void get(Callback Reply) { + { + std::lock_guard Lock(Mu); + Queue.push_back(std::move(Reply)); + } + CV.notify_all(); + } + + // Increment the current value asynchronously. + void increment() { + { + std::lock_guard Lock(Mu); + Queue.push_back(nullptr); + } + CV.notify_all(); + } + }; + + auto *Counter = new AsyncCounter(); + Modules.add(std::unique_ptr(Counter)); + auto &Client = start(); + + llvm::Optional> One, Two, Three; + Counter->increment(); + Counter->get(capture(One)); + Counter->increment(); + Counter->get(capture(Two)); + Counter->increment(); + Counter->get(capture(Three)); + EXPECT_THAT_EXPECTED(Client.call("sync", nullptr).take(), Succeeded()); + ASSERT_TRUE(One && Two && Three) << "sync should wait for pending requests"; + EXPECT_EQ(3, Counter->getSync()) << "sync avoids flaky tests"; + // Sanity check: reads and writes are sequenced on the worker thread. + EXPECT_THAT_EXPECTED(*One, HasValue(1)); + EXPECT_THAT_EXPECTED(*Two, HasValue(2)); + EXPECT_THAT_EXPECTED(*Three, HasValue(3)); + // Throw some work on the queue to make sure shutdown blocks on it. + Counter->increment(); + Counter->increment(); + Counter->increment(); + // And immediately shut down. Module destructor verifies that we blocked. +} + } // namespace } // namespace clangd } // namespace clang