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