Skip to content

Commit f01a32f

Browse files
authored
[lldb] Add an Alarm class for coalescing progress reports (#85329)
The commit introduces a new, generic, Alarm class. The class lets you to schedule functions (callbacks) that will execute after a predefined timeout. Once scheduled, you can cancel and reset a callback, given the timeout hasn't expired yet. The alarm class worker thread that sleeps until the next timeout expires. When the thread wakes up, it checks for all the callbacks that have expired and calls them in order. Because the callback is called from the worker thread, the only guarantee is that a callback is called no sooner than the timeout. A long running callback could potentially block the worker threads and delay other callbacks from getting called. I intentionally kept the implementation as simple as possible while addressing the needs for the use case of coalescing progress events as discussed in [1]. If we want to rely on this somewhere else, we can reassess whether we need to address this class' limitations. [1] https://discourse.llvm.org/t/rfc-improve-lldb-progress-reporting/75717/
1 parent 1865655 commit f01a32f

File tree

5 files changed

+489
-0
lines changed

5 files changed

+489
-0
lines changed

lldb/include/lldb/Host/Alarm.h

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//===-- Alarm.h -------------------------------------------------*- 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+
#ifndef LLDB_HOST_ALARM_H
10+
#define LLDB_HOST_ALARM_H
11+
12+
#include "lldb/Host/HostThread.h"
13+
#include "lldb/lldb-types.h"
14+
#include "llvm/Support/Chrono.h"
15+
16+
namespace lldb_private {
17+
18+
/// \class Alarm abstraction that enables scheduling a callback function after a
19+
/// specified timeout. Creating an alarm for a callback returns a Handle that
20+
/// can be used to restart or cancel the alarm.
21+
class Alarm {
22+
public:
23+
using Handle = uint64_t;
24+
using Callback = std::function<void()>;
25+
using TimePoint = llvm::sys::TimePoint<>;
26+
using Duration = std::chrono::milliseconds;
27+
28+
Alarm(Duration timeout, bool run_callback_on_exit = false);
29+
~Alarm();
30+
31+
/// Create an alarm for the given callback. The alarm will expire and the
32+
/// callback will be called after the timeout.
33+
///
34+
/// \returns
35+
/// Handle which can be used to restart or cancel the alarm.
36+
Handle Create(Callback callback);
37+
38+
/// Restart the alarm for the given Handle. The alarm will expire and the
39+
/// callback will be called after the timeout.
40+
///
41+
/// \returns
42+
/// True if the alarm was successfully restarted. False if there is no alarm
43+
/// for the given Handle or the alarm already expired.
44+
bool Restart(Handle handle);
45+
46+
/// Cancel the alarm for the given Handle. The alarm and its handle will be
47+
/// removed.
48+
///
49+
/// \returns
50+
/// True if the alarm was successfully canceled and the Handle removed.
51+
/// False if there is no alarm for the given Handle or the alarm already
52+
/// expired.
53+
bool Cancel(Handle handle);
54+
55+
static constexpr Handle INVALID_HANDLE = 0;
56+
57+
private:
58+
/// Helper functions to start, stop and check the status of the alarm thread.
59+
/// @{
60+
void StartAlarmThread();
61+
void StopAlarmThread();
62+
bool AlarmThreadRunning();
63+
/// @}
64+
65+
/// Return an unique, monotonically increasing handle.
66+
static Handle GetNextUniqueHandle();
67+
68+
/// Helper to compute the next time the alarm thread needs to wake up.
69+
TimePoint GetNextExpiration() const;
70+
71+
/// Alarm entry.
72+
struct Entry {
73+
Handle handle;
74+
Callback callback;
75+
TimePoint expiration;
76+
77+
Entry(Callback callback, TimePoint expiration);
78+
bool operator==(const Entry &rhs) { return handle == rhs.handle; }
79+
};
80+
81+
/// List of alarm entries.
82+
std::vector<Entry> m_entries;
83+
84+
/// Timeout between when an alarm is created and when it fires.
85+
Duration m_timeout;
86+
87+
/// The alarm thread.
88+
/// @{
89+
HostThread m_alarm_thread;
90+
lldb::thread_result_t AlarmThread();
91+
/// @}
92+
93+
/// Synchronize access between the alarm thread and the main thread.
94+
std::mutex m_alarm_mutex;
95+
96+
/// Condition variable used to wake up the alarm thread.
97+
std::condition_variable m_alarm_cv;
98+
99+
/// Flag to signal the alarm thread that something changed and we need to
100+
/// recompute the next alarm.
101+
bool m_recompute_next_alarm = false;
102+
103+
/// Flag to signal the alarm thread to exit.
104+
bool m_exit = false;
105+
106+
/// Flag to signal we should run all callbacks on exit.
107+
bool m_run_callbacks_on_exit = false;
108+
};
109+
110+
} // namespace lldb_private
111+
112+
#endif // LLDB_HOST_ALARM_H

lldb/source/Host/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ macro(add_host_subdirectory group)
1313
endmacro()
1414

1515
add_host_subdirectory(common
16+
common/Alarm.cpp
1617
common/FileAction.cpp
1718
common/FileCache.cpp
1819
common/File.cpp

lldb/source/Host/common/Alarm.cpp

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
//===-- Alarm.cpp ---------------------------------------------------------===//
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 "lldb/Host/Alarm.h"
10+
#include "lldb/Host/ThreadLauncher.h"
11+
#include "lldb/Utility/LLDBLog.h"
12+
#include "lldb/Utility/Log.h"
13+
14+
using namespace lldb;
15+
using namespace lldb_private;
16+
17+
Alarm::Alarm(Duration timeout, bool run_callback_on_exit)
18+
: m_timeout(timeout), m_run_callbacks_on_exit(run_callback_on_exit) {
19+
StartAlarmThread();
20+
}
21+
22+
Alarm::~Alarm() { StopAlarmThread(); }
23+
24+
Alarm::Handle Alarm::Create(std::function<void()> callback) {
25+
// Gracefully deal with the unlikely event that the alarm thread failed to
26+
// launch.
27+
if (!AlarmThreadRunning())
28+
return INVALID_HANDLE;
29+
30+
// Compute the next expiration before we take the lock. This ensures that
31+
// waiting on the lock doesn't eat into the timeout.
32+
const TimePoint expiration = GetNextExpiration();
33+
34+
Handle handle = INVALID_HANDLE;
35+
36+
{
37+
std::lock_guard alarm_guard(m_alarm_mutex);
38+
39+
// Create a new unique entry and remember its handle.
40+
m_entries.emplace_back(callback, expiration);
41+
handle = m_entries.back().handle;
42+
43+
// Tell the alarm thread we need to recompute the next alarm.
44+
m_recompute_next_alarm = true;
45+
}
46+
47+
m_alarm_cv.notify_one();
48+
return handle;
49+
}
50+
51+
bool Alarm::Restart(Handle handle) {
52+
// Gracefully deal with the unlikely event that the alarm thread failed to
53+
// launch.
54+
if (!AlarmThreadRunning())
55+
return false;
56+
57+
// Compute the next expiration before we take the lock. This ensures that
58+
// waiting on the lock doesn't eat into the timeout.
59+
const TimePoint expiration = GetNextExpiration();
60+
61+
{
62+
std::lock_guard alarm_guard(m_alarm_mutex);
63+
64+
// Find the entry corresponding to the given handle.
65+
const auto it =
66+
std::find_if(m_entries.begin(), m_entries.end(),
67+
[handle](Entry &entry) { return entry.handle == handle; });
68+
if (it == m_entries.end())
69+
return false;
70+
71+
// Update the expiration.
72+
it->expiration = expiration;
73+
74+
// Tell the alarm thread we need to recompute the next alarm.
75+
m_recompute_next_alarm = true;
76+
}
77+
78+
m_alarm_cv.notify_one();
79+
return true;
80+
}
81+
82+
bool Alarm::Cancel(Handle handle) {
83+
// Gracefully deal with the unlikely event that the alarm thread failed to
84+
// launch.
85+
if (!AlarmThreadRunning())
86+
return false;
87+
88+
{
89+
std::lock_guard alarm_guard(m_alarm_mutex);
90+
91+
const auto it =
92+
std::find_if(m_entries.begin(), m_entries.end(),
93+
[handle](Entry &entry) { return entry.handle == handle; });
94+
95+
if (it == m_entries.end())
96+
return false;
97+
98+
m_entries.erase(it);
99+
}
100+
101+
// No need to notify the alarm thread. This only affects the alarm thread if
102+
// we removed the entry that corresponds to the next alarm. If that's the
103+
// case, the thread will wake up as scheduled, find no expired events, and
104+
// recompute the next alarm time.
105+
return true;
106+
}
107+
108+
Alarm::Entry::Entry(Alarm::Callback callback, Alarm::TimePoint expiration)
109+
: handle(Alarm::GetNextUniqueHandle()), callback(std::move(callback)),
110+
expiration(std::move(expiration)) {}
111+
112+
void Alarm::StartAlarmThread() {
113+
if (!m_alarm_thread.IsJoinable()) {
114+
llvm::Expected<HostThread> alarm_thread = ThreadLauncher::LaunchThread(
115+
"lldb.debugger.alarm-thread", [this] { return AlarmThread(); },
116+
8 * 1024 * 1024); // Use larger 8MB stack for this thread
117+
if (alarm_thread) {
118+
m_alarm_thread = *alarm_thread;
119+
} else {
120+
LLDB_LOG_ERROR(GetLog(LLDBLog::Host), alarm_thread.takeError(),
121+
"failed to launch host thread: {0}");
122+
}
123+
}
124+
}
125+
126+
void Alarm::StopAlarmThread() {
127+
if (m_alarm_thread.IsJoinable()) {
128+
{
129+
std::lock_guard alarm_guard(m_alarm_mutex);
130+
m_exit = true;
131+
}
132+
m_alarm_cv.notify_one();
133+
m_alarm_thread.Join(nullptr);
134+
}
135+
}
136+
137+
bool Alarm::AlarmThreadRunning() { return m_alarm_thread.IsJoinable(); }
138+
139+
lldb::thread_result_t Alarm::AlarmThread() {
140+
bool exit = false;
141+
std::optional<TimePoint> next_alarm;
142+
143+
const auto predicate = [this] { return m_exit || m_recompute_next_alarm; };
144+
145+
while (!exit) {
146+
// Synchronization between the main thread and the alarm thread using a
147+
// mutex and condition variable. There are 2 reasons the thread can wake up:
148+
//
149+
// 1. The timeout for the next alarm expired.
150+
//
151+
// 2. The condition variable is notified that one of our shared variables
152+
// (see predicate) was modified. Either the thread is asked to shut down
153+
// or a new alarm came in and we need to recompute the next timeout.
154+
//
155+
// Below we only deal with the timeout expiring and fall through for dealing
156+
// with the rest.
157+
std::unique_lock alarm_lock(m_alarm_mutex);
158+
if (next_alarm) {
159+
if (!m_alarm_cv.wait_until(alarm_lock, *next_alarm, predicate)) {
160+
// The timeout for the next alarm expired.
161+
162+
// Clear the next timeout to signal that we need to recompute the next
163+
// timeout.
164+
next_alarm.reset();
165+
166+
// Iterate over all the callbacks. Call the ones that have expired
167+
// and remove them from the list.
168+
const TimePoint now = std::chrono::system_clock::now();
169+
auto it = m_entries.begin();
170+
while (it != m_entries.end()) {
171+
if (it->expiration <= now) {
172+
it->callback();
173+
it = m_entries.erase(it);
174+
} else {
175+
it++;
176+
}
177+
}
178+
}
179+
} else {
180+
m_alarm_cv.wait(alarm_lock, predicate);
181+
}
182+
183+
// Fall through after waiting on the condition variable. At this point
184+
// either the predicate is true or we woke up because an alarm expired.
185+
186+
// The alarm thread is shutting down.
187+
if (m_exit) {
188+
exit = true;
189+
if (m_run_callbacks_on_exit) {
190+
for (Entry &entry : m_entries)
191+
entry.callback();
192+
}
193+
continue;
194+
}
195+
196+
// A new alarm was added or an alarm expired. Either way we need to
197+
// recompute when this thread should wake up for the next alarm.
198+
if (m_recompute_next_alarm || !next_alarm) {
199+
for (Entry &entry : m_entries) {
200+
if (!next_alarm || entry.expiration < *next_alarm)
201+
next_alarm = entry.expiration;
202+
}
203+
m_recompute_next_alarm = false;
204+
}
205+
}
206+
return {};
207+
}
208+
209+
Alarm::TimePoint Alarm::GetNextExpiration() const {
210+
return std::chrono::system_clock::now() + m_timeout;
211+
}
212+
213+
Alarm::Handle Alarm::GetNextUniqueHandle() {
214+
static std::atomic<Handle> g_next_handle = 1;
215+
return g_next_handle++;
216+
}

0 commit comments

Comments
 (0)