diff --git a/clang-tools-extra/clangd/index/Background.h b/clang-tools-extra/clangd/index/Background.h --- a/clang-tools-extra/clangd/index/Background.h +++ b/clang-tools-extra/clangd/index/Background.h @@ -100,7 +100,7 @@ void boost(llvm::StringRef Tag, unsigned NewPriority); // Process items on the queue until the queue is stopped. - // If the queue becomes empty, OnIdle will be called (on one worker). + // Each time the queue becomes empty, OnIdle will be called (on one worker). void work(std::function OnIdle = nullptr); // Stop processing new tasks, allowing all work() calls to return soon. diff --git a/clang-tools-extra/clangd/index/BackgroundQueue.cpp b/clang-tools-extra/clangd/index/BackgroundQueue.cpp --- a/clang-tools-extra/clangd/index/BackgroundQueue.cpp +++ b/clang-tools-extra/clangd/index/BackgroundQueue.cpp @@ -46,8 +46,11 @@ { std::unique_lock Lock(Mu); ++Stat.Completed; - if (Stat.Active == 1 && Queue.empty()) { - // We just finished the last item, the queue is going idle. + // If we just finished the last item, the queue is going idle. + // Loop in case more items get scheduled and completed during OnIdle(). + // (We treat this as going idle a second time). + while (Stat.Active == 1 && Queue.empty() && + Stat.LastIdle != Stat.Completed) { assert(ShouldStop || Stat.Completed == Stat.Enqueued); Stat.LastIdle = Stat.Completed; if (OnIdle) { diff --git a/clang-tools-extra/clangd/unittests/BackgroundIndexTests.cpp b/clang-tools-extra/clangd/unittests/BackgroundIndexTests.cpp --- a/clang-tools-extra/clangd/unittests/BackgroundIndexTests.cpp +++ b/clang-tools-extra/clangd/unittests/BackgroundIndexTests.cpp @@ -7,6 +7,7 @@ #include "TestTU.h" #include "index/Background.h" #include "index/BackgroundRebuild.h" +#include "support/Threading.h" #include "clang/Tooling/ArgumentsAdjusters.h" #include "clang/Tooling/CompilationDatabase.h" #include "llvm/Support/ScopedPrinter.h" @@ -826,6 +827,41 @@ } } +TEST(BackgroundQueueTest, Idle) { + std::atomic TaskCount(0), IdleCount(0); + BackgroundQueue::Task Task([&] { ++TaskCount; }); + + // Set up a queue initially with 10 tasks. + AsyncTaskRunner ThreadPool; + BackgroundQueue Q; + for (unsigned I = 0; I < 10; ++I) + Q.push(Task); + Notification Start; + for (unsigned I = 0; I < 5; ++I) + ThreadPool.runAsync("worker", [&] { + Start.wait(); + Q.work(/*OnIdle=*/[&] { + // Should be nothing else running now, so this assertion isn't racy. + EXPECT_EQ(10u + 2u * IdleCount, TaskCount); + + // The first two times the queue goes idle, add a couple more tasks. + // This should caute OnIdle to run again. + if (++IdleCount <= 2) { + Q.push(Task); + Q.push(Task); + } + }); + }); + + Start.notify(); + ASSERT_TRUE(Q.blockUntilIdleForTest(/*TimeoutSeconds=*/5)); + Q.stop(); + ASSERT_TRUE(ThreadPool.wait(timeoutSeconds(5))); + + EXPECT_EQ(3u, IdleCount) + << "Queue exhausts original items, then the extra two (twice)"; +} + TEST(BackgroundQueueTest, Progress) { using testing::AnyOf; BackgroundQueue::Stats S;