OSDN Git Service

[clangd] Show background index status using LSP 3.15 work-done progress notifications
authorSam McCall <sam.mccall@gmail.com>
Wed, 22 Jan 2020 18:41:45 +0000 (19:41 +0100)
committerSam McCall <sam.mccall@gmail.com>
Fri, 24 Jan 2020 11:21:08 +0000 (12:21 +0100)
Summary:
It simply shows the completed/total items on the background queue, e.g.
 indexing: 233/1000
The denominator is reset to zero every time the queue goes idle.

The protocol is fairly complicated here (requires creating a remote "progress"
resource before sending updates). We implement the full protocol, but I've added
an extension allowing it to be skipped to reduce the burden on clients - in
particular the lit test takes this shortcut.

The addition of background index progress to DiagnosticConsumer seems ridiculous
at first glance, but I believe that interface is trending in the direction of
"ClangdServer callbacks" anyway. It's due for a rename, but otherwise actually
fits.

Reviewers: kadircet, usaxena95

Subscribers: ilya-biryukov, MaskRay, jkorous, arphaman, jfb, cfe-commits, llvm-commits

Tags: #clang, #llvm

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

13 files changed:
clang-tools-extra/clangd/ClangdLSPServer.cpp
clang-tools-extra/clangd/ClangdLSPServer.h
clang-tools-extra/clangd/ClangdServer.cpp
clang-tools-extra/clangd/ClangdServer.h
clang-tools-extra/clangd/Protocol.cpp
clang-tools-extra/clangd/Protocol.h
clang-tools-extra/clangd/index/Background.cpp
clang-tools-extra/clangd/index/Background.h
clang-tools-extra/clangd/index/BackgroundQueue.cpp
clang-tools-extra/clangd/test/Inputs/background-index/definition.jsonrpc
clang-tools-extra/clangd/test/background-index.test
clang-tools-extra/clangd/unittests/BackgroundIndexTests.cpp
llvm/include/llvm/Support/JSON.h

index 69b4308..633c61e 100644 (file)
@@ -33,6 +33,7 @@
 #include "llvm/Support/ScopedPrinter.h"
 #include <cstddef>
 #include <memory>
+#include <mutex>
 #include <string>
 #include <vector>
 
@@ -522,6 +523,9 @@ void ClangdLSPServer::onInitialize(const InitializeParams &Params,
   SupportFileStatus = Params.initializationOptions.FileStatus;
   HoverContentFormat = Params.capabilities.HoverContentFormat;
   SupportsOffsetsInSignatureHelp = Params.capabilities.OffsetsInSignatureHelp;
+  if (Params.capabilities.WorkDoneProgress)
+    BackgroundIndexProgressState = BackgroundIndexProgress::Empty;
+  BackgroundIndexSkipCreate = Params.capabilities.ImplicitProgressCreation;
 
   // Per LSP, renameProvider can be either boolean or RenameOptions.
   // RenameOptions will be specified if the client states it supports prepare.
@@ -1378,6 +1382,74 @@ void ClangdLSPServer::onDiagnosticsReady(PathRef File,
   publishDiagnostics(URI, std::move(LSPDiagnostics));
 }
 
+void ClangdLSPServer::onBackgroundIndexProgress(
+    const BackgroundQueue::Stats &Stats) {
+  static const char ProgressToken[] = "backgroundIndexProgress";
+  std::lock_guard<std::mutex> Lock(BackgroundIndexProgressMutex);
+
+  auto NotifyProgress = [this](const BackgroundQueue::Stats &Stats) {
+    if (BackgroundIndexProgressState != BackgroundIndexProgress::Live) {
+      WorkDoneProgressBegin Begin;
+      Begin.percentage = true;
+      Begin.title = "indexing";
+      progress(ProgressToken, std::move(Begin));
+      BackgroundIndexProgressState = BackgroundIndexProgress::Live;
+    }
+
+    if (Stats.Completed < Stats.Enqueued) {
+      assert(Stats.Enqueued > Stats.LastIdle);
+      WorkDoneProgressReport Report;
+      Report.percentage = 100.0 * (Stats.Completed - Stats.LastIdle) /
+                          (Stats.Enqueued - Stats.LastIdle);
+      Report.message =
+          llvm::formatv("{0}/{1}", Stats.Completed - Stats.LastIdle,
+                        Stats.Enqueued - Stats.LastIdle);
+      progress(ProgressToken, std::move(Report));
+    } else {
+      assert(Stats.Completed == Stats.Enqueued);
+      progress(ProgressToken, WorkDoneProgressEnd());
+      BackgroundIndexProgressState = BackgroundIndexProgress::Empty;
+    }
+  };
+
+  switch (BackgroundIndexProgressState) {
+  case BackgroundIndexProgress::Unsupported:
+    return;
+  case BackgroundIndexProgress::Creating:
+    // Cache this update for when the progress bar is available.
+    PendingBackgroundIndexProgress = Stats;
+    return;
+  case BackgroundIndexProgress::Empty: {
+    if (BackgroundIndexSkipCreate) {
+      NotifyProgress(Stats);
+      break;
+    }
+    // Cache this update for when the progress bar is available.
+    PendingBackgroundIndexProgress = Stats;
+    BackgroundIndexProgressState = BackgroundIndexProgress::Creating;
+    WorkDoneProgressCreateParams CreateRequest;
+    CreateRequest.token = ProgressToken;
+    call<std::nullptr_t>(
+        "window/workDoneProgress/create", CreateRequest,
+        [this, NotifyProgress](llvm::Expected<std::nullptr_t> E) {
+          std::lock_guard<std::mutex> Lock(BackgroundIndexProgressMutex);
+          if (E) {
+            NotifyProgress(this->PendingBackgroundIndexProgress);
+          } else {
+            elog("Failed to create background index progress bar: {0}",
+                 E.takeError());
+            // give up forever rather than thrashing about
+            BackgroundIndexProgressState = BackgroundIndexProgress::Unsupported;
+          }
+        });
+    break;
+  }
+  case BackgroundIndexProgress::Live:
+    NotifyProgress(Stats);
+    break;
+  }
+}
+
 void ClangdLSPServer::onFileUpdated(PathRef File, const TUStatus &Status) {
   if (!SupportFileStatus)
     return;
index 6f6d017..82fc49a 100644 (file)
@@ -60,6 +60,7 @@ private:
   void
   onHighlightingsReady(PathRef File,
                        std::vector<HighlightingToken> Highlightings) override;
+  void onBackgroundIndexProgress(const BackgroundQueue::Stats &Stats) override;
 
   // LSP methods. Notifications have signature void(const Params&).
   // Calls have signature void(const Params&, Callback<Response>).
@@ -185,6 +186,12 @@ private:
   void callRaw(StringRef Method, llvm::json::Value Params,
                Callback<llvm::json::Value> CB);
   void notify(StringRef Method, llvm::json::Value Params);
+  template <typename T> void progress(const llvm::json::Value &Token, T Value) {
+    ProgressParams<T> Params;
+    Params.token = Token;
+    Params.value = std::move(Value);
+    notify("$/progress", Params);
+  }
 
   const FileSystemProvider &FSProvider;
   /// Options used for code completion
@@ -205,6 +212,24 @@ private:
   MarkupKind HoverContentFormat = MarkupKind::PlainText;
   /// Whether the client supports offsets for parameter info labels.
   bool SupportsOffsetsInSignatureHelp = false;
+  std::mutex BackgroundIndexProgressMutex;
+  enum class BackgroundIndexProgress {
+    // Client doesn't support reporting progress. No transitions possible.
+    Unsupported,
+    // The queue is idle, and the client has no progress bar.
+    // Can transition to Creating when we have some activity.
+    Empty,
+    // We've requested the client to create a progress bar.
+    // Meanwhile, the state is buffered in PendingBackgroundIndexProgress.
+    Creating,
+    // The client has a progress bar, and we can send it updates immediately.
+    Live,
+  } BackgroundIndexProgressState = BackgroundIndexProgress::Unsupported;
+  // The progress to send when the progress bar is created.
+  // Only valid in state Creating.
+  BackgroundQueue::Stats PendingBackgroundIndexProgress;
+  /// LSP extension: skip WorkDoneProgressCreate, just send progress streams.
+  bool BackgroundIndexSkipCreate = false;
   // Store of the current versions of the open documents.
   DraftStore DraftMgr;
 
index c71a8a0..fb02c13 100644 (file)
@@ -147,7 +147,10 @@ ClangdServer::ClangdServer(const GlobalCompilationDatabase &CDB,
         Context::current().clone(), FSProvider, CDB,
         BackgroundIndexStorage::createDiskBackedStorageFactory(
             [&CDB](llvm::StringRef File) { return CDB.getProjectInfo(File); }),
-        std::max(Opts.AsyncThreadsCount, 1u));
+        std::max(Opts.AsyncThreadsCount, 1u),
+        [&DiagConsumer](BackgroundQueue::Stats S) {
+          DiagConsumer.onBackgroundIndexProgress(S);
+        });
     AddIndex(BackgroundIdx.get());
   }
   if (DynamicIdx)
index 4574d6c..142ca9b 100644 (file)
@@ -56,6 +56,9 @@ public:
   virtual void
   onHighlightingsReady(PathRef File,
                        std::vector<HighlightingToken> Highlightings) {}
+
+  // Called when background indexing tasks are enqueued, started, or completed.
+  virtual void onBackgroundIndexProgress(const BackgroundQueue::Stats &Stats) {}
 };
 
 /// When set, used by ClangdServer to get clang-tidy options for each particular
index 29ceb1d..87f7656 100644 (file)
@@ -347,6 +347,12 @@ bool fromJSON(const llvm::json::Value &Params, ClientCapabilities &R) {
       }
     }
   }
+  if (auto *Window = O->getObject("window")) {
+    if (auto WorkDoneProgress = Window->getBoolean("workDoneProgress"))
+      R.WorkDoneProgress = *WorkDoneProgress;
+    if (auto Implicit = Window->getBoolean("implicitWorkDoneProgressCreate"))
+      R.ImplicitProgressCreation = *Implicit;
+  }
   if (auto *OffsetEncoding = O->get("offsetEncoding")) {
     R.offsetEncoding.emplace();
     if (!fromJSON(*OffsetEncoding, *R.offsetEncoding))
@@ -370,6 +376,40 @@ bool fromJSON(const llvm::json::Value &Params, InitializeParams &R) {
   return true;
 }
 
+llvm::json::Value toJSON(const WorkDoneProgressCreateParams &P) {
+  return llvm::json::Object{{"token", P.token}};
+}
+
+llvm::json::Value toJSON(const WorkDoneProgressBegin &P) {
+  llvm::json::Object Result{
+      {"kind", "begin"},
+      {"title", P.title},
+  };
+  if (P.cancellable)
+    Result["cancellable"] = true;
+  if (P.percentage)
+    Result["percentage"] = 0;
+  return Result;
+}
+
+llvm::json::Value toJSON(const WorkDoneProgressReport &P) {
+  llvm::json::Object Result{{"kind", "report"}};
+  if (P.cancellable)
+    Result["cancellable"] = *P.cancellable;
+  if (P.message)
+    Result["message"] = *P.message;
+  if (P.percentage)
+    Result["percentage"] = *P.percentage;
+  return Result;
+}
+
+llvm::json::Value toJSON(const WorkDoneProgressEnd &P) {
+  llvm::json::Object Result{{"kind", "end"}};
+  if (P.message)
+    Result["message"] = *P.message;
+  return Result;
+}
+
 llvm::json::Value toJSON(const MessageType &R) {
   return static_cast<int64_t>(R);
 }
index 1b999f1..255629f 100644 (file)
@@ -427,6 +427,16 @@ struct ClientCapabilities {
   /// The client supports testing for validity of rename operations
   /// before execution.
   bool RenamePrepareSupport = false;
+
+  /// The client supports progress notifications.
+  /// window.workDoneProgress
+  bool WorkDoneProgress = false;
+
+  /// The client supports implicit $/progress work-done progress streams,
+  /// without a preceding window/workDoneProgress/create.
+  /// This is a clangd extension.
+  /// window.implicitWorkDoneProgressCreate
+  bool ImplicitProgressCreation = false;
 };
 bool fromJSON(const llvm::json::Value &, ClientCapabilities &);
 
@@ -499,6 +509,89 @@ struct InitializeParams {
 };
 bool fromJSON(const llvm::json::Value &, InitializeParams &);
 
+struct WorkDoneProgressCreateParams {
+  /// The token to be used to report progress.
+  llvm::json::Value token = nullptr;
+};
+llvm::json::Value toJSON(const WorkDoneProgressCreateParams &P);
+
+template <typename T> struct ProgressParams {
+  /// The progress token provided by the client or server.
+  llvm::json::Value token = nullptr;
+
+  /// The progress data.
+  T value;
+};
+template <typename T> llvm::json::Value toJSON(const ProgressParams<T> &P) {
+  return llvm::json::Object{{"token", P.token}, {"value", P.value}};
+}
+/// To start progress reporting a $/progress notification with the following
+/// payload must be sent.
+struct WorkDoneProgressBegin {
+  /// Mandatory title of the progress operation. Used to briefly inform about
+  /// the kind of operation being performed.
+  ///
+  /// Examples: "Indexing" or "Linking dependencies".
+  std::string title;
+
+  /// Controls if a cancel button should show to allow the user to cancel the
+  /// long-running operation. Clients that don't support cancellation are
+  /// allowed to ignore the setting.
+  bool cancellable = false;
+
+  /// Optional progress percentage to display (value 100 is considered 100%).
+  /// If not provided infinite progress is assumed and clients are allowed
+  /// to ignore the `percentage` value in subsequent in report notifications.
+  ///
+  /// The value should be steadily rising. Clients are free to ignore values
+  /// that are not following this rule.
+  ///
+  /// Clangd implementation note: we only send nonzero percentages in
+  /// the WorkProgressReport. 'true' here means percentages will be used.
+  bool percentage = false;
+};
+llvm::json::Value toJSON(const WorkDoneProgressBegin &);
+
+/// Reporting progress is done using the following payload.
+struct WorkDoneProgressReport {
+  /// Mandatory title of the progress operation. Used to briefly inform about
+  /// the kind of operation being performed.
+  ///
+  /// Examples: "Indexing" or "Linking dependencies".
+  std::string title;
+
+  /// Controls enablement state of a cancel button. This property is only valid
+  /// if a cancel button got requested in the `WorkDoneProgressStart` payload.
+  ///
+  /// Clients that don't support cancellation or don't support control
+  /// the button's enablement state are allowed to ignore the setting.
+  llvm::Optional<bool> cancellable;
+
+  /// Optional, more detailed associated progress message. Contains
+  /// complementary information to the `title`.
+  ///
+  /// Examples: "3/25 files", "project/src/module2", "node_modules/some_dep".
+  /// If unset, the previous progress message (if any) is still valid.
+  llvm::Optional<std::string> message;
+
+  /// Optional progress percentage to display (value 100 is considered 100%).
+  /// If not provided infinite progress is assumed and clients are allowed
+  /// to ignore the `percentage` value in subsequent in report notifications.
+  ///
+  /// The value should be steadily rising. Clients are free to ignore values
+  /// that are not following this rule.
+  llvm::Optional<double> percentage;
+};
+llvm::json::Value toJSON(const WorkDoneProgressReport &);
+//
+/// Signals the end of progress reporting.
+struct WorkDoneProgressEnd {
+  /// Optional, a final message indicating to for example indicate the outcome
+  /// of the operation.
+  llvm::Optional<std::string> message;
+};
+llvm::json::Value toJSON(const WorkDoneProgressEnd &);
+
 enum class MessageType {
   /// An error message.
   Error = 1,
index 1fa9825..dd198f0 100644 (file)
@@ -137,11 +137,13 @@ bool shardIsStale(const LoadedShard &LS, llvm::vfs::FileSystem *FS) {
 BackgroundIndex::BackgroundIndex(
     Context BackgroundContext, const FileSystemProvider &FSProvider,
     const GlobalCompilationDatabase &CDB,
-    BackgroundIndexStorage::Factory IndexStorageFactory, size_t ThreadPoolSize)
-    : SwapIndex(std::make_unique<MemIndex>()), FSProvider(FSProvider),
-      CDB(CDB), BackgroundContext(std::move(BackgroundContext)),
+    BackgroundIndexStorage::Factory IndexStorageFactory, size_t ThreadPoolSize,
+    std::function<void(BackgroundQueue::Stats)> OnProgress)
+    : SwapIndex(std::make_unique<MemIndex>()), FSProvider(FSProvider), CDB(CDB),
+      BackgroundContext(std::move(BackgroundContext)),
       Rebuilder(this, &IndexedSymbols, ThreadPoolSize),
       IndexStorageFactory(std::move(IndexStorageFactory)),
+      Queue(std::move(OnProgress)),
       CommandsChanged(
           CDB.watch([&](const std::vector<std::string> &ChangedFiles) {
             enqueue(ChangedFiles);
index 7ce9e7b..b11008d 100644 (file)
@@ -78,6 +78,17 @@ public:
     bool operator<(const Task &O) const { return QueuePri < O.QueuePri; }
   };
 
+  // Describes the number of tasks processed by the queue.
+  struct Stats {
+    unsigned Enqueued = 0;  // Total number of tasks ever enqueued.
+    unsigned Active = 0;    // Tasks being currently processed by a worker.
+    unsigned Completed = 0; // Tasks that have been finished.
+    unsigned LastIdle = 0;  // Number of completed tasks when last empty.
+  };
+
+  BackgroundQueue(std::function<void(Stats)> OnProgress = nullptr)
+      : OnProgress(OnProgress) {}
+
   // Add tasks to the queue.
   void push(Task);
   void append(std::vector<Task>);
@@ -100,12 +111,15 @@ public:
   blockUntilIdleForTest(llvm::Optional<double> TimeoutSeconds);
 
 private:
+  void notifyProgress() const; // Requires lock Mu
+
   std::mutex Mu;
-  unsigned NumActiveTasks = 0; // Only idle when queue is empty *and* no tasks.
+  Stats Stat;
   std::condition_variable CV;
   bool ShouldStop = false;
   std::vector<Task> Queue; // max-heap
   llvm::StringMap<unsigned> Boosts;
+  std::function<void(Stats)> OnProgress;
 };
 
 // Builds an in-memory index by by running the static indexer action over
@@ -121,7 +135,8 @@ public:
       Context BackgroundContext, const FileSystemProvider &,
       const GlobalCompilationDatabase &CDB,
       BackgroundIndexStorage::Factory IndexStorageFactory,
-      size_t ThreadPoolSize = llvm::heavyweight_hardware_concurrency());
+      size_t ThreadPoolSize = llvm::heavyweight_hardware_concurrency(),
+      std::function<void(BackgroundQueue::Stats)> OnProgress = nullptr);
   ~BackgroundIndex(); // Blocks while the current task finishes.
 
   // Enqueue translation units for indexing.
index 9140972..00d483f 100644 (file)
@@ -6,6 +6,7 @@
 //
 //===----------------------------------------------------------------------===//
 
+#include "Logger.h"
 #include "index/Background.h"
 
 namespace clang {
@@ -28,10 +29,11 @@ void BackgroundQueue::work(std::function<void()> OnIdle) {
         CV.notify_all();
         return;
       }
-      ++NumActiveTasks;
+      ++Stat.Active;
       std::pop_heap(Queue.begin(), Queue.end());
       Task = std::move(Queue.back());
       Queue.pop_back();
+      notifyProgress();
     }
 
     if (Task->ThreadPri != llvm::ThreadPriority::Default &&
@@ -43,14 +45,20 @@ void BackgroundQueue::work(std::function<void()> OnIdle) {
 
     {
       std::unique_lock<std::mutex> Lock(Mu);
-      if (NumActiveTasks == 1 && Queue.empty() && OnIdle) {
+      ++Stat.Completed;
+      if (Stat.Active == 1 && Queue.empty()) {
         // We just finished the last item, the queue is going idle.
-        Lock.unlock();
-        OnIdle();
-        Lock.lock();
+        assert(ShouldStop || Stat.Completed == Stat.Enqueued);
+        Stat.LastIdle = Stat.Completed;
+        if (OnIdle) {
+          Lock.unlock();
+          OnIdle();
+          Lock.lock();
+        }
       }
-      assert(NumActiveTasks > 0 && "before decrementing");
-      --NumActiveTasks;
+      assert(Stat.Active > 0 && "before decrementing");
+      --Stat.Active;
+      notifyProgress();
     }
     CV.notify_all();
   }
@@ -70,6 +78,8 @@ void BackgroundQueue::push(Task T) {
     T.QueuePri = std::max(T.QueuePri, Boosts.lookup(T.Tag));
     Queue.push_back(std::move(T));
     std::push_heap(Queue.begin(), Queue.end());
+    ++Stat.Enqueued;
+    notifyProgress();
   }
   CV.notify_all();
 }
@@ -81,6 +91,8 @@ void BackgroundQueue::append(std::vector<Task> Tasks) {
       T.QueuePri = std::max(T.QueuePri, Boosts.lookup(T.Tag));
     std::move(Tasks.begin(), Tasks.end(), std::back_inserter(Queue));
     std::make_heap(Queue.begin(), Queue.end());
+    Stat.Enqueued += Tasks.size();
+    notifyProgress();
   }
   CV.notify_all();
 }
@@ -108,7 +120,14 @@ bool BackgroundQueue::blockUntilIdleForTest(
     llvm::Optional<double> TimeoutSeconds) {
   std::unique_lock<std::mutex> Lock(Mu);
   return wait(Lock, CV, timeoutSeconds(TimeoutSeconds),
-              [&] { return Queue.empty() && NumActiveTasks == 0; });
+              [&] { return Queue.empty() && Stat.Active == 0; });
+}
+
+void BackgroundQueue::notifyProgress() const {
+  dlog("Queue: {0}/{1} ({2} active). Last idle at {3}", Stat.Completed,
+       Stat.Enqueued, Stat.Active, Stat.LastIdle);
+  if (OnProgress)
+    OnProgress(Stat);
 }
 
 } // namespace clangd
index de99072..9ae0d92 100644 (file)
@@ -5,7 +5,7 @@
   "params": {
     "processId": 123,
     "rootPath": "clangd",
-    "capabilities": {},
+    "capabilities": { "window": { "workDoneProgress": true, "implicitWorkDoneProgressCreate": true} },
     "trace": "off"
   }
 }
     }
   }
 }
+# BUILD:      "method": "$/progress"
+# BUILD-NEXT: "params": {
+# BUILD-NEXT:   "token": "backgroundIndexProgress",
+# BUILD-NEXT:   "value": {
+# BUILD-NEXT:     "kind": "begin",
+# BUILD-NEXT:     "percentage": 0,
+# BUILD-NEXT:     "title": "indexing"
+# BUILD-NEXT:   }
+# BUILD-NEXT: }
+# BUILD:      "method": "$/progress"
+# BUILD-NEXT: "params": {
+# BUILD-NEXT:   "token": "backgroundIndexProgress",
+# BUILD-NEXT:   "value": {
+# BUILD-NEXT:     "kind": "report",
+# BUILD-NEXT:     "message": "0/1",
+# BUILD-NEXT:     "percentage": 0
+# BUILD-NEXT:   }
+# BUILD-NEXT: }
+#    later...
+# BUILD:          "message": "1/2",
+# BUILD-NEXT:     "percentage": 50
+#    finally...
+# BUILD:          "kind": "end"
+# when using the index, we spawn one task to read it, but not more.
+# USE-NOT:        "message": "1/2",
 ---
 {
   "jsonrpc": "2.0",
index 06efcf9..821ac71 100644 (file)
@@ -11,7 +11,8 @@
 # We're editing bar.cpp, which includes foo.h.
 # foo() is declared in foo.h and defined in foo.cpp.
 # The background index should allow us to go-to-definition on foo().
-# RUN: clangd -background-index -lit-test < %t/definition.jsonrpc | FileCheck %t/definition.jsonrpc
+# We should also see indexing progress notifications.
+# RUN: clangd -background-index -lit-test < %t/definition.jsonrpc | FileCheck %t/definition.jsonrpc --check-prefixes=CHECK,BUILD
 
 # Test that the index is writing files in the expected location.
 # RUN: ls %t/.clangd/index/foo.cpp.*.idx
@@ -19,4 +20,4 @@
 
 # Test the index is read from disk: delete code and restart clangd.
 # RUN: rm %t/foo.cpp
-# RUN: clangd -background-index -lit-test < %t/definition.jsonrpc | FileCheck %t/definition.jsonrpc
+# RUN: clangd -background-index -lit-test < %t/definition.jsonrpc | FileCheck %t/definition.jsonrpc --check-prefixes=CHECK,USE
index 345be6a..00fe975 100644 (file)
@@ -711,5 +711,53 @@ TEST(BackgroundQueueTest, Boost) {
   }
 }
 
+TEST(BackgroundQueueTest, Progress) {
+  using testing::AnyOf;
+  BackgroundQueue::Stats S;
+  BackgroundQueue Q([&](BackgroundQueue::Stats New) {
+    // Verify values are sane.
+    // Items are enqueued one at a time (at least in this test).
+    EXPECT_THAT(New.Enqueued, AnyOf(S.Enqueued, S.Enqueued + 1));
+    // Items are completed one at a time.
+    EXPECT_THAT(New.Completed, AnyOf(S.Completed, S.Completed + 1));
+    // Items are started or completed one at a time.
+    EXPECT_THAT(New.Active, AnyOf(S.Active - 1, S.Active, S.Active + 1));
+    // Idle point only advances in time.
+    EXPECT_GE(New.LastIdle, S.LastIdle);
+    // Idle point is a task that has been completed in the past.
+    EXPECT_LE(New.LastIdle, New.Completed);
+    // LastIdle is now only if we're really idle.
+    EXPECT_EQ(New.LastIdle == New.Enqueued,
+              New.Completed == New.Enqueued && New.Active == 0u);
+    S = New;
+  });
+
+  // Two types of tasks: a ping task enqueues a pong task.
+  // This avoids all enqueues followed by all completions (boring!)
+  std::atomic<int> PingCount(0), PongCount(0);
+  BackgroundQueue::Task Pong([&] { ++PongCount; });
+  BackgroundQueue::Task Ping([&] {
+    ++PingCount;
+    Q.push(Pong);
+  });
+
+  for (int I = 0; I < 1000; ++I)
+    Q.push(Ping);
+  // Spin up some workers and stop while idle.
+  AsyncTaskRunner ThreadPool;
+  for (unsigned I = 0; I < 5; ++I)
+    ThreadPool.runAsync("worker", [&] { Q.work([&] { Q.stop(); }); });
+  ThreadPool.wait();
+
+  // Everything's done, check final stats.
+  // Assertions above ensure we got from 0 to 2000 in a reasonable way.
+  EXPECT_EQ(PingCount.load(), 1000);
+  EXPECT_EQ(PongCount.load(), 1000);
+  EXPECT_EQ(S.Active, 0u);
+  EXPECT_EQ(S.Enqueued, 2000u);
+  EXPECT_EQ(S.Completed, 2000u);
+  EXPECT_EQ(S.LastIdle, 2000u);
+}
+
 } // namespace clangd
 } // namespace clang
index 2c63468..56d26d2 100644 (file)
@@ -598,6 +598,13 @@ inline bool fromJSON(const Value &E, bool &Out) {
   }
   return false;
 }
+inline bool fromJSON(const Value &E, std::nullptr_t &Out) {
+  if (auto S = E.getAsNull()) {
+    Out = *S;
+    return true;
+  }
+  return false;
+}
 template <typename T> bool fromJSON(const Value &E, llvm::Optional<T> &Out) {
   if (E.getAsNull()) {
     Out = llvm::None;