| 1 | //===-- ProgressEvent.cpp ---------------------------------------*- C++ -*-===// |
| 2 | // |
| 3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| 4 | // See https://llvm.org/LICENSE.txt for license information. |
| 5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| 6 | // |
| 7 | //===----------------------------------------------------------------------===// |
| 8 | |
| 9 | #include "ProgressEvent.h" |
| 10 | |
| 11 | #include "JSONUtils.h" |
| 12 | #include "llvm/Support/ErrorHandling.h" |
| 13 | #include <optional> |
| 14 | |
| 15 | using namespace lldb_dap; |
| 16 | using namespace llvm; |
| 17 | |
| 18 | // The minimum duration of an event for it to be reported |
| 19 | const std::chrono::duration<double> kStartProgressEventReportDelay = |
| 20 | std::chrono::seconds(1); |
| 21 | // The minimum time interval between update events for reporting. If multiple |
| 22 | // updates fall within the same time interval, only the latest is reported. |
| 23 | const std::chrono::duration<double> kUpdateProgressEventReportDelay = |
| 24 | std::chrono::milliseconds(250); |
| 25 | |
| 26 | ProgressEvent::ProgressEvent(uint64_t progress_id, |
| 27 | std::optional<StringRef> message, |
| 28 | uint64_t completed, uint64_t total, |
| 29 | const ProgressEvent *prev_event) |
| 30 | : m_progress_id(progress_id) { |
| 31 | if (message) |
| 32 | m_message = message->str(); |
| 33 | |
| 34 | const bool calculate_percentage = total != UINT64_MAX; |
| 35 | if (completed == 0) { |
| 36 | // Start event |
| 37 | m_event_type = progressStart; |
| 38 | // Wait a bit before reporting the start event in case in completes really |
| 39 | // quickly. |
| 40 | m_minimum_allowed_report_time = |
| 41 | m_creation_time + kStartProgressEventReportDelay; |
| 42 | if (calculate_percentage) |
| 43 | m_percentage = 0; |
| 44 | } else if (completed == total) { |
| 45 | // End event |
| 46 | m_event_type = progressEnd; |
| 47 | // We should report the end event right away. |
| 48 | m_minimum_allowed_report_time = std::chrono::seconds::zero(); |
| 49 | if (calculate_percentage) |
| 50 | m_percentage = 100; |
| 51 | } else { |
| 52 | // Update event |
| 53 | m_event_type = progressUpdate; |
| 54 | m_percentage = std::min( |
| 55 | a: (uint32_t)((double)completed / (double)total * 100.0), b: (uint32_t)99); |
| 56 | if (prev_event->Reported()) { |
| 57 | // Add a small delay between reports |
| 58 | m_minimum_allowed_report_time = |
| 59 | prev_event->m_minimum_allowed_report_time + |
| 60 | kUpdateProgressEventReportDelay; |
| 61 | } else { |
| 62 | // We should use the previous timestamp, as it's still pending |
| 63 | m_minimum_allowed_report_time = prev_event->m_minimum_allowed_report_time; |
| 64 | } |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | std::optional<ProgressEvent> |
| 69 | ProgressEvent::Create(uint64_t progress_id, std::optional<StringRef> message, |
| 70 | uint64_t completed, uint64_t total, |
| 71 | const ProgressEvent *prev_event) { |
| 72 | // If it's an update without a previous event, we abort |
| 73 | if (completed > 0 && completed < total && !prev_event) |
| 74 | return std::nullopt; |
| 75 | ProgressEvent event(progress_id, message, completed, total, prev_event); |
| 76 | // We shouldn't show unnamed start events in the IDE |
| 77 | if (event.GetEventType() == progressStart && event.GetEventName().empty()) |
| 78 | return std::nullopt; |
| 79 | |
| 80 | if (prev_event && prev_event->EqualsForIDE(other: event)) |
| 81 | return std::nullopt; |
| 82 | |
| 83 | return event; |
| 84 | } |
| 85 | |
| 86 | bool ProgressEvent::EqualsForIDE(const ProgressEvent &other) const { |
| 87 | return m_progress_id == other.m_progress_id && |
| 88 | m_event_type == other.m_event_type && |
| 89 | m_percentage == other.m_percentage && m_message == other.m_message; |
| 90 | } |
| 91 | |
| 92 | ProgressEventType ProgressEvent::GetEventType() const { return m_event_type; } |
| 93 | |
| 94 | StringRef ProgressEvent::GetEventName() const { |
| 95 | switch (m_event_type) { |
| 96 | case progressStart: |
| 97 | return "progressStart" ; |
| 98 | case progressUpdate: |
| 99 | return "progressUpdate" ; |
| 100 | case progressEnd: |
| 101 | return "progressEnd" ; |
| 102 | } |
| 103 | llvm_unreachable("All cases handled above!" ); |
| 104 | } |
| 105 | |
| 106 | json::Value ProgressEvent::ToJSON() const { |
| 107 | llvm::json::Object event(CreateEventObject(event_name: GetEventName())); |
| 108 | llvm::json::Object body; |
| 109 | |
| 110 | std::string progress_id_str; |
| 111 | llvm::raw_string_ostream progress_id_strm(progress_id_str); |
| 112 | progress_id_strm << m_progress_id; |
| 113 | body.try_emplace(K: "progressId" , Args&: progress_id_str); |
| 114 | |
| 115 | if (m_event_type == progressStart) { |
| 116 | EmplaceSafeString(obj&: body, key: "title" , str: m_message); |
| 117 | body.try_emplace(K: "cancellable" , Args: false); |
| 118 | } |
| 119 | |
| 120 | if (m_event_type == progressUpdate) |
| 121 | EmplaceSafeString(obj&: body, key: "message" , str: m_message); |
| 122 | |
| 123 | std::string timestamp(llvm::formatv(Fmt: "{0:f9}" , Vals: m_creation_time.count())); |
| 124 | EmplaceSafeString(obj&: body, key: "timestamp" , str: timestamp); |
| 125 | |
| 126 | if (m_percentage) |
| 127 | body.try_emplace(K: "percentage" , Args: *m_percentage); |
| 128 | |
| 129 | event.try_emplace(K: "body" , Args: std::move(body)); |
| 130 | return json::Value(std::move(event)); |
| 131 | } |
| 132 | |
| 133 | bool ProgressEvent::Report(ProgressEventReportCallback callback) { |
| 134 | if (Reported()) |
| 135 | return true; |
| 136 | if (std::chrono::system_clock::now().time_since_epoch() < |
| 137 | m_minimum_allowed_report_time) |
| 138 | return false; |
| 139 | |
| 140 | m_reported = true; |
| 141 | callback(*this); |
| 142 | return true; |
| 143 | } |
| 144 | |
| 145 | bool ProgressEvent::Reported() const { return m_reported; } |
| 146 | |
| 147 | ProgressEventManager::ProgressEventManager( |
| 148 | const ProgressEvent &start_event, |
| 149 | ProgressEventReportCallback report_callback) |
| 150 | : m_start_event(start_event), m_finished(false), |
| 151 | m_report_callback(report_callback) {} |
| 152 | |
| 153 | bool ProgressEventManager::ReportIfNeeded() { |
| 154 | // The event finished before we were able to report it. |
| 155 | if (!m_start_event.Reported() && Finished()) |
| 156 | return true; |
| 157 | |
| 158 | if (!m_start_event.Report(callback: m_report_callback)) |
| 159 | return false; |
| 160 | |
| 161 | if (m_last_update_event) |
| 162 | m_last_update_event->Report(callback: m_report_callback); |
| 163 | return true; |
| 164 | } |
| 165 | |
| 166 | const ProgressEvent &ProgressEventManager::GetMostRecentEvent() const { |
| 167 | return m_last_update_event ? *m_last_update_event : m_start_event; |
| 168 | } |
| 169 | |
| 170 | void ProgressEventManager::Update(uint64_t progress_id, llvm::StringRef message, |
| 171 | uint64_t completed, uint64_t total) { |
| 172 | if (std::optional<ProgressEvent> event = ProgressEvent::Create( |
| 173 | progress_id, message, completed, total, prev_event: &GetMostRecentEvent())) { |
| 174 | if (event->GetEventType() == progressEnd) |
| 175 | m_finished = true; |
| 176 | |
| 177 | m_last_update_event = *event; |
| 178 | ReportIfNeeded(); |
| 179 | } |
| 180 | } |
| 181 | |
| 182 | bool ProgressEventManager::Finished() const { return m_finished; } |
| 183 | |
| 184 | ProgressEventReporter::ProgressEventReporter( |
| 185 | ProgressEventReportCallback report_callback) |
| 186 | : m_report_callback(report_callback) { |
| 187 | m_thread_should_exit = false; |
| 188 | m_thread = std::thread([&] { |
| 189 | while (!m_thread_should_exit) { |
| 190 | std::this_thread::sleep_for(rtime: kUpdateProgressEventReportDelay); |
| 191 | ReportStartEvents(); |
| 192 | } |
| 193 | }); |
| 194 | } |
| 195 | |
| 196 | ProgressEventReporter::~ProgressEventReporter() { |
| 197 | m_thread_should_exit = true; |
| 198 | m_thread.join(); |
| 199 | } |
| 200 | |
| 201 | void ProgressEventReporter::ReportStartEvents() { |
| 202 | std::lock_guard<std::mutex> locker(m_mutex); |
| 203 | |
| 204 | while (!m_unreported_start_events.empty()) { |
| 205 | ProgressEventManagerSP event_manager = m_unreported_start_events.front(); |
| 206 | if (event_manager->Finished()) |
| 207 | m_unreported_start_events.pop(); |
| 208 | else if (event_manager->ReportIfNeeded()) |
| 209 | m_unreported_start_events |
| 210 | .pop(); // we remove it from the queue as it started reporting |
| 211 | // already, the Push method will be able to continue its |
| 212 | // reports. |
| 213 | else |
| 214 | break; // If we couldn't report it, then the next event in the queue won't |
| 215 | // be able as well, as it came later. |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | void ProgressEventReporter::Push(uint64_t progress_id, const char *message, |
| 220 | uint64_t completed, uint64_t total) { |
| 221 | std::lock_guard<std::mutex> locker(m_mutex); |
| 222 | |
| 223 | auto it = m_event_managers.find(x: progress_id); |
| 224 | if (it == m_event_managers.end()) { |
| 225 | if (std::optional<ProgressEvent> event = ProgressEvent::Create( |
| 226 | progress_id, message: StringRef(message), completed, total)) { |
| 227 | ProgressEventManagerSP event_manager = |
| 228 | std::make_shared<ProgressEventManager>(args&: *event, args&: m_report_callback); |
| 229 | m_event_managers.insert(x: {progress_id, event_manager}); |
| 230 | m_unreported_start_events.push(x: event_manager); |
| 231 | } |
| 232 | } else { |
| 233 | it->second->Update(progress_id, message: StringRef(message), completed, total); |
| 234 | if (it->second->Finished()) |
| 235 | m_event_managers.erase(position: it); |
| 236 | } |
| 237 | } |
| 238 | |