Skip to content

[lldb] Add an Alarm class for coalescing progress reports #85329

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions lldb/include/lldb/Host/Alarm.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//===-- Alarm.h -------------------------------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#ifndef LLDB_HOST_ALARM_H
#define LLDB_HOST_ALARM_H

#include "lldb/Host/HostThread.h"
#include "lldb/lldb-types.h"
#include "llvm/Support/Chrono.h"

namespace lldb_private {

/// \class Alarm abstraction that enables scheduling a callback function after a
/// specified timeout. Creating an alarm for a callback returns a Handle that
/// can be used to restart or cancel the alarm.
class Alarm {
public:
using Handle = uint64_t;
using Callback = std::function<void()>;
using TimePoint = llvm::sys::TimePoint<>;
using Duration = std::chrono::milliseconds;

Alarm(Duration timeout, bool run_callback_on_exit = false);
~Alarm();

/// Create an alarm for the given callback. The alarm will expire and the
/// callback will be called after the timeout.
///
/// \returns
/// Handle which can be used to restart or cancel the alarm.
Handle Create(Callback callback);

/// Restart the alarm for the given Handle. The alarm will expire and the
/// callback will be called after the timeout.
///
/// \returns
/// True if the alarm was successfully restarted. False if there is no alarm
/// for the given Handle or the alarm already expired.
bool Restart(Handle handle);

/// Cancel the alarm for the given Handle. The alarm and its handle will be
/// removed.
///
/// \returns
/// True if the alarm was successfully canceled and the Handle removed.
/// False if there is no alarm for the given Handle or the alarm already
/// expired.
bool Cancel(Handle handle);

static constexpr Handle INVALID_HANDLE = 0;

private:
/// Helper functions to start, stop and check the status of the alarm thread.
/// @{
void StartAlarmThread();
void StopAlarmThread();
bool AlarmThreadRunning();
/// @}

/// Return an unique, monotonically increasing handle.
static Handle GetNextUniqueHandle();

/// Helper to compute the next time the alarm thread needs to wake up.
TimePoint GetNextExpiration() const;

/// Alarm entry.
struct Entry {
Handle handle;
Callback callback;
TimePoint expiration;

Entry(Callback callback, TimePoint expiration);
bool operator==(const Entry &rhs) { return handle == rhs.handle; }
};

/// List of alarm entries.
std::vector<Entry> m_entries;

/// Timeout between when an alarm is created and when it fires.
Duration m_timeout;

/// The alarm thread.
/// @{
HostThread m_alarm_thread;
lldb::thread_result_t AlarmThread();
/// @}

/// Synchronize access between the alarm thread and the main thread.
std::mutex m_alarm_mutex;

/// Condition variable used to wake up the alarm thread.
std::condition_variable m_alarm_cv;

/// Flag to signal the alarm thread that something changed and we need to
/// recompute the next alarm.
bool m_recompute_next_alarm = false;

/// Flag to signal the alarm thread to exit.
bool m_exit = false;

/// Flag to signal we should run all callbacks on exit.
bool m_run_callbacks_on_exit = false;
};

} // namespace lldb_private

#endif // LLDB_HOST_ALARM_H
1 change: 1 addition & 0 deletions lldb/source/Host/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ macro(add_host_subdirectory group)
endmacro()

add_host_subdirectory(common
common/Alarm.cpp
common/FileAction.cpp
common/FileCache.cpp
common/File.cpp
Expand Down
216 changes: 216 additions & 0 deletions lldb/source/Host/common/Alarm.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
//===-- Alarm.cpp ---------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "lldb/Host/Alarm.h"
#include "lldb/Host/ThreadLauncher.h"
#include "lldb/Utility/LLDBLog.h"
#include "lldb/Utility/Log.h"

using namespace lldb;
using namespace lldb_private;

Alarm::Alarm(Duration timeout, bool run_callback_on_exit)
: m_timeout(timeout), m_run_callbacks_on_exit(run_callback_on_exit) {
StartAlarmThread();
}

Alarm::~Alarm() { StopAlarmThread(); }

Alarm::Handle Alarm::Create(std::function<void()> callback) {
// Gracefully deal with the unlikely event that the alarm thread failed to
// launch.
if (!AlarmThreadRunning())
return INVALID_HANDLE;

// Compute the next expiration before we take the lock. This ensures that
// waiting on the lock doesn't eat into the timeout.
const TimePoint expiration = GetNextExpiration();

Handle handle = INVALID_HANDLE;

{
std::lock_guard alarm_guard(m_alarm_mutex);

// Create a new unique entry and remember its handle.
m_entries.emplace_back(callback, expiration);
handle = m_entries.back().handle;

// Tell the alarm thread we need to recompute the next alarm.
m_recompute_next_alarm = true;
}

m_alarm_cv.notify_one();
return handle;
}

bool Alarm::Restart(Handle handle) {
// Gracefully deal with the unlikely event that the alarm thread failed to
// launch.
if (!AlarmThreadRunning())
return false;

// Compute the next expiration before we take the lock. This ensures that
// waiting on the lock doesn't eat into the timeout.
const TimePoint expiration = GetNextExpiration();

{
std::lock_guard alarm_guard(m_alarm_mutex);

// Find the entry corresponding to the given handle.
const auto it =
std::find_if(m_entries.begin(), m_entries.end(),
[handle](Entry &entry) { return entry.handle == handle; });
if (it == m_entries.end())
return false;

// Update the expiration.
it->expiration = expiration;

// Tell the alarm thread we need to recompute the next alarm.
m_recompute_next_alarm = true;
}

m_alarm_cv.notify_one();
return true;
}

bool Alarm::Cancel(Handle handle) {
// Gracefully deal with the unlikely event that the alarm thread failed to
// launch.
if (!AlarmThreadRunning())
return false;

{
std::lock_guard alarm_guard(m_alarm_mutex);

const auto it =
std::find_if(m_entries.begin(), m_entries.end(),
[handle](Entry &entry) { return entry.handle == handle; });

if (it == m_entries.end())
return false;

m_entries.erase(it);
}

// No need to notify the alarm thread. This only affects the alarm thread if
// we removed the entry that corresponds to the next alarm. If that's the
// case, the thread will wake up as scheduled, find no expired events, and
// recompute the next alarm time.
return true;
}

Alarm::Entry::Entry(Alarm::Callback callback, Alarm::TimePoint expiration)
: handle(Alarm::GetNextUniqueHandle()), callback(std::move(callback)),
expiration(std::move(expiration)) {}

void Alarm::StartAlarmThread() {
if (!m_alarm_thread.IsJoinable()) {
llvm::Expected<HostThread> alarm_thread = ThreadLauncher::LaunchThread(
"lldb.debugger.alarm-thread", [this] { return AlarmThread(); },
8 * 1024 * 1024); // Use larger 8MB stack for this thread
if (alarm_thread) {
m_alarm_thread = *alarm_thread;
} else {
LLDB_LOG_ERROR(GetLog(LLDBLog::Host), alarm_thread.takeError(),
"failed to launch host thread: {0}");
}
}
}

void Alarm::StopAlarmThread() {
if (m_alarm_thread.IsJoinable()) {
{
std::lock_guard alarm_guard(m_alarm_mutex);
m_exit = true;
}
m_alarm_cv.notify_one();
m_alarm_thread.Join(nullptr);
}
}

bool Alarm::AlarmThreadRunning() { return m_alarm_thread.IsJoinable(); }

lldb::thread_result_t Alarm::AlarmThread() {
bool exit = false;
std::optional<TimePoint> next_alarm;

const auto predicate = [this] { return m_exit || m_recompute_next_alarm; };

while (!exit) {
// Synchronization between the main thread and the alarm thread using a
// mutex and condition variable. There are 2 reasons the thread can wake up:
//
// 1. The timeout for the next alarm expired.
//
// 2. The condition variable is notified that one of our shared variables
// (see predicate) was modified. Either the thread is asked to shut down
// or a new alarm came in and we need to recompute the next timeout.
//
// Below we only deal with the timeout expiring and fall through for dealing
// with the rest.
std::unique_lock alarm_lock(m_alarm_mutex);
if (next_alarm) {
if (!m_alarm_cv.wait_until(alarm_lock, *next_alarm, predicate)) {
// The timeout for the next alarm expired.

// Clear the next timeout to signal that we need to recompute the next
// timeout.
next_alarm.reset();

// Iterate over all the callbacks. Call the ones that have expired
// and remove them from the list.
const TimePoint now = std::chrono::system_clock::now();
auto it = m_entries.begin();
while (it != m_entries.end()) {
if (it->expiration <= now) {
it->callback();
it = m_entries.erase(it);
} else {
it++;
}
}
}
} else {
m_alarm_cv.wait(alarm_lock, predicate);
}

// Fall through after waiting on the condition variable. At this point
// either the predicate is true or we woke up because an alarm expired.

// The alarm thread is shutting down.
if (m_exit) {
exit = true;
if (m_run_callbacks_on_exit) {
for (Entry &entry : m_entries)
entry.callback();
}
continue;
}

// A new alarm was added or an alarm expired. Either way we need to
// recompute when this thread should wake up for the next alarm.
if (m_recompute_next_alarm || !next_alarm) {
for (Entry &entry : m_entries) {
if (!next_alarm || entry.expiration < *next_alarm)
next_alarm = entry.expiration;
}
m_recompute_next_alarm = false;
}
}
return {};
}

Alarm::TimePoint Alarm::GetNextExpiration() const {
return std::chrono::system_clock::now() + m_timeout;
}

Alarm::Handle Alarm::GetNextUniqueHandle() {
static std::atomic<Handle> g_next_handle = 1;
return g_next_handle++;
}
Loading