[lldb-vscode] Reduce chattiness of progress events

Progress events internally have a completed count and a total count, which can mean that for a job with 20000 total counts, then there will be 20000 events fired. Sending all these events to the IDE can break it. For example, debugging a huge binary resulted in around 50 million messages, which rendered the IDE useless, as it was spending all of its resources simply parsing messages and updating the UI.

A way to fix this is to send unique percentage updates, which are at most 100 per job, which is not much. I was able to debug that big target and confirm that only unique percentage notifications are sent. I can't write a test for this because the current test is flaky. I'll figure out later how to make the test reliable, but fixing this will unblock us from deploy a new version of lldb-vscode.

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

GitOrigin-RevId: cc88d301a0bcd8b93c632af2870503949038c87f
diff --git a/tools/lldb-vscode/CMakeLists.txt b/tools/lldb-vscode/CMakeLists.txt
index c5346aa..eb2f651 100644
--- a/tools/lldb-vscode/CMakeLists.txt
+++ b/tools/lldb-vscode/CMakeLists.txt
@@ -32,6 +32,7 @@
   IOStream.cpp
   JSONUtils.cpp
   LLDBUtils.cpp
+  ProgressEvent.cpp
   RunInTerminal.cpp
   SourceBreakpoint.cpp
   VSCode.cpp
diff --git a/tools/lldb-vscode/ProgressEvent.cpp b/tools/lldb-vscode/ProgressEvent.cpp
new file mode 100644
index 0000000..c282021
--- /dev/null
+++ b/tools/lldb-vscode/ProgressEvent.cpp
@@ -0,0 +1,93 @@
+//===-- ProgressEvent.cpp ---------------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "ProgressEvent.h"
+
+#include "JSONUtils.h"
+
+using namespace lldb_vscode;
+using namespace llvm;
+
+ProgressEvent::ProgressEvent(uint64_t progress_id, const char *message,
+                             uint64_t completed, uint64_t total)
+    : m_progress_id(progress_id), m_message(message) {
+  if (completed == total)
+    m_event_type = progressEnd;
+  else if (completed == 0)
+    m_event_type = progressStart;
+  else if (completed < total)
+    m_event_type = progressUpdate;
+  else
+    m_event_type = progressInvalid;
+
+  if (0 < total && total < UINT64_MAX)
+    m_percentage = (uint32_t)(((float)completed / (float)total) * 100.0);
+}
+
+bool ProgressEvent::operator==(const ProgressEvent &other) const {
+  return m_progress_id == other.m_progress_id &&
+         m_event_type == other.m_event_type &&
+         m_percentage == other.m_percentage;
+}
+
+const char *ProgressEvent::GetEventName() const {
+  if (m_event_type == progressStart)
+    return "progressStart";
+  else if (m_event_type == progressEnd)
+    return "progressEnd";
+  else if (m_event_type == progressUpdate)
+    return "progressUpdate";
+  else
+    return "progressInvalid";
+}
+
+bool ProgressEvent::IsValid() const { return m_event_type != progressInvalid; }
+
+uint64_t ProgressEvent::GetID() const { return m_progress_id; }
+
+json::Value ProgressEvent::ToJSON() const {
+  llvm::json::Object event(CreateEventObject(GetEventName()));
+  llvm::json::Object body;
+
+  std::string progress_id_str;
+  llvm::raw_string_ostream progress_id_strm(progress_id_str);
+  progress_id_strm << m_progress_id;
+  progress_id_strm.flush();
+  body.try_emplace("progressId", progress_id_str);
+
+  if (m_event_type == progressStart) {
+    EmplaceSafeString(body, "title", m_message);
+    body.try_emplace("cancellable", false);
+  }
+
+  auto now = std::chrono::duration<double>(
+      std::chrono::system_clock::now().time_since_epoch());
+  std::string timestamp(llvm::formatv("{0:f9}", now.count()));
+  EmplaceSafeString(body, "timestamp", timestamp);
+
+  if (m_percentage)
+    body.try_emplace("percentage", *m_percentage);
+
+  event.try_emplace("body", std::move(body));
+  return json::Value(std::move(event));
+}
+
+ProgressEventFilterQueue::ProgressEventFilterQueue(
+    std::function<void(ProgressEvent)> callback)
+    : m_callback(callback) {}
+
+void ProgressEventFilterQueue::Push(const ProgressEvent &event) {
+  if (!event.IsValid())
+    return;
+
+  auto it = m_last_events.find(event.GetID());
+  if (it == m_last_events.end() || !(it->second == event)) {
+    m_last_events[event.GetID()] = event;
+    m_callback(event);
+  }
+}
diff --git a/tools/lldb-vscode/ProgressEvent.h b/tools/lldb-vscode/ProgressEvent.h
new file mode 100644
index 0000000..bafe7b3
--- /dev/null
+++ b/tools/lldb-vscode/ProgressEvent.h
@@ -0,0 +1,62 @@
+//===-- ProgressEvent.cpp ---------------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "VSCodeForward.h"
+
+#include "llvm/Support/JSON.h"
+
+namespace lldb_vscode {
+
+enum ProgressEventType {
+  progressInvalid,
+  progressStart,
+  progressUpdate,
+  progressEnd
+};
+
+class ProgressEvent {
+public:
+  ProgressEvent() {}
+
+  ProgressEvent(uint64_t progress_id, const char *message, uint64_t completed,
+                uint64_t total);
+
+  llvm::json::Value ToJSON() const;
+
+  /// This operator returns \b true if two event messages
+  /// would result in the same event for the IDE, e.g.
+  /// same rounded percentage.
+  bool operator==(const ProgressEvent &other) const;
+
+  const char *GetEventName() const;
+
+  bool IsValid() const;
+
+  uint64_t GetID() const;
+
+private:
+  uint64_t m_progress_id;
+  const char *m_message;
+  ProgressEventType m_event_type;
+  llvm::Optional<uint32_t> m_percentage;
+};
+
+/// Class that filters out progress event messages that shouldn't be reported
+/// to the IDE, either because they are invalid or because they are too chatty.
+class ProgressEventFilterQueue {
+public:
+  ProgressEventFilterQueue(std::function<void(ProgressEvent)> callback);
+
+  void Push(const ProgressEvent &event);
+
+private:
+  std::function<void(ProgressEvent)> m_callback;
+  std::map<uint64_t, ProgressEvent> m_last_events;
+};
+
+} // namespace lldb_vscode
diff --git a/tools/lldb-vscode/VSCode.cpp b/tools/lldb-vscode/VSCode.cpp
index 8dc7d28..875be9a 100644
--- a/tools/lldb-vscode/VSCode.cpp
+++ b/tools/lldb-vscode/VSCode.cpp
@@ -40,8 +40,10 @@
            {"swift_catch", "Swift Catch", lldb::eLanguageTypeSwift},
            {"swift_throw", "Swift Throw", lldb::eLanguageTypeSwift}}),
       focus_tid(LLDB_INVALID_THREAD_ID), sent_terminated_event(false),
-      stop_at_entry(false), is_attach(false),
-      reverse_request_seq(0), waiting_for_run_in_terminal(false) {
+      stop_at_entry(false), is_attach(false), reverse_request_seq(0),
+      waiting_for_run_in_terminal(false),
+      progress_event_queue(
+          [&](const ProgressEvent &event) { SendJSON(event.ToJSON()); }) {
   const char *log_file_path = getenv("LLDBVSCODE_LOG");
 #if defined(_WIN32)
   // Windows opens stdout and stdin in text mode which converts \n to 13,10
@@ -320,51 +322,8 @@
 //   };
 // }
 
-void VSCode::SendProgressEvent(uint64_t progress_id, const char *message,
-                               uint64_t completed, uint64_t total) {
-  enum ProgressEventType {
-    progressInvalid,
-    progressStart,
-    progressUpdate,
-    progressEnd
-  };
-  const char *event_name = nullptr;
-  ProgressEventType event_type = progressInvalid;
-  if (completed == 0) {
-    event_type = progressStart;
-    event_name = "progressStart";
-  } else if (completed == total) {
-    event_type = progressEnd;
-    event_name = "progressEnd";
-  } else if (completed < total) {
-    event_type = progressUpdate;
-    event_name = "progressUpdate";
-  }
-  if (event_type == progressInvalid)
-    return;
-
-  llvm::json::Object event(CreateEventObject(event_name));
-  llvm::json::Object body;
-  std::string progress_id_str;
-  llvm::raw_string_ostream progress_id_strm(progress_id_str);
-  progress_id_strm << progress_id;
-  progress_id_strm.flush();
-  body.try_emplace("progressId", progress_id_str);
-  if (event_type == progressStart) {
-    EmplaceSafeString(body, "title", message);
-    body.try_emplace("cancellable", false);
-  }
-  auto now = std::chrono::duration<double>(
-      std::chrono::system_clock::now().time_since_epoch());
-  std::string timestamp(llvm::formatv("{0:f9}", now.count()));
-  EmplaceSafeString(body, "timestamp", timestamp);
-
-  if (0 < total && total < UINT64_MAX) {
-    uint32_t percentage = (uint32_t)(((float)completed / (float)total) * 100.0);
-    body.try_emplace("percentage", percentage);
-  }
-  event.try_emplace("body", std::move(body));
-  SendJSON(llvm::json::Value(std::move(event)));
+void VSCode::SendProgressEvent(const ProgressEvent &event) {
+  progress_event_queue.Push(event);
 }
 
 void __attribute__((format(printf, 3, 4)))
diff --git a/tools/lldb-vscode/VSCode.h b/tools/lldb-vscode/VSCode.h
index 0897e00..45250b9 100644
--- a/tools/lldb-vscode/VSCode.h
+++ b/tools/lldb-vscode/VSCode.h
@@ -49,6 +49,7 @@
 #include "ExceptionBreakpoint.h"
 #include "FunctionBreakpoint.h"
 #include "IOStream.h"
+#include "ProgressEvent.h"
 #include "RunInTerminal.h"
 #include "SourceBreakpoint.h"
 #include "SourceReference.h"
@@ -113,6 +114,7 @@
   uint32_t reverse_request_seq;
   std::map<std::string, RequestCallback> request_handlers;
   bool waiting_for_run_in_terminal;
+  ProgressEventFilterQueue progress_event_queue;
   // Keep track of the last stop thread index IDs as threads won't go away
   // unless we send a "thread" event to indicate the thread exited.
   llvm::DenseSet<lldb::tid_t> thread_ids;
@@ -136,8 +138,7 @@
 
   void SendOutput(OutputType o, const llvm::StringRef output);
 
-  void SendProgressEvent(uint64_t progress_id, const char *message,
-                         uint64_t completed, uint64_t total);
+  void SendProgressEvent(const ProgressEvent &event);
 
   void __attribute__((format(printf, 3, 4)))
   SendFormattedOutput(OutputType o, const char *format, ...);
diff --git a/tools/lldb-vscode/lldb-vscode.cpp b/tools/lldb-vscode/lldb-vscode.cpp
index cf68386..fa623d2 100644
--- a/tools/lldb-vscode/lldb-vscode.cpp
+++ b/tools/lldb-vscode/lldb-vscode.cpp
@@ -374,7 +374,8 @@
         const char *message = lldb::SBDebugger::GetProgressFromEvent(
             event, progress_id, completed, total, is_debugger_specific);
         if (message)
-          g_vsc.SendProgressEvent(progress_id, message, completed, total);
+          g_vsc.SendProgressEvent(
+              ProgressEvent(progress_id, message, completed, total));
       }
     }
   }