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
14using namespace lldb;
15using namespace lldb_private;
16
17Alarm::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
22Alarm::~Alarm() { StopAlarmThread(); }
23
24Alarm::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<std::mutex> alarm_guard(m_alarm_mutex);
38
39 // Create a new unique entry and remember its handle.
40 m_entries.emplace_back(args&: callback, args: 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
51bool 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<std::mutex> alarm_guard(m_alarm_mutex);
63
64 // Find the entry corresponding to the given handle.
65 const auto it =
66 std::find_if(first: m_entries.begin(), last: m_entries.end(),
67 pred: [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
82bool 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<std::mutex> alarm_guard(m_alarm_mutex);
90
91 const auto it =
92 std::find_if(first: m_entries.begin(), last: m_entries.end(),
93 pred: [handle](Entry &entry) { return entry.handle == handle; });
94
95 if (it == m_entries.end())
96 return false;
97
98 m_entries.erase(position: 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
108Alarm::Entry::Entry(Alarm::Callback callback, Alarm::TimePoint expiration)
109 : handle(Alarm::GetNextUniqueHandle()), callback(std::move(callback)),
110 expiration(std::move(expiration)) {}
111
112void Alarm::StartAlarmThread() {
113 if (!m_alarm_thread.IsJoinable()) {
114 llvm::Expected<HostThread> alarm_thread = ThreadLauncher::LaunchThread(
115 name: "lldb.debugger.alarm-thread", thread_function: [this] { return AlarmThread(); },
116 min_stack_byte_size: 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
126void Alarm::StopAlarmThread() {
127 if (m_alarm_thread.IsJoinable()) {
128 {
129 std::lock_guard<std::mutex> alarm_guard(m_alarm_mutex);
130 m_exit = true;
131 }
132 m_alarm_cv.notify_one();
133 m_alarm_thread.Join(result: nullptr);
134 }
135}
136
137bool Alarm::AlarmThreadRunning() { return m_alarm_thread.IsJoinable(); }
138
139lldb::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 llvm::SmallVector<Callback, 1> callbacks;
158 {
159 std::unique_lock<std::mutex> alarm_lock(m_alarm_mutex);
160 if (next_alarm) {
161 if (!m_alarm_cv.wait_until(lock&: alarm_lock, atime: *next_alarm, p: predicate)) {
162 // The timeout for the next alarm expired.
163
164 // Clear the next timeout to signal that we need to recompute the next
165 // timeout.
166 next_alarm.reset();
167
168 // Iterate over all the callbacks. Call the ones that have expired
169 // and remove them from the list.
170 const TimePoint now = std::chrono::system_clock::now();
171 auto it = m_entries.begin();
172 while (it != m_entries.end()) {
173 if (it->expiration <= now) {
174 callbacks.emplace_back(Args: std::move(it->callback));
175 it = m_entries.erase(position: it);
176 } else {
177 it++;
178 }
179 }
180 }
181 } else {
182 m_alarm_cv.wait(lock&: alarm_lock, p: predicate);
183 }
184
185 // Fall through after waiting on the condition variable. At this point
186 // either the predicate is true or we woke up because an alarm expired.
187
188 // The alarm thread is shutting down.
189 if (m_exit) {
190 exit = true;
191 if (m_run_callbacks_on_exit) {
192 for (Entry &entry : m_entries)
193 callbacks.emplace_back(Args: std::move(entry.callback));
194 }
195 }
196
197 // A new alarm was added or an alarm expired. Either way we need to
198 // recompute when this thread should wake up for the next alarm.
199 if (m_recompute_next_alarm || !next_alarm) {
200 for (Entry &entry : m_entries) {
201 if (!next_alarm || entry.expiration < *next_alarm)
202 next_alarm = entry.expiration;
203 }
204 m_recompute_next_alarm = false;
205 }
206 }
207
208 // Outside the lock, call the callbacks.
209 for (Callback &callback : callbacks)
210 callback();
211 }
212 return {};
213}
214
215Alarm::TimePoint Alarm::GetNextExpiration() const {
216 return std::chrono::system_clock::now() + m_timeout;
217}
218
219Alarm::Handle Alarm::GetNextUniqueHandle() {
220 static std::atomic<Handle> g_next_handle = 1;
221 return g_next_handle++;
222}
223

source code of lldb/source/Host/common/Alarm.cpp