Skip to content

Commit

Permalink
[clangd] Cleanup: make diagnostics callbacks from TUScheduler non-racy
Browse files Browse the repository at this point in the history
Summary:
Previously, removeDoc followed by an addDoc to TUScheduler resulted in
racy diagnostic responses, i.e. the old dianostics could be delivered
to the client after the new ones by TUScheduler.

To workaround this, we tracked a version number in ClangdServer and
discarded stale diagnostics. After this commit, the TUScheduler will
stop delivering diagnostics for removed files and the workaround in
ClangdServer is not required anymore.

Reviewers: sammccall

Reviewed By: sammccall

Subscribers: javed.absar, ioeric, MaskRay, jkorous, arphaman, jfb, kadircet, cfe-commits

Differential Revision: https://reviews.llvm.org/D54829

llvm-svn: 347468
  • Loading branch information
ilya-biryukov committed Nov 22, 2018
1 parent c0ac4bb commit c76394b
Show file tree
Hide file tree
Showing 6 changed files with 40 additions and 41 deletions.
26 changes: 3 additions & 23 deletions clang-tools-extra/clangd/ClangdServer.cpp
Expand Up @@ -133,19 +133,17 @@ ClangdServer::ClangdServer(const GlobalCompilationDatabase &CDB,

void ClangdServer::addDocument(PathRef File, StringRef Contents,
WantDiagnostics WantDiags) {
DocVersion Version = ++InternalVersion[File];
ParseInputs Inputs = {getCompileCommand(File), FSProvider.getFileSystem(),
Contents.str()};

Path FileStr = File.str();
WorkScheduler.update(File, std::move(Inputs), WantDiags,
[this, FileStr, Version](std::vector<Diag> Diags) {
consumeDiagnostics(FileStr, Version, std::move(Diags));
[this, FileStr](std::vector<Diag> Diags) {
DiagConsumer.onDiagnosticsReady(FileStr,
std::move(Diags));
});
}

void ClangdServer::removeDocument(PathRef File) {
++InternalVersion[File];
WorkScheduler.remove(File);
}

Expand Down Expand Up @@ -444,24 +442,6 @@ void ClangdServer::findHover(PathRef File, Position Pos,
WorkScheduler.runWithAST("Hover", File, Bind(Action, std::move(CB)));
}

void ClangdServer::consumeDiagnostics(PathRef File, DocVersion Version,
std::vector<Diag> Diags) {
// We need to serialize access to resulting diagnostics to avoid calling
// `onDiagnosticsReady` in the wrong order.
std::lock_guard<std::mutex> DiagsLock(DiagnosticsMutex);
DocVersion &LastReportedDiagsVersion = ReportedDiagnosticVersions[File];

// FIXME(ibiryukov): get rid of '<' comparison here. In the current
// implementation diagnostics will not be reported after version counters'
// overflow. This should not happen in practice, since DocVersion is a
// 64-bit unsigned integer.
if (Version < LastReportedDiagsVersion)
return;
LastReportedDiagsVersion = Version;

DiagConsumer.onDiagnosticsReady(File, std::move(Diags));
}

tooling::CompileCommand ClangdServer::getCompileCommand(PathRef File) {
trace::Span Span("GetCompileCommand");
Optional<tooling::CompileCommand> C = CDB.getCompileCommand(File);
Expand Down
17 changes: 2 additions & 15 deletions clang-tools-extra/clangd/ClangdServer.h
Expand Up @@ -125,7 +125,8 @@ class ClangdServer {
WantDiagnostics WD = WantDiagnostics::Auto);

/// Remove \p File from list of tracked files, schedule a request to free
/// resources associated with it.
/// resources associated with it. Pending diagnostics for closed files may not
/// be delivered, even if requested with WantDiags::Auto or WantDiags::Yes.
void removeDocument(PathRef File);

/// Run code completion for \p File at \p Pos.
Expand Down Expand Up @@ -222,20 +223,12 @@ class ClangdServer {
formatCode(llvm::StringRef Code, PathRef File,
ArrayRef<tooling::Range> Ranges);

typedef uint64_t DocVersion;

void consumeDiagnostics(PathRef File, DocVersion Version,
std::vector<Diag> Diags);

tooling::CompileCommand getCompileCommand(PathRef File);

const GlobalCompilationDatabase &CDB;
DiagnosticsConsumer &DiagConsumer;
const FileSystemProvider &FSProvider;

/// Used to synchronize diagnostic responses for added and removed files.
llvm::StringMap<DocVersion> InternalVersion;

Path ResourceDir;
// The index used to look up symbols. This could be:
// - null (all index functionality is optional)
Expand All @@ -255,12 +248,6 @@ class ClangdServer {

llvm::Optional<std::string> WorkspaceRoot;
std::shared_ptr<PCHContainerOperations> PCHs;
/// Used to serialize diagnostic callbacks.
/// FIXME(ibiryukov): get rid of an extra map and put all version counters
/// into CppFile.
std::mutex DiagnosticsMutex;
/// Maps from a filename to the latest version of reported diagnostics.
llvm::StringMap<DocVersion> ReportedDiagnosticVersions;
// 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.
Expand Down
25 changes: 24 additions & 1 deletion clang-tools-extra/clangd/TUScheduler.cpp
Expand Up @@ -252,6 +252,13 @@ class ASTWorker {
bool Done; /* GUARDED_BY(Mutex) */
std::deque<Request> Requests; /* GUARDED_BY(Mutex) */
mutable std::condition_variable RequestsCV;
/// Guards a critical section for running the diagnostics callbacks.
std::mutex DiagsMu;
// Used to prevent remove document + leading to out-of-order diagnostics:
// The lifetime of the old/new ASTWorkers will overlap, but their handles
// don't. When the old handle is destroyed, the old worker will stop reporting
// diagnostics.
bool ReportDiagnostics = true; /* GUARDED_BY(DiagMu) */
};

/// A smart-pointer-like class that points to an active ASTWorker.
Expand Down Expand Up @@ -406,6 +413,14 @@ void ASTWorker::update(ParseInputs Inputs, WantDiagnostics WantDiags,
if (WantDiags == WantDiagnostics::No)
return;

{
std::lock_guard<std::mutex> Lock(DiagsMu);
// No need to rebuild the AST if we won't send the diagnotics. However,
// note that we don't prevent preamble rebuilds.
if (!ReportDiagnostics)
return;
}

// Get the AST for diagnostics.
Optional<std::unique_ptr<ParsedAST>> AST = IdleASTs.take(this);
if (!AST) {
Expand All @@ -418,7 +433,11 @@ void ASTWorker::update(ParseInputs Inputs, WantDiagnostics WantDiags,
// spam us with updates.
// Note *AST can still be null if buildAST fails.
if (*AST) {
OnUpdated((*AST)->getDiagnostics());
{
std::lock_guard<std::mutex> Lock(DiagsMu);
if (ReportDiagnostics)
OnUpdated((*AST)->getDiagnostics());
}
trace::Span Span("Running main AST callback");
Callbacks.onMainAST(FileName, **AST);
DiagsWereReported = true;
Expand Down Expand Up @@ -512,6 +531,10 @@ std::size_t ASTWorker::getUsedBytes() const {
bool ASTWorker::isASTCached() const { return IdleASTs.getUsedBytes(this) != 0; }

void ASTWorker::stop() {
{
std::lock_guard<std::mutex> Lock(DiagsMu);
ReportDiagnostics = false;
}
{
std::lock_guard<std::mutex> Lock(Mutex);
assert(!Done && "stop() called twice");
Expand Down
3 changes: 2 additions & 1 deletion clang-tools-extra/clangd/TUScheduler.h
Expand Up @@ -108,7 +108,8 @@ class TUScheduler {
llvm::unique_function<void(std::vector<Diag>)> OnUpdated);

/// Remove \p File from the list of tracked files and schedule removal of its
/// resources.
/// resources. Pending diagnostics for closed files may not be delivered, even
/// if requested with WantDiags::Auto or WantDiags::Yes.
void remove(PathRef File);

/// Schedule an async task with no dependencies.
Expand Down
3 changes: 2 additions & 1 deletion clang-tools-extra/unittests/clangd/ClangdTests.cpp
Expand Up @@ -748,7 +748,8 @@ int d;
BlockingRequests[RequestIndex]();
}
}
} // Wait for ClangdServer to shutdown before proceeding.
ASSERT_TRUE(Server.blockUntilIdleForTest());
}

// Check some invariants about the state of the program.
std::vector<FileStat> Stats = DiagConsumer.takeFileStats();
Expand Down
7 changes: 7 additions & 0 deletions clang-tools-extra/unittests/clangd/TUSchedulerTests.cpp
Expand Up @@ -124,6 +124,8 @@ TEST_F(TUSchedulerTests, WantDiagnostics) {
S.update(Path, getInputs(Path, "auto (produces)"), WantDiagnostics::Auto,
[&](std::vector<Diag> Diags) { ++CallbackCount; });
Ready.notify();

ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10)));
}
EXPECT_EQ(2, CallbackCount);
}
Expand All @@ -147,6 +149,8 @@ TEST_F(TUSchedulerTests, Debounce) {
std::this_thread::sleep_for(std::chrono::seconds(2));
S.update(Path, getInputs(Path, "auto (shut down)"), WantDiagnostics::Auto,
[&](std::vector<Diag> Diags) { ++CallbackCount; });

ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10)));
}
EXPECT_EQ(2, CallbackCount);
}
Expand Down Expand Up @@ -267,6 +271,8 @@ TEST_F(TUSchedulerTests, Cancellation) {
Update("U3");
Read("R3")();
Proceed.notify();

ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10)));
}
EXPECT_THAT(DiagsSeen, ElementsAre("U2", "U3"))
<< "U1 and all dependent reads were cancelled. "
Expand Down Expand Up @@ -373,6 +379,7 @@ TEST_F(TUSchedulerTests, ManyUpdates) {
}
}
}
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10)));
} // TUScheduler destructor waits for all operations to finish.

std::lock_guard<std::mutex> Lock(Mut);
Expand Down

0 comments on commit c76394b

Please sign in to comment.