[Lldb-commits] [lldb] a22714d - [lldb-dap] Refactor handling progress events. (#224957)

via lldb-commits lldb-commits at lists.llvm.org
Tue Sep 22 03:52:21 PDT 2026


Author: Ebuka Ezike
Date: 2026-09-22T11:52:10+01:00
New Revision: a22714d1867d7941681c129ea1a37c85541a1ad4

URL: https://github.com/llvm/llvm-project/commit/a22714d1867d7941681c129ea1a37c85541a1ad4
DIFF: https://github.com/llvm/llvm-project/commit/a22714d1867d7941681c129ea1a37c85541a1ad4.diff

LOG: [lldb-dap] Refactor handling progress events. (#224957)

lldb-dap now only uses one thread to receive and throttle received
progress events.
For every progress with a progressId, we delay reporting the progress
for 1000ms until we have recieved a new progress and only send an update
if the progress has a new version after 250ms.

This means for every progress integration test we do nothing for at
least 1250ms. The previous `TestDAP_progress` runs for at least 8s
regardless of how fast the computer is.

Moved most of the test to unittest where we can simulate progress delays
and time passing. We now have only one `TestDAP_progress` test to verify
the client receives progress Events.

When the ProgressEventThread reports a new progressEvent, the reporter
then.
- Create or update the `PendingProgress` with that progressId.
- Determines the action to take based on the ProgressState,
k_start_delay and k_update_interval.
- Sends the apropriate Event based on the action calculated.

Migrate the API test to the new infrastructure.
Add unit tests for the ProgressEventReporter and protocol progressXXXX
POD.

Added: 
    lldb/unittests/DAP/ProgressEventTest.cpp

Modified: 
    lldb/test/API/tools/lldb-dap/progress/Progress_emitter.py
    lldb/test/API/tools/lldb-dap/progress/TestDAP_Progress.py
    lldb/tools/lldb-dap/DAP.cpp
    lldb/tools/lldb-dap/DAP.h
    lldb/tools/lldb-dap/ProgressEvent.cpp
    lldb/tools/lldb-dap/ProgressEvent.h
    lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp
    lldb/tools/lldb-dap/Protocol/ProtocolEvents.h
    lldb/unittests/DAP/CMakeLists.txt
    lldb/unittests/DAP/ProtocolEventsTest.cpp

Removed: 
    


################################################################################
diff  --git a/lldb/test/API/tools/lldb-dap/progress/Progress_emitter.py b/lldb/test/API/tools/lldb-dap/progress/Progress_emitter.py
index 0bf785e3201b0..46098b5d46c4b 100644
--- a/lldb/test/API/tools/lldb-dap/progress/Progress_emitter.py
+++ b/lldb/test/API/tools/lldb-dap/progress/Progress_emitter.py
@@ -1,111 +1,72 @@
-import inspect
 import optparse
 import shlex
-import sys
 import time
 
 import lldb
 
 
-class ProgressTesterCommand:
-    program = "test-progress"
-
-    @classmethod
-    def register_lldb_command(cls, debugger, module_name):
-        parser = cls.create_options()
-        cls.__doc__ = parser.format_help()
-        # Add any commands contained in this module to LLDB
-        command = "command script add -c %s.%s %s" % (
-            module_name,
-            cls.__name__,
-            cls.program,
-        )
-        debugger.HandleCommand(command)
-        print(
-            'The "{0}" command has been installed, type "help {0}" or "{0} '
-            '--help" for detailed help.'.format(cls.program)
-        )
-
-    @classmethod
-    def create_options(cls):
-        usage = "usage: %prog [options]"
-        description = "SBProgress testing tool"
-        # Opt parse is deprecated, but leaving this the way it is because it allows help formating
-        # Additionally all our commands use optparse right now, ideally we migrate them all in one go.
-        parser = optparse.OptionParser(
-            description=description, prog=cls.program, usage=usage
-        )
-
-        parser.add_option(
-            "--total",
-            dest="total",
-            help="Total items in this progress object. When this option is not specified, this will be an indeterminate progress.",
-            type="int",
-            default=None,
-        )
-
-        parser.add_option(
-            "--seconds",
-            dest="seconds",
-            help="Total number of seconds to wait between increments",
-            type="int",
-        )
-
-        parser.add_option(
-            "--no-details",
-            dest="no_details",
-            help="Do not display details",
-            action="store_true",
-            default=False,
-        )
-
-        return parser
-
-    def get_short_help(self):
-        return "Progress Tester"
-
-    def get_long_help(self):
-        return self.help_string
-
-    def __init__(self, debugger, unused):
-        self.parser = self.create_options()
-        self.help_string = self.parser.format_help()
+def make_parser():
+    parser = optparse.OptionParser(
+        prog="send-progress",
+        description="SBProgress testing tool",
+        usage="usage: %prog [options]",
+    )
+    parser.add_option(
+        "--total",
+        type="int",
+        default=None,
+        help="Total items in this progress object. Omit for indeterminate progress.",
+    )
+    parser.add_option(
+        "--seconds",
+        type="float",
+        default=0.0,
+        help="Seconds to sleep between increments.",
+    )
+    parser.add_option(
+        "--no-details",
+        action="store_true",
+        default=False,
+        help="Do not attach a per-step detail string.",
+    )
+    return parser
+
+
+class SendProgressCommand:
+    """Drive an lldb.SBProgress for lldb-dap tests."""
+
+    def __init__(self, debugger, internal_dict):
+        pass
 
     def __call__(self, debugger, command, exe_ctx, result):
-        command_args = shlex.split(command)
         try:
-            (cmd_options, args) = self.parser.parse_args(command_args)
-        except:
+            parser = make_parser()
+            opts, _ = parser.parse_args(shlex.split(command))
+        except SystemExit:
             result.SetError("option parsing failed")
             return
 
-        total = cmd_options.total
-        if total is None:
+        if opts.total is None:
             progress = lldb.SBProgress(
                 "Progress tester", "Initial Indeterminate Detail", debugger
             )
+            iterations = 5
         else:
             progress = lldb.SBProgress(
-                "Progress tester", "Initial Detail", total, debugger
+                "Progress tester", "Initial Detail", opts.total, debugger
             )
-        # Check to see if total is set to None to indicate an indeterminate
-        # progress then default to 3 steps.
-        with progress:
-            if total is None:
-                total = 3
+            iterations = opts.total - 1
 
-            for i in range(1, total):
-                if cmd_options.no_details:
+        with progress:
+            for i in range(iterations):
+                if opts.no_details:
                     progress.Increment(1)
                 else:
                     progress.Increment(1, f"Step {i}")
-                time.sleep(cmd_options.seconds)
+                time.sleep(opts.seconds)
 
 
-def __lldb_init_module(debugger, dict):
-    # Register all classes that have a register_lldb_command method
-    for _name, cls in inspect.getmembers(sys.modules[__name__]):
-        if inspect.isclass(cls) and callable(
-            getattr(cls, "register_lldb_command", None)
-        ):
-            cls.register_lldb_command(debugger, __name__)
+def __lldb_init_module(debugger, internal_dict):
+    debugger.HandleCommand(
+        f"command script add -c {__name__}.SendProgressCommand send-progress"
+    )

diff  --git a/lldb/test/API/tools/lldb-dap/progress/TestDAP_Progress.py b/lldb/test/API/tools/lldb-dap/progress/TestDAP_Progress.py
index 3f57dfb66024d..f41d8cef2b7f8 100755
--- a/lldb/test/API/tools/lldb-dap/progress/TestDAP_Progress.py
+++ b/lldb/test/API/tools/lldb-dap/progress/TestDAP_Progress.py
@@ -1,100 +1,82 @@
 """
-Test lldb-dap output events
+Test lldb-dap progress events (smoke test).
+
+This test only verifies that `ProgressReport` events sent actually reaches the client
+from lldb.
+
+The throttling check is covered by `lldb/unittests/DAP/ProgressEventTest.cpp`.
 """
 
 from lldbsuite.test.decorators import *
-from lldbsuite.test.lldbtest import *
-import json
-import os
-import time
-import re
+from lldbsuite.test.tools.lldb_dap import DAPTestCaseBase, DAPTestSession
+from lldbsuite.test.tools.lldb_dap.types import *
 
-import lldbdap_testcase
+_ProgressEvent = Union[ProgressStartEvent, ProgressUpdateEvent, ProgressEndEvent]
 
 
-class TestDAP_progress(lldbdap_testcase.DAPTestCaseBase):
-    def verify_progress_events(
-        self,
-        expected_title,
-        expected_message=None,
-        expected_message_regex=None,
-        expected_not_in_message=None,
-        only_verify_first_update=False,
-    ):
-        self.dap_server.wait_for_event(["progressEnd"])
-        self.assertTrue(len(self.dap_server.progress_events) > 0)
-        start_found = False
-        update_found = False
-        end_found = False
-        for event in self.dap_server.progress_events:
-            event_type = event["event"]
-            if "progressStart" in event_type:
-                title = event["body"]["title"]
-                self.assertIn(expected_title, title)
-                start_found = True
-            if "progressUpdate" in event_type:
-                message = event["body"]["message"]
-                if only_verify_first_update and update_found:
-                    continue
-                if expected_message is not None:
-                    self.assertIn(expected_message, message)
-                if expected_message_regex is not None:
-                    self.assertTrue(re.match(expected_message_regex, message))
-                if expected_not_in_message is not None:
-                    self.assertNotIn(expected_not_in_message, message)
-                update_found = True
-            if "progressEnd" in event_type:
-                end_found = True
-
-        self.assertTrue(start_found)
-        self.assertTrue(update_found)
-        self.assertTrue(end_found)
-        self.dap_server.progress_events.clear()
+class TestDAP_Progress(DAPTestCaseBase):
+    def collect_progress_events(self, session: DAPTestSession, *, after):
+        """Collect ProgressXXXX events between `after` and the next ProgressEndEvent."""
+        events: List[_ProgressEvent] = []
 
-    @skipIfWindows
-    def test(self):
-        program = self.getBuildArtifact("a.out")
-        self.build_and_launch(program, stopOnEntry=True)
-        progress_emitter = os.path.join(os.getcwd(), "Progress_emitter.py")
-        self.dap_server.request_evaluate(
-            f"`command script import {progress_emitter}", context="repl"
-        )
+        def matches_progress_end(evt) -> bool:
+            events.append(evt)
+            return isinstance(evt, ProgressEndEvent)
 
-        # Test details.
-        self.dap_server.request_evaluate(
-            "`test-progress --total 3 --seconds 1", context="repl"
+        session.wait_for_any_event(
+            (ProgressStartEvent, ProgressUpdateEvent, ProgressEndEvent),
+            after=after,
+            until=matches_progress_end,
+            timeout_msg="Collecting ProgressXXXXEvents until ProgressEndEvent",
         )
+        return events
 
-        self.verify_progress_events(
-            expected_title="Progress tester",
-            expected_not_in_message="Progress tester",
+    def verify_progress_events(
+        self,
+        events: List[_ProgressEvent],
+        *,
+        expected_title: str,
+        expected_message: Optional[str] = None,
+        expected_message_regex: Optional[str] = None,
+        expected_not_in_message: Optional[str] = None,
+    ):
+        # A progress group is shaped: [ProgressStart, ProgressUpdate*, ProgressEnd].
+        self.assertGreaterEqual(
+            len(events), 3, "expected at least start + one update + end"
         )
+        [start, *updates, end] = events
 
-        # Test no details.
-        self.dap_server.request_evaluate(
-            "`test-progress --total 3 --seconds 1 --no-details", context="repl"
-        )
+        self.assertIsInstance(start, ProgressStartEvent)
+        self.assertIn(expected_title, start.body.title)
+        self.assertIsInstance(end, ProgressEndEvent)
 
-        self.verify_progress_events(
-            expected_title="Progress tester",
-            expected_message="Initial Detail",
-        )
+        for update in updates:
+            self.assertIsInstance(update, ProgressUpdateEvent)
+            message = update.body.message or ""
 
-        # Test details indeterminate.
-        self.dap_server.request_evaluate("`test-progress --seconds 1", context="repl")
+            if expected_message is not None:
+                self.assertIn(expected_message, message)
+            if expected_message_regex is not None:
+                self.assertTrue(re.match(expected_message_regex, message))
+            if expected_not_in_message is not None:
+                self.assertNotIn(expected_not_in_message, message)
 
-        self.verify_progress_events(
-            expected_title="Progress tester: Initial Indeterminate Detail",
-            expected_message_regex=r"Step [0-9]+",
-        )
+    @skipIfWindows
+    def test_progress(self):
+        program = self.getBuildArtifact("a.out")
+        session = self.build_and_create_session()
+        process_event = session.launch(LaunchArgs(program, stopOnEntry=True))
+        stopped = session.verify_stopped_on_entry(after=process_event)
 
-        # Test no details indeterminate.
-        self.dap_server.request_evaluate(
-            "`test-progress --seconds 1 --no-details", context="repl"
-        )
+        progress_emitter = self.getSourcePath("Progress_emitter.py")
+        session.evaluate(f"`command script import {progress_emitter}", context="repl")
 
+        # Test details.
+        # 1 progress every 200ms, 10 times = 2s.
+        session.evaluate("`send-progress --total 10 --seconds 0.2", context="repl")
+        events = self.collect_progress_events(session, after=stopped)
         self.verify_progress_events(
-            expected_title="Progress tester: Initial Indeterminate Detail",
-            expected_message="Initial Indeterminate Detail",
-            only_verify_first_update=True,
+            events,
+            expected_title="Progress tester",
+            expected_not_in_message="Progress tester",
         )

diff  --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp
index 7e59541e6c834..c76ea99caaa4f 100644
--- a/lldb/tools/lldb-dap/DAP.cpp
+++ b/lldb/tools/lldb-dap/DAP.cpp
@@ -16,6 +16,7 @@
 #include "JSONUtils.h"
 #include "LLDBUtils.h"
 #include "OutputRedirector.h"
+#include "ProgressEvent.h"
 #include "Protocol/ProtocolBase.h"
 #include "Protocol/ProtocolEvents.h"
 #include "Protocol/ProtocolRequests.h"
@@ -120,11 +121,8 @@ DAP::DAP(Log &log, const ReplMode default_repl_mode,
          const std::vector<String> &pre_init_commands, bool no_lldbinit,
          llvm::StringRef client_name, DAPTransport &transport, MainLoop &loop)
     : log(log), transport(transport), reference_storage(log, configuration),
-      broadcaster("lldb-dap"),
-      progress_event_reporter(
-          [&](const ProgressEvent &event) { SendJSON(event.ToJSON()); }),
-      repl_mode(default_repl_mode), no_lldbinit(no_lldbinit),
-      m_client_name(client_name), m_loop(loop) {
+      broadcaster("lldb-dap"), repl_mode(default_repl_mode),
+      no_lldbinit(no_lldbinit), m_client_name(client_name), m_loop(loop) {
   configuration.preInitCommands = pre_init_commands;
   RegisterRequests();
 }
@@ -423,104 +421,6 @@ void DAP::SendOutput(OutputType o, const llvm::StringRef output) {
   } while (idx < output.size());
 }
 
-// interface ProgressStartEvent extends Event {
-//   event: 'progressStart';
-//
-//   body: {
-//     /**
-//      * An ID that must be used in subsequent 'progressUpdate' and
-//      'progressEnd'
-//      * events to make them refer to the same progress reporting.
-//      * IDs must be unique within a debug session.
-//      */
-//     progressId: string;
-//
-//     /**
-//      * Mandatory (short) title of the progress reporting. Shown in the UI to
-//      * describe the long running operation.
-//      */
-//     title: string;
-//
-//     /**
-//      * The request ID that this progress report is related to. If specified a
-//      * debug adapter is expected to emit
-//      * progress events for the long running request until the request has
-//      been
-//      * either completed or cancelled.
-//      * If the request ID is omitted, the progress report is assumed to be
-//      * related to some general activity of the debug adapter.
-//      */
-//     requestId?: number;
-//
-//     /**
-//      * If true, the request that reports progress may be canceled with a
-//      * 'cancel' request.
-//      * So this property basically controls whether the client should use UX
-//      that
-//      * supports cancellation.
-//      * Clients that don't support cancellation are allowed to ignore the
-//      * setting.
-//      */
-//     cancellable?: boolean;
-//
-//     /**
-//      * Optional, more detailed progress message.
-//      */
-//     message?: string;
-//
-//     /**
-//      * Optional progress percentage to display (value range: 0 to 100). If
-//      * omitted no percentage will be shown.
-//      */
-//     percentage?: number;
-//   };
-// }
-//
-// interface ProgressUpdateEvent extends Event {
-//   event: 'progressUpdate';
-//
-//   body: {
-//     /**
-//      * The ID that was introduced in the initial 'progressStart' event.
-//      */
-//     progressId: string;
-//
-//     /**
-//      * Optional, more detailed progress message. If omitted, the previous
-//      * message (if any) is used.
-//      */
-//     message?: string;
-//
-//     /**
-//      * Optional progress percentage to display (value range: 0 to 100). If
-//      * omitted no percentage will be shown.
-//      */
-//     percentage?: number;
-//   };
-// }
-//
-// interface ProgressEndEvent extends Event {
-//   event: 'progressEnd';
-//
-//   body: {
-//     /**
-//      * The ID that was introduced in the initial 'ProgressStartEvent'.
-//      */
-//     progressId: string;
-//
-//     /**
-//      * Optional, more detailed progress message. If omitted, the previous
-//      * message (if any) is used.
-//      */
-//     message?: string;
-//   };
-// }
-
-void DAP::SendProgressEvent(uint64_t progress_id, const char *message,
-                            uint64_t completed, uint64_t total) {
-  progress_event_reporter.Push(progress_id, message, completed, total);
-}
-
 src_ref_t DAP::CreateSourceReference(lldb::addr_t address) {
   std::lock_guard<std::mutex> guard(m_source_references_mutex);
   auto iter = llvm::find(m_source_references, address);
@@ -1380,57 +1280,42 @@ llvm::Error DAP::InitializeDebugger() {
 }
 
 void DAP::ProgressEventThread(lldb::SBListener listener) {
+  using namespace std::chrono;
+
+  ProgressEventReporter reporter(
+      [this](protocol::Event event) { Send(std::move(event)); });
+  constexpr uint32_t start_delay =
+      duration_cast<seconds>(ProgressEventReporter::k_start_delay).count();
+
   lldb::SBEvent event;
   bool done = false;
   while (!done) {
-    if (listener.WaitForEvent(UINT32_MAX, event)) {
-      const auto event_mask = event.GetType();
-      if (event.BroadcasterMatchesRef(broadcaster)) {
-        if (event_mask & eBroadcastBitStopProgressThread) {
-          done = true;
-        }
-      } else {
-        lldb::SBStructuredData data =
-            lldb::SBDebugger::GetProgressDataFromEvent(event);
-
-        const uint64_t progress_id =
-            GetUintFromStructuredData(data, "progress_id");
-        const uint64_t completed = GetUintFromStructuredData(data, "completed");
-        const uint64_t total = GetUintFromStructuredData(data, "total");
-        const std::string details =
-            GetStringFromStructuredData(data, "details");
-
-        if (completed == 0) {
-          if (total == UINT64_MAX) {
-            // This progress is non deterministic and won't get updated until it
-            // is completed. Send the "message" which will be the combined title
-            // and detail. The only other progress event for thus
-            // non-deterministic progress will be the completed event So there
-            // will be no need to update the detail.
-            const std::string message =
-                GetStringFromStructuredData(data, "message");
-            SendProgressEvent(progress_id, message.c_str(), completed, total);
-          } else {
-            // This progress is deterministic and will receive updates,
-            // on the progress creation event VSCode will save the message in
-            // the create packet and use that as the title, so we send just the
-            // title in the progressCreate packet followed immediately by a
-            // detail packet, if there is any detail.
-            const std::string title =
-                GetStringFromStructuredData(data, "title");
-            SendProgressEvent(progress_id, title.c_str(), completed, total);
-            if (!details.empty())
-              SendProgressEvent(progress_id, details.c_str(), completed, total);
-          }
-        } else {
-          // This progress event is either the end of the progress dialog, or an
-          // update with possible detail. The "detail" string we send to VS Code
-          // will be appended to the progress dialog's initial text from when it
-          // was created.
-          SendProgressEvent(progress_id, details.c_str(), completed, total);
-        }
-      }
+    const uint32_t poll_time = reporter.HasPending() ? start_delay : UINT32_MAX;
+    if (!listener.WaitForEvent(poll_time, event)) {
+      reporter.Drain(steady_clock::now());
+      continue;
+    }
+
+    const auto event_mask = event.GetType();
+    if (event.BroadcasterMatchesRef(broadcaster)) {
+      if (event_mask & eBroadcastBitStopProgressThread)
+        done = true;
+      continue;
     }
+
+    lldb::SBStructuredData data =
+        lldb::SBDebugger::GetProgressDataFromEvent(event);
+    const uint64_t progress_id = GetUintFromStructuredData(data, "progress_id");
+    const uint64_t completed = GetUintFromStructuredData(data, "completed");
+    const uint64_t total = GetUintFromStructuredData(data, "total");
+
+    std::optional<std::string> title = std::nullopt;
+    if (completed == 0) // first event for this progressId.
+      title = GetStringFromStructuredData(data, "title");
+    std::string details = GetStringFromStructuredData(data, "details");
+
+    reporter.Report(progress_id, std::move(title), std::move(details),
+                    completed, total, steady_clock::now());
   }
   DAP_LOG(log, "Stopped ProgressEvent Thread.");
 }

diff  --git a/lldb/tools/lldb-dap/DAP.h b/lldb/tools/lldb-dap/DAP.h
index ed4fa0b2d84c5..99261860d5f1f 100644
--- a/lldb/tools/lldb-dap/DAP.h
+++ b/lldb/tools/lldb-dap/DAP.h
@@ -15,7 +15,6 @@
 #include "FunctionBreakpoint.h"
 #include "InstructionBreakpoint.h"
 #include "OutputRedirector.h"
-#include "ProgressEvent.h"
 #include "Protocol/ProtocolBase.h"
 #include "Protocol/ProtocolRequests.h"
 #include "Protocol/ProtocolTypes.h"
@@ -136,7 +135,6 @@ struct DAP final : public DAPTransport::MessageHandler {
   bool configuration_done;
 
   std::mutex call_mutex;
-  ProgressEventReporter progress_event_reporter;
 
   /// 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.
@@ -235,9 +233,6 @@ struct DAP final : public DAPTransport::MessageHandler {
 
   void SendOutput(OutputType o, const llvm::StringRef output);
 
-  void SendProgressEvent(uint64_t progress_id, const char *message,
-                         uint64_t completed, uint64_t total);
-
   src_ref_t CreateSourceReference(lldb::addr_t address);
 
   std::optional<lldb::addr_t> GetSourceReferenceAddress(src_ref_t reference);

diff  --git a/lldb/tools/lldb-dap/ProgressEvent.cpp b/lldb/tools/lldb-dap/ProgressEvent.cpp
index 5ef03ccb24bb9..0861e2ebc8326 100644
--- a/lldb/tools/lldb-dap/ProgressEvent.cpp
+++ b/lldb/tools/lldb-dap/ProgressEvent.cpp
@@ -1,4 +1,4 @@
-//===-- ProgressEvent.cpp ---------------------------------------*- C++ -*-===//
+//===-- ProgressEvent.cpp -------------------------------------------------===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
@@ -7,232 +7,123 @@
 //===----------------------------------------------------------------------===//
 
 #include "ProgressEvent.h"
+#include "Protocol/ProtocolEvents.h"
 
-#include "JSONUtils.h"
-#include "llvm/Support/ErrorHandling.h"
 #include <optional>
+#include <string>
+#include <utility>
 
 using namespace lldb_dap;
-using namespace llvm;
-
-// The minimum duration of an event for it to be reported
-const std::chrono::duration<double> kStartProgressEventReportDelay =
-    std::chrono::seconds(1);
-// The minimum time interval between update events for reporting. If multiple
-// updates fall within the same time interval, only the latest is reported.
-const std::chrono::duration<double> kUpdateProgressEventReportDelay =
-    std::chrono::milliseconds(250);
-
-ProgressEvent::ProgressEvent(uint64_t progress_id,
-                             std::optional<StringRef> message,
-                             uint64_t completed, uint64_t total,
-                             const ProgressEvent *prev_event)
-    : m_progress_id(progress_id) {
-  if (message)
-    m_message = message->str();
-
-  const bool calculate_percentage = total != UINT64_MAX;
-  if (completed == 0) {
-    // Start event
-    m_event_type = progressStart;
-    // Wait a bit before reporting the start event in case in completes really
-    // quickly.
-    m_minimum_allowed_report_time =
-        m_creation_time + kStartProgressEventReportDelay;
-    if (calculate_percentage)
-      m_percentage = 0;
-  } else if (completed == total) {
-    // End event
-    m_event_type = progressEnd;
-    // We should report the end event right away.
-    m_minimum_allowed_report_time = std::chrono::seconds::zero();
-    if (calculate_percentage)
-      m_percentage = 100;
-  } else {
-    // Update event
-    m_event_type = progressUpdate;
-    m_percentage = std::min(
-        (uint32_t)((double)completed / (double)total * 100.0), (uint32_t)99);
-    if (prev_event->Reported()) {
-      // Add a small delay between reports
-      m_minimum_allowed_report_time =
-          prev_event->m_minimum_allowed_report_time +
-          kUpdateProgressEventReportDelay;
-    } else {
-      // We should use the previous timestamp, as it's still pending
-      m_minimum_allowed_report_time = prev_event->m_minimum_allowed_report_time;
-    }
-  }
-}
+using namespace lldb_dap::protocol;
 
-std::optional<ProgressEvent>
-ProgressEvent::Create(uint64_t progress_id, std::optional<StringRef> message,
-                      uint64_t completed, uint64_t total,
-                      const ProgressEvent *prev_event) {
-  // If it's an update without a previous event, we abort
-  if (completed > 0 && completed < total && !prev_event)
-    return std::nullopt;
-  ProgressEvent event(progress_id, message, completed, total, prev_event);
-  // We shouldn't show unnamed start events in the IDE
-  if (event.GetEventType() == progressStart && event.GetEventName().empty())
-    return std::nullopt;
-
-  if (prev_event && prev_event->EqualsForIDE(event))
-    return std::nullopt;
-
-  return event;
-}
+ProgressEventReporter::ProgressEventReporter(SendEventFn send)
+    : m_send(std::move(send)) {}
 
-bool ProgressEvent::EqualsForIDE(const ProgressEvent &other) const {
-  return m_progress_id == other.m_progress_id &&
-         m_event_type == other.m_event_type &&
-         m_percentage == other.m_percentage && m_message == other.m_message;
-}
-
-ProgressEventType ProgressEvent::GetEventType() const { return m_event_type; }
-
-StringRef ProgressEvent::GetEventName() const {
-  switch (m_event_type) {
-  case progressStart:
-    return "progressStart";
-  case progressUpdate:
-    return "progressUpdate";
-  case progressEnd:
-    return "progressEnd";
-  }
-  llvm_unreachable("All cases handled above!");
-}
+void ProgressEventReporter::Report(uint64_t progress_id,
+                                   std::optional<std::string> title,
+                                   std::string details, uint64_t completed,
+                                   uint64_t total, TimePoint now) {
 
-json::Value ProgressEvent::ToJSON() const {
-  llvm::json::Object event(CreateEventObject(GetEventName()));
-  llvm::json::Object body;
+  auto [it, is_new_progress] = m_pending.try_emplace(progress_id);
+  PendingProgress &pending = it->second;
 
-  std::string progress_id_str;
-  llvm::raw_string_ostream progress_id_strm(progress_id_str);
-  progress_id_strm << m_progress_id;
-  body.try_emplace("progressId", progress_id_str);
-
-  if (m_event_type == progressStart) {
-    EmplaceSafeString(body, "title", m_message);
-    body.try_emplace("cancellable", false);
+  if (is_new_progress) {
+    // Title only updates when it is a new progress.
+    if (!title.has_value()) {
+      m_pending.erase(it);
+      return;
+    }
+    pending.title = std::move(*title);
+    pending.state.start_time = now;
   }
 
-  if (m_event_type == progressUpdate)
-    EmplaceSafeString(body, "message", m_message);
-
-  std::string timestamp(llvm::formatv("{0:f9}", m_creation_time.count()));
-  EmplaceSafeString(body, "timestamp", timestamp);
-
-  if (m_percentage)
-    body.try_emplace("percentage", *m_percentage);
+  pending.latest_details = std::move(details);
+  pending.latest_completed = completed;
+  pending.latest_total = total;
+  pending.state.has_pending_update = true;
 
-  event.try_emplace("body", std::move(body));
-  return json::Value(std::move(event));
+  const bool progress_ended = completed == total;
+  Flush(progress_id, pending, progress_ended, now);
+  if (progress_ended)
+    m_pending.erase(it);
 }
 
-bool ProgressEvent::Report(ProgressEventReportCallback callback) {
-  if (Reported())
-    return true;
-  if (std::chrono::system_clock::now().time_since_epoch() <
-      m_minimum_allowed_report_time)
-    return false;
-
-  m_reported = true;
-  callback(*this);
-  return true;
-}
-
-bool ProgressEvent::Reported() const { return m_reported; }
-
-ProgressEventManager::ProgressEventManager(
-    const ProgressEvent &start_event,
-    ProgressEventReportCallback report_callback)
-    : m_start_event(start_event), m_finished(false),
-      m_report_callback(report_callback) {}
-
-bool ProgressEventManager::ReportIfNeeded() {
-  // The event finished before we were able to report it.
-  if (!m_start_event.Reported() && Finished())
-    return true;
-
-  if (!m_start_event.Report(m_report_callback))
-    return false;
-
-  if (m_last_update_event)
-    m_last_update_event->Report(m_report_callback);
-  return true;
-}
-
-const ProgressEvent &ProgressEventManager::GetMostRecentEvent() const {
-  return m_last_update_event ? *m_last_update_event : m_start_event;
-}
-
-void ProgressEventManager::Update(uint64_t progress_id, llvm::StringRef message,
-                                  uint64_t completed, uint64_t total) {
-  if (std::optional<ProgressEvent> event = ProgressEvent::Create(
-          progress_id, message, completed, total, &GetMostRecentEvent())) {
-    if (event->GetEventType() == progressEnd)
-      m_finished = true;
-
-    m_last_update_event = *event;
-    ReportIfNeeded();
+void ProgressEventReporter::Flush(uint64_t progress_id,
+                                  PendingProgress &pending, bool has_finished,
+                                  TimePoint now) {
+
+  auto percentage = [&]() -> std::optional<uint32_t> {
+    const uint64_t completed = pending.latest_completed;
+    const uint64_t total = pending.latest_total;
+
+    if (total == UINT64_MAX || total == 0 || completed > total)
+      return std::nullopt;
+    return static_cast<uint32_t>(100.0 * completed / total);
+  };
+  auto send_start = [&] {
+    ProgressStartEventBody body;
+    body.progressId = std::to_string(progress_id);
+    body.title = pending.title;
+    body.message = pending.latest_details;
+    body.percentage = percentage();
+    m_send(Event{"progressStart", toJSON(body)});
+  };
+  auto send_update = [&] {
+    ProgressUpdateEventBody body;
+    body.progressId = std::to_string(progress_id);
+    body.message = pending.latest_details;
+    body.percentage = percentage();
+    m_send(Event{"progressUpdate", toJSON(body)});
+  };
+  auto send_end = [&] {
+    ProgressEndEventBody body;
+    body.progressId = std::to_string(progress_id);
+    body.message = pending.latest_details;
+    m_send(Event{"progressEnd", toJSON(body)});
+  };
+
+  State &state = pending.state;
+  switch (GetAction(state, now, has_finished)) {
+  case Action::SendStart:
+    send_start();
+    state.start_sent = true;
+    state.has_pending_update = false;
+    state.last_send_time = now;
+    break;
+  case Action::SendStartAndEnd:
+    send_start();
+    send_end();
+    break;
+  case Action::SendEnd:
+    send_end();
+    break;
+  case Action::SendUpdate:
+    send_update();
+    state.has_pending_update = false;
+    state.last_send_time = now;
+    break;
+  case Action::None:
+    break;
   }
 }
 
-bool ProgressEventManager::Finished() const { return m_finished; }
+ProgressEventReporter::Action
+ProgressEventReporter::GetAction(const State &state, TimePoint now,
+                                 bool has_finished) {
+  if (!state.start_sent) {
+    if (state.start_time + k_start_delay > now)
+      return Action::None;
 
-ProgressEventReporter::ProgressEventReporter(
-    ProgressEventReportCallback report_callback)
-    : m_report_callback(report_callback) {
-  m_thread_should_exit = false;
-  m_thread = std::thread([&] {
-    while (!m_thread_should_exit) {
-      std::this_thread::sleep_for(kUpdateProgressEventReportDelay);
-      ReportStartEvents();
-    }
-  });
-}
+    return has_finished ? Action::SendStartAndEnd : Action::SendStart;
+  }
 
-ProgressEventReporter::~ProgressEventReporter() {
-  m_thread_should_exit = true;
-  if (m_thread.joinable())
-    m_thread.join();
-}
+  if (has_finished)
+    return Action::SendEnd;
 
-void ProgressEventReporter::ReportStartEvents() {
-  std::lock_guard<std::mutex> locker(m_mutex);
-
-  while (!m_unreported_start_events.empty()) {
-    ProgressEventManagerSP event_manager = m_unreported_start_events.front();
-    if (event_manager->Finished())
-      m_unreported_start_events.pop();
-    else if (event_manager->ReportIfNeeded())
-      m_unreported_start_events
-          .pop(); // we remove it from the queue as it started reporting
-                  // already, the Push method will be able to continue its
-                  // reports.
-    else
-      break; // If we couldn't report it, then the next event in the queue won't
-             // be able as well, as it came later.
-  }
-}
+  if (!state.has_pending_update)
+    return Action::None;
 
-void ProgressEventReporter::Push(uint64_t progress_id, const char *message,
-                                 uint64_t completed, uint64_t total) {
-  std::lock_guard<std::mutex> locker(m_mutex);
-
-  auto it = m_event_managers.find(progress_id);
-  if (it == m_event_managers.end()) {
-    if (std::optional<ProgressEvent> event = ProgressEvent::Create(
-            progress_id, StringRef(message), completed, total)) {
-      ProgressEventManagerSP event_manager =
-          std::make_shared<ProgressEventManager>(*event, m_report_callback);
-      m_event_managers.insert({progress_id, event_manager});
-      m_unreported_start_events.push(event_manager);
-    }
-  } else {
-    it->second->Update(progress_id, StringRef(message), completed, total);
-    if (it->second->Finished())
-      m_event_managers.erase(it);
-  }
+  if (state.last_send_time + k_update_interval > now)
+    return Action::None; // throttled
+  return Action::SendUpdate;
 }

diff  --git a/lldb/tools/lldb-dap/ProgressEvent.h b/lldb/tools/lldb-dap/ProgressEvent.h
index 9dfed4c301a8e..d569265b4e4ce 100644
--- a/lldb/tools/lldb-dap/ProgressEvent.h
+++ b/lldb/tools/lldb-dap/ProgressEvent.h
@@ -1,4 +1,4 @@
-//===-- ProgressEvent.cpp ---------------------------------------*- C++ -*-===//
+//===-- ProgressEvent.h -----------------------------------------*- C++ -*-===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
@@ -9,155 +9,112 @@
 #ifndef LLDB_TOOLS_LLDB_DAP_PROGRESS_EVENT_H
 #define LLDB_TOOLS_LLDB_DAP_PROGRESS_EVENT_H
 
-#include <atomic>
-#include <chrono>
-#include <mutex>
-#include <optional>
-#include <queue>
-#include <thread>
+#include "Protocol/ProtocolBase.h"
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/FunctionExtras.h"
 
-#include "llvm/Support/JSON.h"
+#include <chrono>
+#include <cstdint>
+#include <string>
 
 namespace lldb_dap {
 
-enum ProgressEventType { progressStart, progressUpdate, progressEnd };
-
-class ProgressEvent;
-using ProgressEventReportCallback = std::function<void(ProgressEvent &)>;
-
-class ProgressEvent {
-public:
-  /// Actual constructor to use that returns an optional, as the event might be
-  /// not apt for the IDE, e.g. an unnamed start event, or a redundant one.
-  ///
-  /// \param[in] progress_id
-  ///   ID for this event.
-  ///
-  /// \param[in] message
-  ///   Message to display in the UI. Required for start events.
-  ///
-  /// \param[in] completed
-  ///   Number of jobs completed.
-  ///
-  /// \param[in] total
-  ///   Total number of jobs, or \b UINT64_MAX if not determined.
-  ///
-  /// \param[in] prev_event
-  ///   Previous event if this one is an update. If \b nullptr, then a start
-  ///   event will be created.
-  static std::optional<ProgressEvent>
-  Create(uint64_t progress_id, std::optional<llvm::StringRef> message,
-         uint64_t completed, uint64_t total,
-         const ProgressEvent *prev_event = nullptr);
-
-  llvm::json::Value ToJSON() const;
-
-  /// \return
-  ///       \b true if two event messages would result in the same event for the
-  ///       IDE, e.g. same rounded percentage.
-  bool EqualsForIDE(const ProgressEvent &other) const;
-
-  llvm::StringRef GetEventName() const;
-
-  ProgressEventType GetEventType() const;
-
-  /// Report this progress event to the provided callback only if enough time
-  /// has passed since the creation of the event and since the previous reported
-  /// update.
-  bool Report(ProgressEventReportCallback callback);
-
-  bool Reported() const;
-
-private:
-  ProgressEvent(uint64_t progress_id, std::optional<llvm::StringRef> message,
-                uint64_t completed, uint64_t total,
-                const ProgressEvent *prev_event);
-
-  uint64_t m_progress_id;
-  std::string m_message;
-  ProgressEventType m_event_type;
-  std::optional<uint32_t> m_percentage;
-  std::chrono::duration<double> m_creation_time =
-      std::chrono::system_clock::now().time_since_epoch();
-  std::chrono::duration<double> m_minimum_allowed_report_time;
-  bool m_reported = false;
-};
-
-/// Class that keeps the start event and its most recent update.
-/// It controls when the event should start being reported to the IDE.
-class ProgressEventManager {
-public:
-  ProgressEventManager(const ProgressEvent &start_event,
-                       ProgressEventReportCallback report_callback);
-
-  /// Report the start event and the most recent update if the event has lasted
-  /// for long enough.
-  ///
-  /// \return
-  ///     \b false if the event hasn't finished and hasn't reported anything
-  ///     yet.
-  bool ReportIfNeeded();
-
-  /// Receive a new progress event for the start event and try to report it if
-  /// appropriate.
-  void Update(uint64_t progress_id, llvm::StringRef message, uint64_t completed,
-              uint64_t total);
-
-  /// \return
-  ///     \b true if a \a progressEnd event has been notified. There's no
-  ///     need to try to report manually an event that has finished.
-  bool Finished() const;
-
-  const ProgressEvent &GetMostRecentEvent() const;
-
-private:
-  ProgressEvent m_start_event;
-  std::optional<ProgressEvent> m_last_update_event;
-  bool m_finished;
-  ProgressEventReportCallback m_report_callback;
-};
-
-using ProgressEventManagerSP = std::shared_ptr<ProgressEventManager>;
-
-/// Class that filters out progress event messages that shouldn't be reported
-/// to the IDE, because they are invalid, they carry no new information, or they
-/// don't last long enough.
+/// Translates lldb `Progress` events into DAP progress events.
 ///
-/// We need to limit the amount of events that are sent to the IDE, as they slow
-/// the render thread of the UI user, and they end up spamming the DAP
-/// connection, which also takes some processing time out of the IDE.
+/// Filters out progress events that shouldn't be reported either because they
+/// spam the client's UI or they don't last long enough.
 class ProgressEventReporter {
 public:
-  /// \param[in] report_callback
-  ///     Function to invoke to report the event to the IDE.
-  explicit ProgressEventReporter(ProgressEventReportCallback report_callback);
+  /// The Callback used to send DAP progress events.
+  using SendEventFn = llvm::unique_function<void(protocol::Event)>;
+
+  using TimePoint = std::chrono::steady_clock::time_point;
+
+  explicit ProgressEventReporter(SendEventFn send);
 
   ProgressEventReporter(const ProgressEventReporter &) = delete;
   ProgressEventReporter(ProgressEventReporter &&) = delete;
   ProgressEventReporter &operator=(const ProgressEventReporter &) = delete;
   ProgressEventReporter &operator=(ProgressEventReporter &&) = delete;
-  ~ProgressEventReporter();
+  ~ProgressEventReporter() = default;
+
+  // The minimum duration of an event for it to be reported
+  static constexpr auto k_start_delay = std::chrono::milliseconds(1000);
+  // The minimum time interval between update events for reporting. If multiple
+  // updates fall within the same time interval, only the latest is reported.
+  static constexpr auto k_update_interval = std::chrono::milliseconds(250);
+
+  /// Reports a new lldb progress event and determines if we need to send
+  /// it to the client. `now` is the wall time observed by the caller.
+  void Report(uint64_t progress_id, std::optional<std::string> title,
+              std::string details, uint64_t completed, uint64_t total,
+              TimePoint now);
+
+  /// Flush any pending events whose deadlines have passed.
+  ///
+  /// \param now
+  ///   The current wall timepoint of the caller.
+  void Drain(TimePoint now) {
+    for (auto &[progress_id, pending] : m_pending)
+      Flush(progress_id, pending, /*has_finished=*/false, now);
+  }
 
-  /// Add a new event to the internal queue and report the event if
-  /// appropriate.
-  void Push(uint64_t progress_id, const char *message, uint64_t completed,
-            uint64_t total);
+  /// Whether the reporter is currently tracking any progress. Used to decide
+  /// if we need to wait indefinitely for the next event.
+  bool HasPending() const { return !m_pending.empty(); }
 
 private:
-  /// Report to the IDE events that haven't been reported to the IDE and have
-  /// lasted long enough.
-  void ReportStartEvents();
-
-  ProgressEventReportCallback m_report_callback;
-  std::map<uint64_t, ProgressEventManagerSP> m_event_managers;
-  /// Queue of start events in chronological order
-  std::queue<ProgressEventManagerSP> m_unreported_start_events;
-  /// Thread used to invoke \a ReportStartEvents periodically.
-  std::thread m_thread;
-  std::atomic<bool> m_thread_should_exit;
-  /// Mutex that prevents running \a Push and \a ReportStartEvents
-  /// simultaneously, as both read and modify the same underlying objects.
-  std::mutex m_mutex;
+  /// The action the reporter should take for a given state and time.
+  enum class Action {
+    /// Do nothing.
+    None,
+    /// Send `progressStart`
+    SendStart,
+    /// Send `progressStart` and `progressEnd`.
+    SendStartAndEnd,
+    /// Send `progressEnd`.
+    SendEnd,
+    /// Send `progressUpdate`.
+    SendUpdate,
+  };
+
+  /// The current state of a pending progress.
+  struct State {
+    TimePoint start_time;
+    TimePoint last_send_time;
+    bool start_sent = false;
+    bool has_pending_update = false;
+  };
+
+  struct PendingProgress {
+    /// Fields that end up in DAP progressEvent bodies.
+    std::string title;
+    std::string latest_details;
+    uint64_t latest_completed = 0;
+    uint64_t latest_total = 0;
+
+    State state;
+  };
+
+  /// Determines the if the reporter needs to do nothing or what
+  /// progressEvent(s) it needs to send.
+  static Action GetAction(const State &state, TimePoint now, bool has_finished);
+
+  /// Send progressEvent for the progress id if it is needed.
+  /// \param progress_id
+  ///   The id for the progressEvent to send.
+  /// \param pending
+  ///   The data and state of the pending progress.
+  /// \param has_finished.
+  ///   If the progress for the id has ended.
+  /// \param now.
+  ///   Is the wall time observed by the caller to determine if we need to send
+  ///   a new progress event see `GetAction` for the heuristic.
+  void Flush(uint64_t progress_id, PendingProgress &pending, bool has_finished,
+             TimePoint now);
+
+  SendEventFn m_send;
+  llvm::DenseMap<uint64_t, PendingProgress> m_pending;
 };
 
 } // namespace lldb_dap

diff  --git a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp
index 99e3ceec08fb2..f43dd345585cc 100644
--- a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp
+++ b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp
@@ -114,4 +114,34 @@ llvm::json::Value toJSON(const StoppedEventBody &SEB) {
   return Result;
 }
 
+llvm::json::Value toJSON(const ProgressStartEventBody &PSB) {
+  llvm::json::Object Result{{"progressId", PSB.progressId},
+                            {"title", PSB.title}};
+  if (PSB.message)
+    Result.insert({"message", *PSB.message});
+  if (PSB.requestId)
+    Result.insert({"requestId", *PSB.requestId});
+  if (PSB.percentage)
+    Result.insert({"percentage", *PSB.percentage});
+  if (PSB.cancellable)
+    Result.insert({"cancellable", *PSB.cancellable});
+  return Result;
+}
+
+llvm::json::Value toJSON(const ProgressUpdateEventBody &PUB) {
+  llvm::json::Object Result{{"progressId", PUB.progressId}};
+  if (PUB.message)
+    Result.insert({"message", *PUB.message});
+  if (PUB.percentage)
+    Result.insert({"percentage", *PUB.percentage});
+  return Result;
+}
+
+llvm::json::Value toJSON(const ProgressEndEventBody &PEB) {
+  llvm::json::Object Result{{"progressId", PEB.progressId}};
+  if (PEB.message)
+    Result.insert({"message", *PEB.message});
+  return Result;
+}
+
 } // namespace lldb_dap::protocol

diff  --git a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h
index 5c415f76c37fd..1d38d9a7b941c 100644
--- a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h
+++ b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h
@@ -179,6 +179,81 @@ struct StoppedEventBody {
 };
 llvm::json::Value toJSON(const StoppedEventBody &);
 
+/// The event signals that a long running operation is about to start and
+/// provides additional information for the client to set up a corresponding
+/// progress and cancellation UI.
+///
+/// The client is free to delay the showing of the UI in order to reduce
+/// flicker.
+///
+/// This event should only be sent if the corresponding capability
+/// `supportsProgressReporting` is true
+struct ProgressStartEventBody {
+  /// An ID that can be used in subsequent `progressUpdate` and `progressEnd`
+  /// events to make them refer to the same progress reporting. IDs must be
+  /// unique within a debug session.
+  String progressId;
+
+  /// Short title of the progress reporting. Shown in the UI to describe the
+  /// long running operation.
+  String title;
+
+  /// More detailed progress message.
+  std::optional<String> message;
+
+  /// The request ID that this progress report is related to. If specified a
+  /// debug adapter is expected to emit progress events for the long running
+  /// request until the request has been either completed or cancelled.  If the
+  /// request ID is omitted, the progress report is assumed to be related to
+  /// some general activity of the debug adapter.
+  std::optional<uint32_t> requestId;
+
+  /// Progress percentage to display (value range: 0 to 100). If omitted no
+  /// percentage is shown.
+  std::optional<uint32_t> percentage;
+
+  /// If true, the request that reports progress may be cancelled with a
+  /// `cancel` request.
+  std::optional<bool> cancellable;
+};
+llvm::json::Value toJSON(const ProgressStartEventBody &);
+
+/// The event signals that the progress reporting needs to be updated with a new
+/// message and/or percentage.
+///
+/// The client does not have to update the UI immediately, but the clients needs
+/// to keep track of the message and/or percentage values.
+///
+/// This event should only be sent if the corresponding capability
+/// `supportsProgressReporting` is true.
+struct ProgressUpdateEventBody {
+  /// The ID that was introduced in the initial `ProgressStartEventBody` event.
+  String progressId;
+
+  /// More detailed progress message. If omitted, the previous message (if any)
+  /// is used.
+  std::optional<String> message;
+
+  /// Progress percentage to display (value range: 0 to 100). If omitted no
+  /// percentage is shown.
+  std::optional<uint32_t> percentage;
+};
+llvm::json::Value toJSON(const ProgressUpdateEventBody &);
+
+/// The event signals the end of the progress reporting with a final message.
+///
+/// This event should only be sent if the corresponding capability
+/// `supportsProgressReporting` is true.
+struct ProgressEndEventBody {
+  /// The ID that was introduced in the initial `ProgressStartEventBody` event.
+  String progressId;
+
+  /// More detailed progress message. If omitted, the previous message (if any)
+  /// is used.
+  std::optional<String> message;
+};
+llvm::json::Value toJSON(const ProgressEndEventBody &);
+
 } // end namespace lldb_dap::protocol
 
 #endif

diff  --git a/lldb/unittests/DAP/CMakeLists.txt b/lldb/unittests/DAP/CMakeLists.txt
index 3fce38d2b582f..18ab66f5decdc 100644
--- a/lldb/unittests/DAP/CMakeLists.txt
+++ b/lldb/unittests/DAP/CMakeLists.txt
@@ -15,6 +15,7 @@ add_lldb_unittest(DAPTests
   ProtocolRequestsTest.cpp
   ProtocolTypesTest.cpp
   ProtocolUtilsTest.cpp
+  ProgressEventTest.cpp
   RunInTerminalTest.cpp
   TestBase.cpp
   VariablesTest.cpp

diff  --git a/lldb/unittests/DAP/ProgressEventTest.cpp b/lldb/unittests/DAP/ProgressEventTest.cpp
new file mode 100644
index 0000000000000..92110edd90b11
--- /dev/null
+++ b/lldb/unittests/DAP/ProgressEventTest.cpp
@@ -0,0 +1,254 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 "Protocol/ProtocolBase.h"
+#include "TestingSupport/TestUtilities.h"
+#include "llvm/Support/JSON.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+#include <chrono>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace lldb_dap;
+using namespace lldb_dap::protocol;
+using namespace std::chrono_literals;
+
+using ::testing::AllOf;
+using ::testing::ElementsAre;
+using ::testing::IsEmpty;
+
+namespace lldb_dap::protocol {
+// For formatted error messages.
+// see https://google.github.io/googletest/advanced.html.
+void PrintTo(const Event &e, std::ostream *os) {
+  *os << lldb_private::PrettyPrint(toJSON(e));
+}
+} // namespace lldb_dap::protocol
+
+namespace {
+
+const llvm::json::Value *BodyField(const Event &e, llvm::StringRef key) {
+  if (!e.body)
+    return nullptr;
+  const auto *obj = e.body->getAsObject();
+  if (!obj)
+    return nullptr;
+  return obj->get(key);
+}
+
+std::string BodyFieldStr(const Event &e, llvm::StringRef key) {
+  const auto *v = BodyField(e, key);
+  if (!v)
+    return {};
+  return v->getAsString().value_or("").str();
+}
+
+MATCHER_P(ProgressKind, kind,
+          "event.event is " + testing::PrintToString(kind)) {
+  return arg.event == std::string(kind);
+}
+
+MATCHER_P(HasId, id, "body.progressId is " + testing::PrintToString(id)) {
+  return BodyFieldStr(arg, "progressId") == std::to_string(id);
+}
+
+MATCHER_P(HasMessage, message,
+          "body.message is " + testing::PrintToString(message)) {
+  return BodyFieldStr(arg, "message") == std::string(message);
+}
+
+MATCHER_P(HasTitle, title, "body.title is " + testing::PrintToString(title)) {
+  return BodyFieldStr(arg, "title") == std::string(title);
+}
+
+MATCHER(HasNoPercentage, "body has no percentage field") {
+  return BodyField(arg, "percentage") == nullptr;
+}
+
+MATCHER_P(HasPercentage, expected,
+          "body.percentage is " + testing::PrintToString(expected)) {
+  const auto *v = BodyField(arg, "percentage");
+  if (!v)
+    return false;
+  auto n = v->getAsNumber();
+  return n && *n == expected;
+}
+
+template <typename... Fields>
+auto Progress(llvm::StringRef kind, Fields &&...fields) {
+  return AllOf(ProgressKind(kind), std::forward<Fields>(fields)...);
+}
+
+class ProgressEventReporterTest : public ::testing::Test {
+protected:
+  ProgressEventReporter::TimePoint now{std::chrono::seconds(0)};
+  std::vector<Event> sent_events;
+  protocol::Id seq_id = 0;
+
+  ProgressEventReporter m_reporter{[this](Event e) {
+    e.seq = ++seq_id;
+    sent_events.push_back(std::move(e));
+  }};
+
+  void IncreaseTime(std::chrono::milliseconds d) { now += d; }
+
+  // Advance the fake clock by `offset`, then Report.
+  void ReportAt(std::chrono::milliseconds offset, uint64_t id,
+                llvm::StringRef title, llvm::StringRef details,
+                uint64_t completed, uint64_t total) {
+    IncreaseTime(offset);
+    m_reporter.Report(id, title.str(), details.str(), completed, total, now);
+  }
+};
+
+/// Progress reported before the start delay is not sent.
+TEST_F(ProgressEventReporterTest, DropsFastProgress) {
+  const llvm::StringRef title = "Parsing Compile Units";
+  ReportAt(0ms, 1, title, "", 0, 4);
+  ReportAt(ProgressEventReporter::k_start_delay - 500ms, 1, title, "foo.cpp", 1,
+           4);
+  ReportAt(0ms, 1, title, "bar.cpp", 2, 4);
+  ReportAt(0ms, 1, title, "baz.cpp", 3, 4);
+
+  EXPECT_THAT(sent_events, IsEmpty());
+}
+
+/// Progress ending without an update after the start delay, sends both the
+/// start and end progressEvents.
+TEST_F(ProgressEventReporterTest, EmitsStartAndEndProgressAfterStartDelay) {
+  const llvm::StringRef title = "Reading all files";
+  const llvm::StringRef end_detail = "done reading.";
+  ReportAt(1ms, 1, title, "in folder", 0, 3);
+  ReportAt(ProgressEventReporter::k_start_delay + 500ms, 1, title, end_detail,
+           3, 3);
+
+  auto expected_events = ElementsAre(
+      Progress("progressStart", HasTitle(title), HasMessage(end_detail)),
+      Progress("progressEnd", HasMessage(end_detail)));
+  EXPECT_THAT(sent_events, expected_events);
+}
+
+TEST_F(ProgressEventReporterTest, DrainEmitsStartForIdleProgress) {
+  ReportAt(0ms, 1, "title", "", 0, 3);
+  EXPECT_THAT(sent_events, IsEmpty()); // buffered
+
+  IncreaseTime(ProgressEventReporter::k_start_delay + 500ms);
+  m_reporter.Drain(now);
+
+  EXPECT_THAT(sent_events, ElementsAre(Progress("progressStart", HasId(1))));
+
+  // Calling drain multiple times does not create new events.
+  for (int i = 0; i < 10; i++)
+    m_reporter.Drain(now);
+  EXPECT_TRUE(m_reporter.HasPending());
+  EXPECT_THAT(sent_events, ElementsAre(Progress("progressStart")));
+}
+
+/// Multiple update progessEvents are reported until after the update_interval.
+TEST_F(ProgressEventReporterTest, ThrottlesUpdatesWithinInterval) {
+  // Send the start.
+  const uint32_t id = 10;
+  ReportAt(0ms, id, "throttled", "d", 0, 10);
+  ReportAt(ProgressEventReporter::k_start_delay + 10ms, id, "throttled", "d1",
+           1, 10);
+  ASSERT_EQ(sent_events.size(), 1U);
+
+  // Report an update before the update_internal.
+  ReportAt(ProgressEventReporter::k_update_interval - 50ms, id, "throttled",
+           "d2", 2, 10);
+  ASSERT_EQ(sent_events.size(), 1U);
+
+  // Report an update after the update_interval.
+  ReportAt(ProgressEventReporter::k_update_interval * 2, id, "throttled", "d3",
+           3, 10);
+
+  // Report end.
+  ReportAt(0ms, id, "", "throttled end", 10, 10);
+
+  auto expected_events = ElementsAre(
+      Progress("progressStart", HasTitle("throttled")),
+      Progress("progressUpdate", HasMessage("d3")),
+      Progress("progressEnd", HasId(id), HasMessage("throttled end")));
+  ASSERT_THAT(sent_events, expected_events);
+}
+
+TEST_F(ProgressEventReporterTest, EndFlushesLatestSnapshotEvenWhenThrottled) {
+  const uint32_t id = 32;
+  ReportAt(0ms, id, "t", "d0", 0, 10);
+  ReportAt(ProgressEventReporter::k_start_delay + 100ms, id, "t", "d1", 1, 10);
+  ASSERT_EQ(sent_events.size(), 1U);
+  ASSERT_THAT(sent_events[0], Progress("progressStart", HasTitle("t")));
+
+  // Throttled update.
+  ReportAt(50ms, id, "", "d2", 2, 10);
+  ASSERT_EQ(sent_events.size(), 1U);
+
+  // Report end.
+  ReportAt(0ms, id, "t", "final", 10, 10);
+
+  EXPECT_THAT(sent_events,
+              ElementsAre(Progress("progressStart"),
+                          Progress("progressEnd", HasMessage("final"))));
+}
+
+TEST_F(ProgressEventReporterTest, IndeterminateProgressOmitsPercentage) {
+  const uint64_t total = UINT64_MAX;
+  const std::string title = "Indeterminate";
+
+  ReportAt(0ms, 1, title, "d", 0, total);
+  ReportAt(ProgressEventReporter::k_start_delay + 500ms, 1, title, "d", 5,
+           total);
+  // Report End.
+  ReportAt(10ms, 1, "", "end", total, total);
+
+  EXPECT_THAT(sent_events,
+              ElementsAre(Progress("progressStart", HasNoPercentage()),
+                          Progress("progressEnd", HasMessage("end"))));
+}
+
+TEST_F(ProgressEventReporterTest, DeterministicProgressEmitsPercentage) {
+  ReportAt(0ms, 1, "t", "d", 0, 4);
+  ReportAt(ProgressEventReporter::k_start_delay + 500ms, 1, "t", "d", 2, 4);
+
+  EXPECT_THAT(sent_events,
+              ElementsAre(Progress("progressStart", HasPercentage(50))));
+}
+
+TEST_F(ProgressEventReporterTest, IndependentIdsAreTrackedSeparately) {
+  const uint32_t id_1 = 1;
+  const uint32_t id_2 = 2;
+  ReportAt(0ms, id_1, "one", "d1", 0, 3);
+  ReportAt(0ms, id_2, "two", "d2", 0, 3);
+  ReportAt(ProgressEventReporter::k_start_delay + 500ms, id_1, "one", "d1", 3,
+           3);
+  ReportAt(0ms, id_2, "two", "d2", 3, 3);
+  EXPECT_FALSE(m_reporter.HasPending());
+
+  EXPECT_THAT(
+      sent_events,
+      ElementsAre(Progress("progressStart", HasId(1)),
+                  Progress("progressEnd", HasId(id_1), HasMessage("d1")),
+                  Progress("progressStart", HasId(id_2)),
+                  Progress("progressEnd", HasId(id_2), HasMessage("d2"))));
+}
+
+/// A new progress reported without a title is dropped.
+/// It cannot reach this state based on the current lldb_private::Progress
+/// implementation because `completed` always starts at 0.
+TEST_F(ProgressEventReporterTest, NewProgressWithoutTitleIsDropped) {
+  m_reporter.Report(/*progress_id=*/1, /*title=*/std::nullopt, /*details=*/"d",
+                    /*completed=*/0, /*total=*/3, now);
+  EXPECT_FALSE(m_reporter.HasPending());
+  EXPECT_THAT(sent_events, IsEmpty());
+}
+
+} // namespace

diff  --git a/lldb/unittests/DAP/ProtocolEventsTest.cpp b/lldb/unittests/DAP/ProtocolEventsTest.cpp
index b6efc2791e578..28da96d14e846 100644
--- a/lldb/unittests/DAP/ProtocolEventsTest.cpp
+++ b/lldb/unittests/DAP/ProtocolEventsTest.cpp
@@ -43,3 +43,68 @@ TEST(ProtocolEventsTest, StoppedEventBody) {
   ASSERT_THAT_EXPECTED(expected_body, llvm::Succeeded());
   EXPECT_EQ(PrettyPrint(*expected_body), PrettyPrint(body));
 }
+
+TEST(ProtocolEventsTest, ProgressStartEventBody) {
+  ProgressStartEventBody body;
+  body.progressId = "1";
+  body.title = "Parsing symbols";
+  Expected<Value> expected_body = parse(R"({
+    "progressId": "1",
+    "title": "Parsing symbols"
+  })");
+  ASSERT_THAT_EXPECTED(expected_body, llvm::Succeeded());
+  EXPECT_EQ(PrettyPrint(*expected_body), PrettyPrint(body));
+
+  body.message = "foo.cpp";
+  body.requestId = 30;
+  body.percentage = 25;
+  body.cancellable = true;
+  expected_body = parse(R"({
+    "progressId": "1",
+    "title": "Parsing symbols",
+    "message": "foo.cpp",
+    "requestId": 30,
+    "percentage": 25,
+    "cancellable": true
+  })");
+  ASSERT_THAT_EXPECTED(expected_body, llvm::Succeeded());
+  EXPECT_EQ(PrettyPrint(*expected_body), PrettyPrint(body));
+}
+
+TEST(ProtocolEventsTest, ProgressUpdateEventBody) {
+  ProgressUpdateEventBody body;
+  body.progressId = "1";
+  Expected<Value> expected_body = parse(R"({
+    "progressId": "1"
+  })");
+  ASSERT_THAT_EXPECTED(expected_body, llvm::Succeeded());
+  EXPECT_EQ(PrettyPrint(*expected_body), PrettyPrint(body));
+
+  body.message = "bar.cpp";
+  body.percentage = 75;
+  expected_body = parse(R"({
+    "progressId": "1",
+    "message": "bar.cpp",
+    "percentage": 75
+  })");
+  ASSERT_THAT_EXPECTED(expected_body, llvm::Succeeded());
+  EXPECT_EQ(PrettyPrint(*expected_body), PrettyPrint(body));
+}
+
+TEST(ProtocolEventsTest, ProgressEndEventBody) {
+  ProgressEndEventBody body;
+  body.progressId = "1";
+  Expected<Value> expected_body = parse(R"({
+    "progressId": "1"
+  })");
+  ASSERT_THAT_EXPECTED(expected_body, llvm::Succeeded());
+  EXPECT_EQ(PrettyPrint(*expected_body), PrettyPrint(body));
+
+  body.message = "done.";
+  expected_body = parse(R"({
+    "progressId": "1",
+    "message": "done."
+  })");
+  ASSERT_THAT_EXPECTED(expected_body, llvm::Succeeded());
+  EXPECT_EQ(PrettyPrint(*expected_body), PrettyPrint(body));
+}


        


More information about the lldb-commits mailing list