Skip to content

Commit 156c290

Browse files
committed
[lldb] Implement coalescing of disjoint progress events (llvm#84854)
This implements coalescing of progress events using a timeout, as discussed in the RFC on Discourse [1]. This PR consists of two commits which, depending on the feedback, I may split up into two PRs. For now, I think it's easier to review this as a whole. 1. The first commit introduces a new generic `Alarm` class. The class lets you to schedule a function (callback) to be executed after a given timeout expires. You can cancel and reset a callback before its corresponding timeout expires. It achieves this with the help of a worker thread that sleeps until the next timeout expires. The only guarantee it provides is that your function is called no sooner than the requested timeout. Because the callback is called directly from the worker thread, a long running callback could potentially block the worker thread. I intentionally kept the implementation as simple as possible while addressing the needs for the `ProgressManager` use case. If we want to rely on this somewhere else, we can reassess whether we need to address those limitations. 2. The second commit uses the Alarm class to coalesce progress events. To recap the Discourse discussion, when multiple progress events with the same title execute in close succession, they get broadcast as one to `eBroadcastBitProgressCategory`. The `ProgressManager` keeps track of the in-flight progress events and when the refcount hits zero, the Alarm class is used to schedule broadcasting the event. If a new progress event comes in before the alarm fires, the alarm is reset (and the process repeats when the new progress event ends). If no new event comes in before the timeout expires, the progress event is broadcast. [1] https://discourse.llvm.org/t/rfc-improve-lldb-progress-reporting/75717/
1 parent 5e6e40f commit 156c290

File tree

4 files changed

+167
-35
lines changed

4 files changed

+167
-35
lines changed

lldb/include/lldb/Core/Progress.h

+31-4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#ifndef LLDB_CORE_PROGRESS_H
1010
#define LLDB_CORE_PROGRESS_H
1111

12+
#include "lldb/Host/Alarm.h"
1213
#include "lldb/lldb-forward.h"
1314
#include "lldb/lldb-types.h"
1415
#include "llvm/ADT/StringMap.h"
@@ -150,19 +151,45 @@ class ProgressManager {
150151
void Increment(const Progress::ProgressData &);
151152
void Decrement(const Progress::ProgressData &);
152153

154+
static void Initialize();
155+
static void Terminate();
156+
static bool Enabled();
153157
static ProgressManager &Instance();
154158

155-
private:
159+
protected:
156160
enum class EventType {
157161
Begin,
158162
End,
159163
};
160164
static void ReportProgress(const Progress::ProgressData &progress_data,
161165
EventType type);
162166

163-
llvm::StringMap<std::pair<uint64_t, Progress::ProgressData>>
164-
m_progress_category_map;
165-
std::mutex m_progress_map_mutex;
167+
static std::optional<ProgressManager> &InstanceImpl();
168+
169+
/// Helper function for reporting progress when the alarm in the corresponding
170+
/// entry in the map expires.
171+
void Expire(llvm::StringRef key);
172+
173+
/// Entry used for bookkeeping.
174+
struct Entry {
175+
/// Reference count used for overlapping events.
176+
uint64_t refcount = 0;
177+
178+
/// Data used to emit progress events.
179+
Progress::ProgressData data;
180+
181+
/// Alarm handle used when the refcount reaches zero.
182+
Alarm::Handle handle = Alarm::INVALID_HANDLE;
183+
};
184+
185+
/// Map used for bookkeeping.
186+
llvm::StringMap<Entry> m_entries;
187+
188+
/// Mutex to provide the map.
189+
std::mutex m_entries_mutex;
190+
191+
/// Alarm instance to coalesce progress events.
192+
Alarm m_alarm;
166193
};
167194

168195
} // namespace lldb_private

lldb/source/API/SystemInitializerFull.cpp

+11-3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "lldb/API/SBCommandInterpreter.h"
1111
#include "lldb/Core/Debugger.h"
1212
#include "lldb/Core/PluginManager.h"
13+
#include "lldb/Core/Progress.h"
1314
#include "lldb/Host/Config.h"
1415
#include "lldb/Host/Host.h"
1516
#include "lldb/Initialization/SystemInitializerCommon.h"
@@ -57,6 +58,7 @@ llvm::Error SystemInitializerFull::Initialize() {
5758
llvm::InitializeAllAsmPrinters();
5859
llvm::InitializeAllTargetMCs();
5960
llvm::InitializeAllDisassemblers();
61+
6062
// Initialize the command line parser in LLVM. This usually isn't necessary
6163
// as we aren't dealing with command line options here, but otherwise some
6264
// other code in Clang/LLVM might be tempted to call this function from a
@@ -65,10 +67,13 @@ llvm::Error SystemInitializerFull::Initialize() {
6567
const char *arg0 = "lldb";
6668
llvm::cl::ParseCommandLineOptions(1, &arg0);
6769

70+
// Initialize the progress manager.
71+
ProgressManager::Initialize();
72+
6873
#define LLDB_PLUGIN(p) LLDB_PLUGIN_INITIALIZE(p);
6974
#include "Plugins/Plugins.def"
7075

71-
// Scan for any system or user LLDB plug-ins
76+
// Scan for any system or user LLDB plug-ins.
7277
PluginManager::Initialize();
7378

7479
// The process settings need to know about installed plug-ins, so the
@@ -84,15 +89,18 @@ llvm::Error SystemInitializerFull::Initialize() {
8489
void SystemInitializerFull::Terminate() {
8590
Debugger::SettingsTerminate();
8691

87-
// Terminate plug-ins in core LLDB
92+
// Terminate plug-ins in core LLDB.
8893
ProcessTrace::Terminate();
8994

90-
// Terminate and unload and loaded system or user LLDB plug-ins
95+
// Terminate and unload and loaded system or user LLDB plug-ins.
9196
PluginManager::Terminate();
9297

9398
#define LLDB_PLUGIN(p) LLDB_PLUGIN_TERMINATE(p);
9499
#include "Plugins/Plugins.def"
95100

101+
// Terminate the progress manager.
102+
ProgressManager::Terminate();
103+
96104
// Now shutdown the common parts, in reverse order.
97105
SystemInitializerCommon::Terminate();
98106
}

lldb/source/Core/Progress.cpp

+88-26
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ Progress::Progress(std::string title, std::string details,
3535

3636
std::lock_guard<std::mutex> guard(m_mutex);
3737
ReportProgress();
38-
ProgressManager::Instance().Increment(m_progress_data);
38+
39+
// Report to the ProgressManager if that subsystem is enabled.
40+
if (ProgressManager::Enabled())
41+
ProgressManager::Instance().Increment(m_progress_data);
3942
}
4043

4144
Progress::~Progress() {
@@ -45,7 +48,10 @@ Progress::~Progress() {
4548
if (!m_completed)
4649
m_completed = m_total;
4750
ReportProgress();
48-
ProgressManager::Instance().Decrement(m_progress_data);
51+
52+
// Report to the ProgressManager if that subsystem is enabled.
53+
if (ProgressManager::Enabled())
54+
ProgressManager::Instance().Decrement(m_progress_data);
4955
}
5056

5157
void Progress::Increment(uint64_t amount,
@@ -75,45 +81,84 @@ void Progress::ReportProgress() {
7581
}
7682
}
7783

78-
ProgressManager::ProgressManager() : m_progress_category_map() {}
84+
ProgressManager::ProgressManager()
85+
: m_entries(), m_alarm(std::chrono::milliseconds(100)) {}
7986

8087
ProgressManager::~ProgressManager() {}
8188

89+
void ProgressManager::Initialize() {
90+
assert(!InstanceImpl() && "Already initialized.");
91+
InstanceImpl().emplace();
92+
}
93+
94+
void ProgressManager::Terminate() {
95+
assert(InstanceImpl() && "Already terminated.");
96+
InstanceImpl().reset();
97+
}
98+
99+
bool ProgressManager::Enabled() { return InstanceImpl().operator bool(); }
100+
82101
ProgressManager &ProgressManager::Instance() {
83-
static std::once_flag g_once_flag;
84-
static ProgressManager *g_progress_manager = nullptr;
85-
std::call_once(g_once_flag, []() {
86-
// NOTE: known leak to avoid global destructor chain issues.
87-
g_progress_manager = new ProgressManager();
88-
});
89-
return *g_progress_manager;
102+
assert(InstanceImpl() && "ProgressManager must be initialized");
103+
return *InstanceImpl();
104+
}
105+
106+
std::optional<ProgressManager> &ProgressManager::InstanceImpl() {
107+
static std::optional<ProgressManager> g_progress_manager;
108+
return g_progress_manager;
90109
}
91110

92111
void ProgressManager::Increment(const Progress::ProgressData &progress_data) {
93-
std::lock_guard<std::mutex> lock(m_progress_map_mutex);
94-
// If the current category exists in the map then it is not an initial report,
95-
// therefore don't broadcast to the category bit. Also, store the current
96-
// progress data in the map so that we have a note of the ID used for the
97-
// initial progress report.
98-
if (!m_progress_category_map.contains(progress_data.title)) {
99-
m_progress_category_map[progress_data.title].second = progress_data;
112+
std::lock_guard<std::mutex> lock(m_entries_mutex);
113+
114+
llvm::StringRef key = progress_data.title;
115+
bool new_entry = !m_entries.contains(key);
116+
Entry &entry = m_entries[progress_data.title];
117+
118+
if (new_entry) {
119+
// This is a new progress event. Report progress and store the progress
120+
// data.
100121
ReportProgress(progress_data, EventType::Begin);
122+
entry.data = progress_data;
123+
} else if (entry.refcount == 0) {
124+
// This is an existing entry that was scheduled to be deleted but a new one
125+
// came in before the timer expired.
126+
assert(entry.handle != Alarm::INVALID_HANDLE);
127+
128+
if (!m_alarm.Cancel(entry.handle)) {
129+
// The timer expired before we had a chance to cancel it. We have to treat
130+
// this as an entirely new progress event.
131+
ReportProgress(progress_data, EventType::Begin);
132+
}
133+
// Clear the alarm handle.
134+
entry.handle = Alarm::INVALID_HANDLE;
101135
}
102-
m_progress_category_map[progress_data.title].first++;
136+
137+
// Regardless of how we got here, we need to bump the reference count.
138+
entry.refcount++;
103139
}
104140

105141
void ProgressManager::Decrement(const Progress::ProgressData &progress_data) {
106-
std::lock_guard<std::mutex> lock(m_progress_map_mutex);
107-
auto pos = m_progress_category_map.find(progress_data.title);
142+
std::lock_guard<std::mutex> lock(m_entries_mutex);
143+
llvm::StringRef key = progress_data.title;
108144

109-
if (pos == m_progress_category_map.end())
145+
if (!m_entries.contains(key))
110146
return;
111147

112-
if (pos->second.first <= 1) {
113-
ReportProgress(pos->second.second, EventType::End);
114-
m_progress_category_map.erase(progress_data.title);
115-
} else {
116-
--pos->second.first;
148+
Entry &entry = m_entries[key];
149+
entry.refcount--;
150+
151+
if (entry.refcount == 0) {
152+
assert(entry.handle == Alarm::INVALID_HANDLE);
153+
154+
// Copy the key to a std::string so we can pass it by value to the lambda.
155+
// The underlying StringRef will not exist by the time the callback is
156+
// called.
157+
std::string key_str = std::string(key);
158+
159+
// Start a timer. If it expires before we see another progress event, it
160+
// will be reported.
161+
entry.handle = m_alarm.Create([=]() { Expire(key_str); });
117162
}
118163
}
119164

@@ -129,3 +174,20 @@ void ProgressManager::ReportProgress(
129174
progress_data.debugger_id,
130175
Debugger::eBroadcastBitProgressCategory);
131176
}
177+
178+
void ProgressManager::Expire(llvm::StringRef key) {
179+
std::lock_guard<std::mutex> lock(m_entries_mutex);
180+
181+
// This shouldn't happen but be resilient anyway.
182+
if (!m_entries.contains(key))
183+
return;
184+
185+
// A new event came in and the alarm fired before we had a chance to restart
186+
// it.
187+
if (m_entries[key].refcount != 0)
188+
return;
189+
190+
// We're done with this entry.
191+
ReportProgress(m_entries[key].data, EventType::End);
192+
m_entries.erase(key);
193+
}

lldb/unittests/Core/ProgressReportTest.cpp

+37-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
using namespace lldb;
2323
using namespace lldb_private;
2424

25-
static std::chrono::milliseconds TIMEOUT(100);
25+
static std::chrono::milliseconds TIMEOUT(500);
2626

2727
class ProgressReportTest : public ::testing::Test {
2828
public:
@@ -56,7 +56,8 @@ class ProgressReportTest : public ::testing::Test {
5656

5757
DebuggerSP m_debugger_sp;
5858
ListenerSP m_listener_sp;
59-
SubsystemRAII<FileSystem, HostInfo, PlatformMacOSX> subsystems;
59+
SubsystemRAII<FileSystem, HostInfo, PlatformMacOSX, ProgressManager>
60+
subsystems;
6061
};
6162

6263
TEST_F(ProgressReportTest, TestReportCreation) {
@@ -210,3 +211,37 @@ TEST_F(ProgressReportTest, TestOverlappingEvents) {
210211
// initial report.
211212
EXPECT_EQ(data->GetID(), expected_progress_id);
212213
}
214+
215+
TEST_F(ProgressReportTest, TestProgressManagerDisjointReports) {
216+
ListenerSP listener_sp =
217+
CreateListenerFor(Debugger::eBroadcastBitProgressCategory);
218+
EventSP event_sp;
219+
const ProgressEventData *data;
220+
uint64_t expected_progress_id;
221+
222+
{ Progress progress("Coalesced report 1", "Starting report 1"); }
223+
{ Progress progress("Coalesced report 1", "Starting report 2"); }
224+
{ Progress progress("Coalesced report 1", "Starting report 3"); }
225+
226+
ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
227+
data = ProgressEventData::GetEventDataFromEvent(event_sp.get());
228+
expected_progress_id = data->GetID();
229+
230+
EXPECT_EQ(data->GetDetails(), "");
231+
EXPECT_FALSE(data->IsFinite());
232+
EXPECT_FALSE(data->GetCompleted());
233+
EXPECT_EQ(data->GetTotal(), Progress::kNonDeterministicTotal);
234+
EXPECT_EQ(data->GetMessage(), "Coalesced report 1");
235+
236+
ASSERT_TRUE(listener_sp->GetEvent(event_sp, TIMEOUT));
237+
data = ProgressEventData::GetEventDataFromEvent(event_sp.get());
238+
239+
EXPECT_EQ(data->GetID(), expected_progress_id);
240+
EXPECT_EQ(data->GetDetails(), "");
241+
EXPECT_FALSE(data->IsFinite());
242+
EXPECT_TRUE(data->GetCompleted());
243+
EXPECT_EQ(data->GetTotal(), Progress::kNonDeterministicTotal);
244+
EXPECT_EQ(data->GetMessage(), "Coalesced report 1");
245+
246+
ASSERT_FALSE(listener_sp->GetEvent(event_sp, TIMEOUT));
247+
}

0 commit comments

Comments
 (0)