Skip to content

Commit 808c285

Browse files
committed
[clangd] Add tests that no-op changes are cheap
Summary: We want to be sure they don't cause AST rebuilds or evict items from the cache. D77847 is going to start sending spurious no-op changes (in case the preamble was invalidated), this is cheap enough but we shouldn't regress that in future. Reviewers: kadircet Subscribers: ilya-biryukov, javed.absar, MaskRay, jkorous, arphaman, jfb, usaxena95, cfe-commits Tags: #clang Differential Revision: https://reviews.llvm.org/D78048
1 parent 24fad72 commit 808c285

File tree

6 files changed

+97
-40
lines changed

6 files changed

+97
-40
lines changed

clang-tools-extra/clangd/ClangdServer.cpp

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -703,9 +703,8 @@ void ClangdServer::semanticHighlights(
703703
TUScheduler::InvalidateOnUpdate);
704704
}
705705

706-
std::vector<std::pair<Path, std::size_t>>
707-
ClangdServer::getUsedBytesPerFile() const {
708-
return WorkScheduler.getUsedBytesPerFile();
706+
llvm::StringMap<TUScheduler::FileStats> ClangdServer::fileStats() const {
707+
return WorkScheduler.fileStats();
709708
}
710709

711710
LLVM_NODISCARD bool

clang-tools-extra/clangd/ClangdServer.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -304,14 +304,14 @@ class ClangdServer {
304304
void semanticHighlights(PathRef File,
305305
Callback<std::vector<HighlightingToken>>);
306306

307-
/// Returns estimated memory usage for each of the currently open files.
308-
/// The order of results is unspecified.
307+
/// Returns estimated memory usage and other statistics for each of the
308+
/// currently open files.
309309
/// Overall memory usage of clangd may be significantly more than reported
310310
/// here, as this metric does not account (at least) for:
311311
/// - memory occupied by static and dynamic index,
312312
/// - memory required for in-flight requests,
313313
/// FIXME: those metrics might be useful too, we should add them.
314-
std::vector<std::pair<Path, std::size_t>> getUsedBytesPerFile() const;
314+
llvm::StringMap<TUScheduler::FileStats> fileStats() const;
315315

316316
// Blocks the main thread until the server is idle. Only for use in tests.
317317
// Returns false if the timeout expires.

clang-tools-extra/clangd/TUScheduler.cpp

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ class ASTWorker {
397397
/// getPossiblyStalePreamble() can be null even after this function returns.
398398
void waitForFirstPreamble() const;
399399

400-
std::size_t getUsedBytes() const;
400+
TUScheduler::FileStats stats() const;
401401
bool isASTCached() const;
402402

403403
private:
@@ -478,6 +478,8 @@ class ASTWorker {
478478
// don't. When the old handle is destroyed, the old worker will stop reporting
479479
// any results to the user.
480480
bool CanPublishResults = true; /* GUARDED_BY(PublishMu) */
481+
std::atomic<unsigned> ASTBuildCount = {0};
482+
std::atomic<unsigned> PreambleBuildCount = {0};
481483

482484
SynchronizedTUStatus Status;
483485
PreambleThread PreamblePeer;
@@ -661,11 +663,13 @@ void ASTWorker::runWithAST(
661663
// FIXME: We might need to build a patched ast once preamble thread starts
662664
// running async. Currently getPossiblyStalePreamble below will always
663665
// return a compatible preamble as ASTWorker::update blocks.
664-
llvm::Optional<ParsedAST> NewAST =
665-
Invocation ? buildAST(FileName, std::move(Invocation),
666-
CompilerInvocationDiagConsumer.take(),
667-
FileInputs, getPossiblyStalePreamble())
668-
: None;
666+
llvm::Optional<ParsedAST> NewAST;
667+
if (Invocation) {
668+
NewAST = buildAST(FileName, std::move(Invocation),
669+
CompilerInvocationDiagConsumer.take(), FileInputs,
670+
getPossiblyStalePreamble());
671+
++ASTBuildCount;
672+
}
669673
AST = NewAST ? std::make_unique<ParsedAST>(std::move(*NewAST)) : nullptr;
670674
}
671675
// Make sure we put the AST back into the LRU cache.
@@ -732,6 +736,7 @@ void ASTWorker::updatePreamble(std::unique_ptr<CompilerInvocation> CI,
732736
// running inside ASTWorker assumes internals won't change until it
733737
// finishes.
734738
if (Preamble != LatestPreamble) {
739+
++PreambleBuildCount;
735740
// Cached AST is no longer valid.
736741
IdleASTs.take(this);
737742
RanASTCallback = false;
@@ -802,6 +807,7 @@ void ASTWorker::generateDiagnostics(
802807
llvm::Optional<ParsedAST> NewAST = buildAST(
803808
FileName, std::move(Invocation), CIDiags, Inputs, LatestPreamble);
804809
auto RebuildDuration = DebouncePolicy::clock::now() - RebuildStartTime;
810+
++ASTBuildCount;
805811
// Try to record the AST-build time, to inform future update debouncing.
806812
// This is best-effort only: if the lock is held, don't bother.
807813
std::unique_lock<std::mutex> Lock(Mutex, std::try_to_lock);
@@ -897,13 +903,16 @@ tooling::CompileCommand ASTWorker::getCurrentCompileCommand() const {
897903
return FileInputs.CompileCommand;
898904
}
899905

900-
std::size_t ASTWorker::getUsedBytes() const {
906+
TUScheduler::FileStats ASTWorker::stats() const {
907+
TUScheduler::FileStats Result;
908+
Result.ASTBuilds = ASTBuildCount;
909+
Result.PreambleBuilds = PreambleBuildCount;
901910
// Note that we don't report the size of ASTs currently used for processing
902911
// the in-flight requests. We used this information for debugging purposes
903912
// only, so this should be fine.
904-
std::size_t Result = IdleASTs.getUsedBytes(this);
913+
Result.UsedBytes = IdleASTs.getUsedBytes(this);
905914
if (auto Preamble = getPossiblyStalePreamble())
906-
Result += Preamble->Preamble.getSize();
915+
Result.UsedBytes += Preamble->Preamble.getSize();
907916
return Result;
908917
}
909918

@@ -1345,13 +1354,11 @@ void TUScheduler::runWithPreamble(llvm::StringRef Name, PathRef File,
13451354
std::move(Task));
13461355
}
13471356

1348-
std::vector<std::pair<Path, std::size_t>>
1349-
TUScheduler::getUsedBytesPerFile() const {
1350-
std::vector<std::pair<Path, std::size_t>> Result;
1351-
Result.reserve(Files.size());
1352-
for (auto &&PathAndFile : Files)
1353-
Result.push_back({std::string(PathAndFile.first()),
1354-
PathAndFile.second->Worker->getUsedBytes()});
1357+
llvm::StringMap<TUScheduler::FileStats> TUScheduler::fileStats() const {
1358+
llvm::StringMap<TUScheduler::FileStats> Result;
1359+
for (const auto &PathAndFile : Files)
1360+
Result.try_emplace(PathAndFile.first(),
1361+
PathAndFile.second->Worker->stats());
13551362
return Result;
13561363
}
13571364

clang-tools-extra/clangd/TUScheduler.h

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,14 @@ class TUScheduler {
197197
std::unique_ptr<ParsingCallbacks> ASTCallbacks = nullptr);
198198
~TUScheduler();
199199

200-
/// Returns estimated memory usage for each of the currently open files.
201-
/// The order of results is unspecified.
202-
std::vector<std::pair<Path, std::size_t>> getUsedBytesPerFile() const;
200+
struct FileStats {
201+
std::size_t UsedBytes = 0;
202+
unsigned PreambleBuilds = 0;
203+
unsigned ASTBuilds = 0;
204+
};
205+
/// Returns resources used for each of the currently open files.
206+
/// Results are inherently racy as they measure activity of other threads.
207+
llvm::StringMap<FileStats> fileStats() const;
203208

204209
/// Returns a list of files with ASTs currently stored in memory. This method
205210
/// is not very reliable and is only used for test. E.g., the results will not

clang-tools-extra/clangd/unittests/ClangdTests.cpp

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ namespace clangd {
3838

3939
namespace {
4040

41+
using ::testing::AllOf;
4142
using ::testing::ElementsAre;
4243
using ::testing::Field;
4344
using ::testing::Gt;
@@ -496,7 +497,13 @@ int hello;
496497
EXPECT_THAT(*Locations, ElementsAre(DeclAt(FooCpp, FooSource.range("two"))));
497498
}
498499

499-
TEST_F(ClangdVFSTest, MemoryUsage) {
500+
MATCHER_P4(Stats, Name, UsesMemory, PreambleBuilds, ASTBuilds, "") {
501+
return arg.first() == Name && (arg.second.UsedBytes != 0) == UsesMemory &&
502+
std::tie(arg.second.PreambleBuilds, ASTBuilds) ==
503+
std::tie(PreambleBuilds, ASTBuilds);
504+
}
505+
506+
TEST_F(ClangdVFSTest, FileStats) {
500507
MockFSProvider FS;
501508
ErrorCheckingCallbacks DiagConsumer;
502509
MockCompilationDatabase CDB;
@@ -513,22 +520,23 @@ struct Something {
513520
FS.Files[FooCpp] = "";
514521
FS.Files[BarCpp] = "";
515522

516-
EXPECT_THAT(Server.getUsedBytesPerFile(), IsEmpty());
523+
EXPECT_THAT(Server.fileStats(), IsEmpty());
517524

518525
Server.addDocument(FooCpp, SourceContents);
519526
Server.addDocument(BarCpp, SourceContents);
520527
ASSERT_TRUE(Server.blockUntilIdleForTest());
521528

522-
EXPECT_THAT(Server.getUsedBytesPerFile(),
523-
UnorderedElementsAre(Pair(FooCpp, Gt(0u)), Pair(BarCpp, Gt(0u))));
529+
EXPECT_THAT(Server.fileStats(),
530+
UnorderedElementsAre(Stats(FooCpp, true, 1, 1),
531+
Stats(BarCpp, true, 1, 1)));
524532

525533
Server.removeDocument(FooCpp);
526534
ASSERT_TRUE(Server.blockUntilIdleForTest());
527-
EXPECT_THAT(Server.getUsedBytesPerFile(), ElementsAre(Pair(BarCpp, Gt(0u))));
535+
EXPECT_THAT(Server.fileStats(), ElementsAre(Stats(BarCpp, true, 1, 1)));
528536

529537
Server.removeDocument(BarCpp);
530538
ASSERT_TRUE(Server.blockUntilIdleForTest());
531-
EXPECT_THAT(Server.getUsedBytesPerFile(), IsEmpty());
539+
EXPECT_THAT(Server.fileStats(), IsEmpty());
532540
}
533541

534542
TEST_F(ClangdVFSTest, InvalidCompileCommand) {

clang-tools-extra/clangd/unittests/TUSchedulerTests.cpp

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,37 @@ TEST_F(TUSchedulerTests, EvictedAST) {
608608
UnorderedElementsAre(Foo, AnyOf(Bar, Baz)));
609609
}
610610

611+
// We send "empty" changes to TUScheduler when we think some external event
612+
// *might* have invalidated current state (e.g. a header was edited).
613+
// Verify that this doesn't evict our cache entries.
614+
TEST_F(TUSchedulerTests, NoopChangesDontThrashCache) {
615+
auto Opts = optsForTest();
616+
Opts.RetentionPolicy.MaxRetainedASTs = 1;
617+
TUScheduler S(CDB, Opts);
618+
619+
auto Foo = testPath("foo.cpp");
620+
auto FooInputs = getInputs(Foo, "int x=1;");
621+
auto Bar = testPath("bar.cpp");
622+
auto BarInputs = getInputs(Bar, "int x=2;");
623+
624+
// After opening Foo then Bar, AST cache contains Bar.
625+
S.update(Foo, FooInputs, WantDiagnostics::Auto);
626+
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10)));
627+
S.update(Bar, BarInputs, WantDiagnostics::Auto);
628+
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10)));
629+
ASSERT_THAT(S.getFilesWithCachedAST(), ElementsAre(Bar));
630+
631+
// Any number of no-op updates to Foo don't dislodge Bar from the cache.
632+
S.update(Foo, FooInputs, WantDiagnostics::Auto);
633+
S.update(Foo, FooInputs, WantDiagnostics::Auto);
634+
S.update(Foo, FooInputs, WantDiagnostics::Auto);
635+
ASSERT_TRUE(S.blockUntilIdle(timeoutSeconds(10)));
636+
ASSERT_THAT(S.getFilesWithCachedAST(), ElementsAre(Bar));
637+
// In fact each file has been built only once.
638+
ASSERT_EQ(S.fileStats().lookup(Foo).ASTBuilds, 1u);
639+
ASSERT_EQ(S.fileStats().lookup(Bar).ASTBuilds, 1u);
640+
}
641+
611642
TEST_F(TUSchedulerTests, EmptyPreamble) {
612643
TUScheduler S(CDB, optsForTest());
613644

@@ -686,7 +717,7 @@ TEST_F(TUSchedulerTests, NoopOnEmptyChanges) {
686717
Files[Header] = "int a;";
687718
Timestamps[Header] = time_t(0);
688719

689-
auto SourceContents = R"cpp(
720+
std::string SourceContents = R"cpp(
690721
#include "foo.h"
691722
int b = a;
692723
)cpp";
@@ -705,25 +736,32 @@ TEST_F(TUSchedulerTests, NoopOnEmptyChanges) {
705736

706737
// Test that subsequent updates with the same inputs do not cause rebuilds.
707738
ASSERT_TRUE(DoUpdate(SourceContents));
739+
ASSERT_EQ(S.fileStats().lookup(Source).ASTBuilds, 1u);
740+
ASSERT_EQ(S.fileStats().lookup(Source).PreambleBuilds, 1u);
708741
ASSERT_FALSE(DoUpdate(SourceContents));
742+
ASSERT_EQ(S.fileStats().lookup(Source).ASTBuilds, 1u);
743+
ASSERT_EQ(S.fileStats().lookup(Source).PreambleBuilds, 1u);
709744

710745
// Update to a header should cause a rebuild, though.
711746
Timestamps[Header] = time_t(1);
712747
ASSERT_TRUE(DoUpdate(SourceContents));
713748
ASSERT_FALSE(DoUpdate(SourceContents));
749+
ASSERT_EQ(S.fileStats().lookup(Source).ASTBuilds, 2u);
750+
ASSERT_EQ(S.fileStats().lookup(Source).PreambleBuilds, 2u);
714751

715752
// Update to the contents should cause a rebuild.
716-
auto OtherSourceContents = R"cpp(
717-
#include "foo.h"
718-
int c = d;
719-
)cpp";
720-
ASSERT_TRUE(DoUpdate(OtherSourceContents));
721-
ASSERT_FALSE(DoUpdate(OtherSourceContents));
753+
SourceContents += "\nint c = b;";
754+
ASSERT_TRUE(DoUpdate(SourceContents));
755+
ASSERT_FALSE(DoUpdate(SourceContents));
756+
ASSERT_EQ(S.fileStats().lookup(Source).ASTBuilds, 3u);
757+
ASSERT_EQ(S.fileStats().lookup(Source).PreambleBuilds, 2u);
722758

723759
// Update to the compile commands should also cause a rebuild.
724760
CDB.ExtraClangFlags.push_back("-DSOMETHING");
725-
ASSERT_TRUE(DoUpdate(OtherSourceContents));
726-
ASSERT_FALSE(DoUpdate(OtherSourceContents));
761+
ASSERT_TRUE(DoUpdate(SourceContents));
762+
ASSERT_FALSE(DoUpdate(SourceContents));
763+
ASSERT_EQ(S.fileStats().lookup(Source).ASTBuilds, 4u);
764+
ASSERT_EQ(S.fileStats().lookup(Source).PreambleBuilds, 3u);
727765
}
728766

729767
TEST_F(TUSchedulerTests, ForceRebuild) {

0 commit comments

Comments
 (0)