: 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<UpdateIndexCallbacks>(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<UpdateIndexCallbacks>(DynamicIdx.get(), Callbacks));
// Adds an index to the stack, at higher priority than existing indexes.
auto AddIndex = [&](SymbolIndex *Idx) {
if (this->Index != nullptr) {
if (Opts.Modules) {
Module::Facilities F{
- this->WorkScheduler,
+ *this->WorkScheduler,
this->Index,
this->TFS,
};
}
}
+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) {
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);
};
}
-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,
};
// We use a potentially-stale preamble because latency is critical here.
- WorkScheduler.runWithPreamble(
+ WorkScheduler->runWithPreamble(
"CodeComplete", File,
(Opts.RunParser == CodeCompleteOptions::AlwaysParse)
? TUScheduler::Stale
};
// 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,
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,
}
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<RenameResult> CB) {
// A snapshot of all file dirty buffers.
- llvm::StringMap<std::string> Snapshot = WorkScheduler.getAllFileContents();
+ llvm::StringMap<std::string> Snapshot = WorkScheduler->getAllFileContents();
auto Action = [File = File.str(), NewName = NewName.str(), Pos, Opts,
CB = std::move(CB), Snapshot = std::move(Snapshot),
this](llvm::Expected<InputsAndAST> InpAST) mutable {
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.
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,
}
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,
CB(clangd::locateSymbolAt(InpAST->AST, Pos, Index));
};
- WorkScheduler.runWithAST("Definitions", File, std::move(Action));
+ WorkScheduler->runWithAST("Definitions", File, std::move(Action));
}
void ClangdServer::switchSourceHeader(
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,
tooling::calculateRangesAfterReplacements(IncludeReplaces, Ranges),
File)));
};
- WorkScheduler.runQuick("Format", File, std::move(Action));
+ WorkScheduler->runQuick("Format", File, std::move(Action));
}
void ClangdServer::findDocumentHighlights(
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,
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,
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<llvm::Optional<TypeHierarchyItem>> CB) {
- WorkScheduler.run(
+ WorkScheduler->run(
"Resolve Type Hierarchy", "", [=, CB = std::move(CB)]() mutable {
clangd::resolveTypeHierarchy(Item, Resolve, Direction, Index);
CB(Item);
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<std::vector<CallHierarchyIncomingCall>> 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) {
void ClangdServer::workspaceSymbols(
llvm::StringRef Query, int Limit,
Callback<std::vector<SymbolInformation>> CB) {
- WorkScheduler.run(
+ WorkScheduler->run(
"getWorkspaceSymbols", /*Path=*/"",
[Query = Query.str(), Limit, CB = std::move(CB), this]() mutable {
CB(clangd::getWorkspaceSymbols(Query, Limit, Index,
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,
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(
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,
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,
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,
}
CB(std::move(Result));
};
- WorkScheduler.runWithAST("SemanticRanges", File, std::move(Action));
+ WorkScheduler->runWithAST("SemanticRanges", File, std::move(Action));
}
void ClangdServer::documentLinks(PathRef File,
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(
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,
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<InputsAndAST> Action) {
- WorkScheduler.runWithAST(Name, File, std::move(Action));
+ WorkScheduler->runWithAST(Name, File, std::move(Action));
}
llvm::StringMap<TUScheduler::FileStats> ClangdServer::fileStats() const {
- return WorkScheduler.fileStats();
+ return WorkScheduler->fileStats();
}
LLVM_NODISCARD bool
// Nothing else can schedule work on TUScheduler, because it's not threadsafe
// and we're blocking the main thread.
- if (!WorkScheduler.blockUntilIdle(timeoutSeconds(TimeoutSeconds)))
+ if (!WorkScheduler->blockUntilIdle(timeoutSeconds(TimeoutSeconds)))
return false;
// Unfortunately we don't have strict topological order between the rest of
return false;
if (BackgroundIdx && !BackgroundIdx->blockUntilIdleForTest(Timeout))
return false;
+ if (Modules && llvm::any_of(*Modules, [&](Module &M) {
+ return !M.blockUntilIdle(timeoutSeconds(Timeout));
+ }))
+ return false;
}
- assert(WorkScheduler.blockUntilIdle(Deadline::zero()) &&
+ assert(WorkScheduler->blockUntilIdle(Deadline::zero()) &&
"Something scheduled work while we're blocking the main thread!");
return true;
}
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
#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"
namespace clang {
namespace clangd {
namespace {
+using llvm::Succeeded;
using testing::ElementsAre;
MATCHER_P(DiagMessage, M, "") {
Base = ClangdServer::optsForTest();
// This is needed to we can test index-based operations like call hierarchy.
Base.BuildDynamicSymbolIndex = true;
+ Base.Modules = &Modules;
}
LSPClient &start() {
MockFS FS;
ClangdLSPServer::Options Opts;
+ ModuleSet Modules;
private:
// Color logs so we can distinguish them from test output.
[Reply(std::move(Reply)), Value(Value)]() mutable { Reply(Value); });
}
};
- ModuleSet Mods;
- Mods.add(std::make_unique<MathModule>());
- Opts.Modules = &Mods;
+ Modules.add(std::make_unique<MathModule>());
auto &Client = start();
Client.notify("add", 2);
ElementsAre(llvm::json::Value(2), llvm::json::Value(10)));
}
+// Creates a Callback that writes its received value into an Optional<Expected>.
+template <typename T>
+llvm::unique_function<void(llvm::Expected<T>)>
+capture(llvm::Optional<llvm::Expected<T>> &Out) {
+ Out.reset();
+ return [&Out](llvm::Expected<T> 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 final : public Module {
+ bool ShouldStop = false;
+ int State = 0;
+ std::deque<Callback<int>> Queue; // null = increment, non-null = read.
+ std::condition_variable CV;
+ std::mutex Mu;
+ std::thread Thread;
+
+ void run() {
+ std::unique_lock<std::mutex> Lock(Mu);
+ while (true) {
+ CV.wait(Lock, [&] { return ShouldStop || !Queue.empty(); });
+ if (ShouldStop) {
+ Queue.clear();
+ CV.notify_all();
+ return;
+ }
+ Callback<int> &Task = Queue.front();
+ if (Task)
+ Task(State);
+ else
+ ++State;
+ Queue.pop_front();
+ CV.notify_all();
+ }
+ }
+
+ bool blockUntilIdle(Deadline D) override {
+ std::unique_lock<std::mutex> Lock(Mu);
+ return clangd::wait(Lock, CV, D, [this] { return Queue.empty(); });
+ }
+
+ void stop() override {
+ {
+ std::lock_guard<std::mutex> 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();
+ }
+
+ void initializeLSP(LSPBinder &Bind, const llvm::json::Object &ClientCaps,
+ llvm::json::Object &ServerCaps) override {
+ Bind.notification("increment", this, &AsyncCounter::increment);
+ }
+
+ // Get the current value, bypassing the queue.
+ // Used to verify that sync->blockUntilIdle avoids races in tests.
+ int getSync() {
+ std::lock_guard<std::mutex> Lock(Mu);
+ return State;
+ }
+
+ // Increment the current value asynchronously.
+ void increment(const std::nullptr_t &) {
+ {
+ std::lock_guard<std::mutex> Lock(Mu);
+ Queue.push_back(nullptr);
+ }
+ CV.notify_all();
+ }
+ };
+
+ Modules.add(std::make_unique<AsyncCounter>());
+ auto &Client = start();
+
+ Client.notify("increment", nullptr);
+ Client.notify("increment", nullptr);
+ Client.notify("increment", nullptr);
+ EXPECT_THAT_EXPECTED(Client.call("sync", nullptr).take(), Succeeded());
+ EXPECT_EQ(3, Modules.get<AsyncCounter>()->getSync());
+ // Throw some work on the queue to make sure shutdown blocks on it.
+ Client.notify("increment", nullptr);
+ Client.notify("increment", nullptr);
+ Client.notify("increment", nullptr);
+ // And immediately shut down. Module destructor verifies that we blocked.
+}
+
} // namespace
} // namespace clangd
} // namespace clang