llvm-project/lldb/tools/lldb-dap/ProgressEvent.cpp
Jacob Lalonde f27cfeae6a
[LLDB][Progress-On-Dap] Have indeterminate progress actually send events. (#140162)
Recently, I got a report how a user 'hung', and come to find out it's
actually because DAP is checking percentage, and on non-deterministic
events, we will effectively never send a progress event to DAP. Here we
short circuit and don't view percentages for DAP messages and solely
depend on the DAP 250ms throttle, which is probably still too
aggressive, but better than no updates.
2025-05-19 15:19:55 -07:00

238 lines
7.9 KiB
C++

//===-- 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"
#include "llvm/Support/ErrorHandling.h"
#include <optional>
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;
}
}
}
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;
}
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!");
}
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;
body.try_emplace("progressId", progress_id_str);
if (m_event_type == progressStart) {
EmplaceSafeString(body, "title", m_message);
body.try_emplace("cancellable", false);
}
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);
event.try_emplace("body", std::move(body));
return json::Value(std::move(event));
}
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();
}
}
bool ProgressEventManager::Finished() const { return m_finished; }
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();
}
});
}
ProgressEventReporter::~ProgressEventReporter() {
m_thread_should_exit = true;
m_thread.join();
}
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.
}
}
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);
}
}