New ThreadPlanSingleThreadTimeout to resolve potential deadlock in single thread stepping (#90930)

This PR introduces a new `ThreadPlanSingleThreadTimeout` that will be
used to address potential deadlock during single-thread stepping.

While debugging a target with a non-trivial number of threads (around
5000 threads in one example target), we noticed that a simple step over
can take as long as 10 seconds. Enabling single-thread stepping mode
significantly reduces the stepping time to around 3 seconds. However,
this can introduce deadlock if we try to step over a method that depends
on other threads to release a lock.

To address this issue, we introduce a new
`ThreadPlanSingleThreadTimeout` that can be controlled by the
`target.process.thread.single-thread-plan-timeout` setting during
single-thread stepping mode. The concept involves counting the elapsed
time since the last internal stop to detect overall stepping progress.
Once a timeout occurs, we assume the target is not making progress due
to a potential deadlock, as mentioned above. We then send a new async
interrupt, resume all threads, and `ThreadPlanSingleThreadTimeout`
completes its task.

To support this design, the major changes made in this PR are:
1. `ThreadPlanSingleThreadTimeout` is popped during every internal stop
and reset (re-pushed) to the top of the stack (as a leaf node) during
resume. This is achieved by always returning `true` from
`ThreadPlanSingleThreadTimeout::DoPlanExplainsStop()` and
`ThreadPlanSingleThreadTimeout::MischiefManaged()`.
2. A new thread-specific async interrupt stop is introduced, which can
be detected/consumed by `ThreadPlanSingleThreadTimeout`.
3. The clearing of branch breakpoints in the range thread plan has been
moved from `DoPlanExplainsStop()` to `ShouldStop()`, as it is not
guaranteed that it will be called.

The detailed design is discussed in the RFC below:

[https://discourse.llvm.org/t/improve-single-thread-stepping/74599](https://discourse.llvm.org/t/improve-single-thread-stepping/74599)

---------

Co-authored-by: jeffreytan81 <jeffreytan@fb.com>
This commit is contained in:
jeffreytan81 2024-08-05 17:26:39 -07:00 committed by GitHub
parent 84cc1865ef
commit f838fa820f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 974 additions and 53 deletions

View File

@ -1314,10 +1314,16 @@ public:
size_t GetThreadStatus(Stream &ostrm, bool only_threads_with_stop_reason,
uint32_t start_frame, uint32_t num_frames,
uint32_t num_frames_with_source,
bool stop_format);
uint32_t num_frames_with_source, bool stop_format);
void SendAsyncInterrupt();
/// Send an async interrupt request.
///
/// If \a thread is specified the async interrupt stop will be attributed to
/// the specified thread.
///
/// \param[in] thread
/// The thread the async interrupt will be attributed to.
void SendAsyncInterrupt(Thread *thread = nullptr);
// Notify this process class that modules got loaded.
//
@ -2867,6 +2873,17 @@ protected:
return std::nullopt;
}
/// Handle thread specific async interrupt and return the original thread
/// that requested the async interrupt. It can be null if original thread
/// has exited.
///
/// \param[in] description
/// Returns the stop reason description of the async interrupt.
virtual lldb::ThreadSP
HandleThreadAsyncInterrupt(uint8_t signo, const std::string &description) {
return lldb::ThreadSP();
}
lldb::StateType GetPrivateState();
/// The "private" side of resuming a process. This doesn't alter the state
@ -3153,6 +3170,11 @@ protected:
// Resume will only request a resume, using this
// flag to check.
lldb::tid_t m_interrupt_tid; /// The tid of the thread that issued the async
/// interrupt, used by thread plan timeout. It
/// can be LLDB_INVALID_THREAD_ID to indicate
/// user level async interrupt.
/// This is set at the beginning of Process::Finalize() to stop functions
/// from looking up or creating things during or after a finalize call.
std::atomic<bool> m_finalizing;

View File

@ -123,6 +123,10 @@ public:
const char *description = nullptr,
std::optional<int> code = std::nullopt);
static lldb::StopInfoSP
CreateStopReasonWithInterrupt(Thread &thread, int signo,
const char *description);
static lldb::StopInfoSP CreateStopReasonToTrace(Thread &thread);
static lldb::StopInfoSP

View File

@ -58,6 +58,8 @@ public:
bool GetStepOutAvoidsNoDebug() const;
uint64_t GetMaxBacktraceDepth() const;
uint64_t GetSingleThreadPlanTimeout() const;
};
class Thread : public std::enable_shared_from_this<Thread>,

View File

@ -302,7 +302,8 @@ public:
eKindStepInRange,
eKindRunToAddress,
eKindStepThrough,
eKindStepUntil
eKindStepUntil,
eKindSingleThreadTimeout,
};
virtual ~ThreadPlan();
@ -395,6 +396,11 @@ public:
bool IsControllingPlan() { return m_is_controlling_plan; }
// Returns true if this plan is a leaf plan, meaning the plan will be popped
// during each stop if it does not explain the stop and re-pushed before
// resuming to stay at the top of the stack.
virtual bool IsLeafPlan() { return false; }
bool SetIsControllingPlan(bool value) {
bool old_value = m_is_controlling_plan;
m_is_controlling_plan = value;
@ -483,6 +489,8 @@ public:
return m_takes_iteration_count;
}
virtual lldb::StateType GetPlanRunState() = 0;
protected:
// Constructors and Destructors
ThreadPlan(ThreadPlanKind kind, const char *name, Thread &thread,
@ -522,8 +530,6 @@ protected:
GetThread().SetStopInfo(stop_reason_sp);
}
virtual lldb::StateType GetPlanRunState() = 0;
bool IsUsuallyUnexplainedStopReason(lldb::StopReason);
Status m_status;

View File

@ -0,0 +1,107 @@
//===-- ThreadPlanSingleThreadTimeout.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_TARGET_THREADPLANSINGLETHREADTIMEOUT_H
#define LLDB_TARGET_THREADPLANSINGLETHREADTIMEOUT_H
#include "lldb/Target/Thread.h"
#include "lldb/Target/ThreadPlan.h"
#include "lldb/Utility/Event.h"
#include "lldb/Utility/LLDBLog.h"
#include "lldb/Utility/State.h"
#include <chrono>
#include <thread>
namespace lldb_private {
//
// Thread plan used by single thread execution to issue timeout. This is useful
// to detect potential deadlock in single thread execution. The timeout measures
// the elapsed time from the last internal stop and gets reset by each internal
// stop to ensure we are accurately detecting execution not moving forward.
// This means this thread plan may be created/destroyed multiple times by the
// parent execution plan.
//
// When a timeout happens, the thread plan resolves the potential deadlock by
// issuing a thread specific async interrupt to enter stop state, then execution
// is resumed with all threads running to resolve the potential deadlock
//
class ThreadPlanSingleThreadTimeout : public ThreadPlan {
enum class State {
WaitTimeout, // Waiting for timeout.
AsyncInterrupt, // Async interrupt has been issued.
Done, // Finished resume all threads.
};
public:
// TODO: allow timeout to be set on per thread plan basis.
struct TimeoutInfo {
// Whether there is a ThreadPlanSingleThreadTimeout instance alive.
bool m_isAlive = false;
ThreadPlanSingleThreadTimeout::State m_last_state = State::WaitTimeout;
};
~ThreadPlanSingleThreadTimeout() override;
// If input \param thread is running in single thread mode, push a
// new ThreadPlanSingleThreadTimeout based on timeout setting from fresh new
// state. The reference of \param info is passed in so that when
// ThreadPlanSingleThreadTimeout got popped its last state can be stored
// in it for future resume.
static void PushNewWithTimeout(Thread &thread, TimeoutInfo &info);
// Push a new ThreadPlanSingleThreadTimeout by restoring state from
// input \param info and resume execution.
static void ResumeFromPrevState(Thread &thread, TimeoutInfo &info);
void GetDescription(Stream *s, lldb::DescriptionLevel level) override;
bool ValidatePlan(Stream *error) override { return true; }
bool WillStop() override;
void DidPop() override;
bool IsLeafPlan() override { return true; }
bool DoPlanExplainsStop(Event *event_ptr) override;
lldb::StateType GetPlanRunState() override;
static void TimeoutThreadFunc(ThreadPlanSingleThreadTimeout *self);
bool MischiefManaged() override;
bool ShouldStop(Event *event_ptr) override;
void SetStopOthers(bool new_value) override;
bool StopOthers() override;
private:
ThreadPlanSingleThreadTimeout(Thread &thread, TimeoutInfo &info);
bool IsTimeoutAsyncInterrupt(Event *event_ptr);
bool HandleEvent(Event *event_ptr);
void HandleTimeout();
uint64_t GetRemainingTimeoutMilliSeconds();
static std::string StateToString(State state);
ThreadPlanSingleThreadTimeout(const ThreadPlanSingleThreadTimeout &) = delete;
const ThreadPlanSingleThreadTimeout &
operator=(const ThreadPlanSingleThreadTimeout &) = delete;
TimeoutInfo &m_info; // Reference to controlling ThreadPlan's TimeoutInfo.
State m_state;
// Lock for m_wakeup_cv and m_exit_flag between thread plan thread and timer
// thread
std::mutex m_mutex;
std::condition_variable m_wakeup_cv;
std::thread m_timer_thread;
std::chrono::steady_clock::time_point m_timeout_start;
};
} // namespace lldb_private
#endif // LLDB_TARGET_THREADPLANSINGLETHREADTIMEOUT_H

View File

@ -30,6 +30,7 @@ public:
bool ValidatePlan(Stream *error) override;
bool ShouldStop(Event *event_ptr) override;
bool StopOthers() override;
void SetStopOthers(bool new_value) override { m_stop_others = new_value; }
lldb::StateType GetPlanRunState() override;
bool WillStop() override;
bool MischiefManaged() override;

View File

@ -13,11 +13,13 @@
#include "lldb/Target/StackID.h"
#include "lldb/Target/Thread.h"
#include "lldb/Target/ThreadPlanStepRange.h"
#include "lldb/Target/TimeoutResumeAll.h"
namespace lldb_private {
class ThreadPlanStepOverRange : public ThreadPlanStepRange,
ThreadPlanShouldStopHere {
ThreadPlanShouldStopHere,
TimeoutResumeAll {
public:
ThreadPlanStepOverRange(Thread &thread, const AddressRange &range,
const SymbolContext &addr_context,
@ -27,7 +29,9 @@ public:
~ThreadPlanStepOverRange() override;
void GetDescription(Stream *s, lldb::DescriptionLevel level) override;
void SetStopOthers(bool new_value) override;
bool ShouldStop(Event *event_ptr) override;
void DidPush() override;
protected:
bool DoPlanExplainsStop(Event *event_ptr) override;
@ -44,6 +48,7 @@ private:
bool IsEquivalentContext(const SymbolContext &context);
bool m_first_resume;
lldb::RunMode m_run_mode;
ThreadPlanStepOverRange(const ThreadPlanStepOverRange &) = delete;
const ThreadPlanStepOverRange &

View File

@ -58,8 +58,15 @@ protected:
// run' plan, then just single step.
bool SetNextBranchBreakpoint();
// Whether the input stop info is caused by the next branch breakpoint.
// Note: this does not check if branch breakpoint site is shared by other
// breakpoints or not.
bool IsNextBranchBreakpointStop(lldb::StopInfoSP stop_info_sp);
void ClearNextBranchBreakpoint();
void ClearNextBranchBreakpointExplainedStop();
bool NextRangeBreakpointExplainsStop(lldb::StopInfoSP stop_info_sp);
SymbolContext m_addr_context;

View File

@ -0,0 +1,40 @@
//===-- TimeoutResumeAll.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_TARGET_TIMEOUTRESUMEALL_H
#define LLDB_TARGET_TIMEOUTRESUMEALL_H
#include "lldb/Target/ThreadPlanSingleThreadTimeout.h"
namespace lldb_private {
// Mixin class that provides the capability for ThreadPlan to support single
// thread execution that resumes all threads after a timeout.
// Opt-in thread plan should call PushNewTimeout() in its DidPush() and
// ResumeWithTimeout() during DoWillResume().
class TimeoutResumeAll {
public:
TimeoutResumeAll(Thread &thread) : m_thread(thread) {}
void PushNewTimeout() {
ThreadPlanSingleThreadTimeout::PushNewWithTimeout(m_thread, m_timeout_info);
}
void ResumeWithTimeout() {
ThreadPlanSingleThreadTimeout::ResumeFromPrevState(m_thread,
m_timeout_info);
}
private:
Thread &m_thread;
ThreadPlanSingleThreadTimeout::TimeoutInfo m_timeout_info;
};
} // namespace lldb_private
#endif // LLDB_TARGET_TIMEOUTRESUMEALL_H

View File

@ -253,6 +253,7 @@ enum StopReason {
eStopReasonFork,
eStopReasonVFork,
eStopReasonVForkDone,
eStopReasonInterrupt, ///< Thread requested interrupt
};
/// Command Return Status Types.

View File

@ -192,6 +192,9 @@ size_t SBThread::GetStopReasonDataCount() {
case eStopReasonSignal:
return 1;
case eStopReasonInterrupt:
return 1;
case eStopReasonException:
return 1;
@ -261,6 +264,9 @@ uint64_t SBThread::GetStopReasonDataAtIndex(uint32_t idx) {
case eStopReasonSignal:
return stop_info_sp->GetValue();
case eStopReasonInterrupt:
return stop_info_sp->GetValue();
case eStopReasonException:
return stop_info_sp->GetValue();

View File

@ -2513,7 +2513,7 @@ bool CommandInterpreter::DidProcessStopAbnormally() const {
const StopReason reason = stop_info->GetStopReason();
if (reason == eStopReasonException ||
reason == eStopReasonInstrumentation ||
reason == eStopReasonProcessorTrace)
reason == eStopReasonProcessorTrace || reason == eStopReasonInterrupt)
return true;
if (reason == eStopReasonSignal) {

View File

@ -714,6 +714,8 @@ static const char *GetStopReasonString(StopReason stop_reason) {
return "vfork";
case eStopReasonVForkDone:
return "vforkdone";
case eStopReasonInterrupt:
return "async interrupt";
case eStopReasonInstrumentation:
case eStopReasonInvalid:
case eStopReasonPlanComplete:

View File

@ -1730,14 +1730,24 @@ ThreadSP ProcessGDBRemote::SetThreadStopInfo(
thread_sp = memory_thread_sp;
if (exc_type != 0) {
const size_t exc_data_size = exc_data.size();
thread_sp->SetStopInfo(
StopInfoMachException::CreateStopReasonWithMachException(
*thread_sp, exc_type, exc_data_size,
exc_data_size >= 1 ? exc_data[0] : 0,
exc_data_size >= 2 ? exc_data[1] : 0,
exc_data_size >= 3 ? exc_data[2] : 0));
// For thread plan async interrupt, creating stop info on the
// original async interrupt request thread instead. If interrupt thread
// does not exist anymore we fallback to current signal receiving thread
// instead.
ThreadSP interrupt_thread;
if (m_interrupt_tid != LLDB_INVALID_THREAD_ID)
interrupt_thread = HandleThreadAsyncInterrupt(signo, description);
if (interrupt_thread)
thread_sp = interrupt_thread;
else {
const size_t exc_data_size = exc_data.size();
thread_sp->SetStopInfo(
StopInfoMachException::CreateStopReasonWithMachException(
*thread_sp, exc_type, exc_data_size,
exc_data_size >= 1 ? exc_data[0] : 0,
exc_data_size >= 2 ? exc_data[1] : 0,
exc_data_size >= 3 ? exc_data[2] : 0));
}
} else {
bool handled = false;
bool did_exec = false;
@ -1936,9 +1946,20 @@ ThreadSP ProcessGDBRemote::SetThreadStopInfo(
*thread_sp, signo, description.c_str()));
}
}
if (!handled)
thread_sp->SetStopInfo(StopInfo::CreateStopReasonWithSignal(
*thread_sp, signo, description.c_str()));
if (!handled) {
// For thread plan async interrupt, creating stop info on the
// original async interrupt request thread instead. If interrupt
// thread does not exist anymore we fallback to current signal
// receiving thread instead.
ThreadSP interrupt_thread;
if (m_interrupt_tid != LLDB_INVALID_THREAD_ID)
interrupt_thread = HandleThreadAsyncInterrupt(signo, description);
if (interrupt_thread)
thread_sp = interrupt_thread;
else
thread_sp->SetStopInfo(StopInfo::CreateStopReasonWithSignal(
*thread_sp, signo, description.c_str()));
}
}
if (!description.empty()) {
@ -1957,6 +1978,24 @@ ThreadSP ProcessGDBRemote::SetThreadStopInfo(
return thread_sp;
}
ThreadSP
ProcessGDBRemote::HandleThreadAsyncInterrupt(uint8_t signo,
const std::string &description) {
ThreadSP thread_sp;
{
std::lock_guard<std::recursive_mutex> guard(m_thread_list_real.GetMutex());
thread_sp = m_thread_list_real.FindThreadByProtocolID(m_interrupt_tid,
/*can_update=*/false);
}
if (thread_sp)
thread_sp->SetStopInfo(StopInfo::CreateStopReasonWithInterrupt(
*thread_sp, signo, description.c_str()));
// Clear m_interrupt_tid regardless we can find original interrupt thread or
// not.
m_interrupt_tid = LLDB_INVALID_THREAD_ID;
return thread_sp;
}
lldb::ThreadSP
ProcessGDBRemote::SetThreadStopInfo(StructuredData::Dictionary *thread_dict) {
static constexpr llvm::StringLiteral g_key_tid("tid");

View File

@ -440,6 +440,10 @@ private:
void HandleStopReply() override;
void HandleAsyncStructuredDataPacket(llvm::StringRef data) override;
lldb::ThreadSP
HandleThreadAsyncInterrupt(uint8_t signo,
const std::string &description) override;
void SetThreadPc(const lldb::ThreadSP &thread_sp, uint64_t index);
using ModuleCacheKey = std::pair<std::string, std::string>;
// KeyInfo for the cached module spec DenseMap.

View File

@ -59,6 +59,7 @@ add_lldb_library(lldbTarget
ThreadPlanCallOnFunctionExit.cpp
ThreadPlanCallUserExpression.cpp
ThreadPlanRunToAddress.cpp
ThreadPlanSingleThreadTimeout.cpp
ThreadPlanShouldStopHere.cpp
ThreadPlanStepInRange.cpp
ThreadPlanStepInstruction.cpp

View File

@ -473,7 +473,8 @@ Process::Process(lldb::TargetSP target_sp, ListenerSP listener_sp,
m_memory_cache(*this), m_allocated_memory_cache(*this),
m_should_detach(false), m_next_event_action_up(), m_public_run_lock(),
m_private_run_lock(), m_currently_handling_do_on_removals(false),
m_resume_requested(false), m_finalizing(false), m_destructing(false),
m_resume_requested(false), m_interrupt_tid(LLDB_INVALID_THREAD_ID),
m_finalizing(false), m_destructing(false),
m_clear_thread_plans_on_stop(false), m_force_next_event_delivery(false),
m_last_broadcast_state(eStateInvalid), m_destroy_in_process(false),
m_can_interpret_function_calls(false), m_run_thread_plan_lock(),
@ -895,6 +896,7 @@ bool Process::HandleProcessStateChangedEvent(
case eStopReasonThreadExiting:
case eStopReasonInstrumentation:
case eStopReasonProcessorTrace:
case eStopReasonInterrupt:
if (!other_thread)
other_thread = thread;
break;
@ -3873,7 +3875,11 @@ void Process::ControlPrivateStateThread(uint32_t signal) {
}
}
void Process::SendAsyncInterrupt() {
void Process::SendAsyncInterrupt(Thread *thread) {
if (thread != nullptr)
m_interrupt_tid = thread->GetProtocolID();
else
m_interrupt_tid = LLDB_INVALID_THREAD_ID;
if (PrivateStateThreadIsValid())
m_private_state_broadcaster.BroadcastEvent(Process::eBroadcastBitInterrupt,
nullptr);
@ -4099,9 +4105,14 @@ thread_result_t Process::RunPrivateStateThread(bool is_secondary_thread) {
if (interrupt_requested) {
if (StateIsStoppedState(internal_state, true)) {
// We requested the interrupt, so mark this as such in the stop event
// so clients can tell an interrupted process from a natural stop
ProcessEventData::SetInterruptedInEvent(event_sp.get(), true);
// Only mark interrupt event if it is not thread specific async
// interrupt.
if (m_interrupt_tid == LLDB_INVALID_THREAD_ID) {
// We requested the interrupt, so mark this as such in the stop
// event so clients can tell an interrupted process from a natural
// stop
ProcessEventData::SetInterruptedInEvent(event_sp.get(), true);
}
interrupt_requested = false;
} else if (log) {
LLDB_LOGF(log,

View File

@ -1125,6 +1125,29 @@ private:
std::optional<int> m_code;
};
// StopInfoInterrupt
class StopInfoInterrupt : public StopInfo {
public:
StopInfoInterrupt(Thread &thread, int signo, const char *description)
: StopInfo(thread, signo) {
SetDescription(description);
}
~StopInfoInterrupt() override = default;
StopReason GetStopReason() const override {
return lldb::eStopReasonInterrupt;
}
const char *GetDescription() override {
if (m_description.empty()) {
m_description = "async interrupt";
}
return m_description.c_str();
}
};
// StopInfoTrace
class StopInfoTrace : public StopInfo {
@ -1390,6 +1413,11 @@ StopInfoSP StopInfo::CreateStopReasonWithSignal(Thread &thread, int signo,
return StopInfoSP(new StopInfoUnixSignal(thread, signo, description, code));
}
StopInfoSP StopInfo::CreateStopReasonWithInterrupt(Thread &thread, int signo,
const char *description) {
return StopInfoSP(new StopInfoInterrupt(thread, signo, description));
}
StopInfoSP StopInfo::CreateStopReasonToTrace(Thread &thread) {
return StopInfoSP(new StopInfoTrace(thread));
}

View File

@ -313,6 +313,10 @@ let Definition = "thread" in {
def MaxBacktraceDepth: Property<"max-backtrace-depth", "UInt64">,
DefaultUnsignedValue<600000>,
Desc<"Maximum number of frames to backtrace.">;
def SingleThreadPlanTimeout: Property<"single-thread-plan-timeout", "UInt64">,
Global,
DefaultUnsignedValue<1000>,
Desc<"The time in milliseconds to wait for single thread ThreadPlan to move forward before resuming all threads to resolve any potential deadlock. Specify value 0 to disable timeout.">;
}
let Definition = "language" in {

View File

@ -143,6 +143,12 @@ uint64_t ThreadProperties::GetMaxBacktraceDepth() const {
idx, g_thread_properties[idx].default_uint_value);
}
uint64_t ThreadProperties::GetSingleThreadPlanTimeout() const {
const uint32_t idx = ePropertySingleThreadPlanTimeout;
return GetPropertyAtIndexAs<uint64_t>(
idx, g_thread_properties[idx].default_uint_value);
}
// Thread Event Data
llvm::StringRef Thread::ThreadEventData::GetFlavorString() {
@ -813,12 +819,17 @@ bool Thread::ShouldStop(Event *event_ptr) {
// decide whether they still need to do more work.
bool done_processing_current_plan = false;
if (!current_plan->PlanExplainsStop(event_ptr)) {
if (current_plan->TracerExplainsStop()) {
done_processing_current_plan = true;
should_stop = false;
} else {
// Leaf plan that does not explain the stop should be popped.
// The plan should be push itself later again before resuming to stay
// as leaf.
if (current_plan->IsLeafPlan())
PopPlan();
// If the current plan doesn't explain the stop, then find one that does
// and let it handle the situation.
ThreadPlan *plan_ptr = current_plan;
@ -1715,6 +1726,8 @@ std::string Thread::StopReasonAsString(lldb::StopReason reason) {
return "instrumentation break";
case eStopReasonProcessorTrace:
return "processor trace";
case eStopReasonInterrupt:
return "async interrupt";
}
return "StopReason = " + std::to_string(reason);

View File

@ -174,6 +174,7 @@ bool ThreadPlan::IsUsuallyUnexplainedStopReason(lldb::StopReason reason) {
case eStopReasonFork:
case eStopReasonVFork:
case eStopReasonVForkDone:
case eStopReasonInterrupt:
return true;
default:
return false;

View File

@ -0,0 +1,250 @@
//===-- ThreadPlanStepOverRange.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/Target/ThreadPlanSingleThreadTimeout.h"
#include "lldb/Symbol/Block.h"
#include "lldb/Symbol/CompileUnit.h"
#include "lldb/Symbol/Function.h"
#include "lldb/Symbol/LineTable.h"
#include "lldb/Target/Process.h"
#include "lldb/Target/RegisterContext.h"
#include "lldb/Target/Target.h"
#include "lldb/Target/Thread.h"
#include "lldb/Target/ThreadPlanStepOut.h"
#include "lldb/Target/ThreadPlanStepThrough.h"
#include "lldb/Utility/LLDBLog.h"
#include "lldb/Utility/Log.h"
#include "lldb/Utility/Stream.h"
using namespace lldb_private;
using namespace lldb;
ThreadPlanSingleThreadTimeout::ThreadPlanSingleThreadTimeout(Thread &thread,
TimeoutInfo &info)
: ThreadPlan(ThreadPlan::eKindSingleThreadTimeout, "Single thread timeout",
thread, eVoteNo, eVoteNoOpinion),
m_info(info), m_state(State::WaitTimeout) {
// TODO: reuse m_timer_thread without recreation.
m_timer_thread = std::thread(TimeoutThreadFunc, this);
m_info.m_isAlive = true;
m_state = m_info.m_last_state;
}
ThreadPlanSingleThreadTimeout::~ThreadPlanSingleThreadTimeout() {
m_info.m_isAlive = false;
}
uint64_t ThreadPlanSingleThreadTimeout::GetRemainingTimeoutMilliSeconds() {
uint64_t timeout_in_ms = GetThread().GetSingleThreadPlanTimeout();
std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
std::chrono::milliseconds duration_ms =
std::chrono::duration_cast<std::chrono::milliseconds>(now -
m_timeout_start);
return timeout_in_ms - duration_ms.count();
}
void ThreadPlanSingleThreadTimeout::GetDescription(
Stream *s, lldb::DescriptionLevel level) {
s->Printf("Single thread timeout, state(%s), remaining %" PRIu64 " ms",
StateToString(m_state).c_str(), GetRemainingTimeoutMilliSeconds());
}
std::string ThreadPlanSingleThreadTimeout::StateToString(State state) {
switch (state) {
case State::WaitTimeout:
return "WaitTimeout";
case State::AsyncInterrupt:
return "AsyncInterrupt";
case State::Done:
return "Done";
}
}
void ThreadPlanSingleThreadTimeout::PushNewWithTimeout(Thread &thread,
TimeoutInfo &info) {
uint64_t timeout_in_ms = thread.GetSingleThreadPlanTimeout();
if (timeout_in_ms == 0)
return;
// Do not create timeout if we are not stopping other threads.
if (!thread.GetCurrentPlan()->StopOthers())
return;
auto timeout_plan = new ThreadPlanSingleThreadTimeout(thread, info);
ThreadPlanSP thread_plan_sp(timeout_plan);
auto status = thread.QueueThreadPlan(thread_plan_sp,
/*abort_other_plans*/ false);
Log *log = GetLog(LLDBLog::Step);
LLDB_LOGF(
log,
"ThreadPlanSingleThreadTimeout pushing a brand new one with %" PRIu64
" ms",
timeout_in_ms);
}
void ThreadPlanSingleThreadTimeout::ResumeFromPrevState(Thread &thread,
TimeoutInfo &info) {
uint64_t timeout_in_ms = thread.GetSingleThreadPlanTimeout();
if (timeout_in_ms == 0)
return;
// There is already an instance alive.
if (info.m_isAlive)
return;
// Do not create timeout if we are not stopping other threads.
if (!thread.GetCurrentPlan()->StopOthers())
return;
auto timeout_plan = new ThreadPlanSingleThreadTimeout(thread, info);
ThreadPlanSP thread_plan_sp(timeout_plan);
auto status = thread.QueueThreadPlan(thread_plan_sp,
/*abort_other_plans*/ false);
Log *log = GetLog(LLDBLog::Step);
LLDB_LOGF(
log,
"ThreadPlanSingleThreadTimeout reset from previous state with %" PRIu64
" ms",
timeout_in_ms);
}
bool ThreadPlanSingleThreadTimeout::WillStop() {
Log *log = GetLog(LLDBLog::Step);
LLDB_LOGF(log, "ThreadPlanSingleThreadTimeout::WillStop().");
// Reset the state during stop.
m_info.m_last_state = State::WaitTimeout;
return true;
}
void ThreadPlanSingleThreadTimeout::DidPop() {
Log *log = GetLog(LLDBLog::Step);
{
std::lock_guard<std::mutex> lock(m_mutex);
LLDB_LOGF(log, "ThreadPlanSingleThreadTimeout::DidPop().");
// Tell timer thread to exit.
m_info.m_isAlive = false;
}
m_wakeup_cv.notify_one();
// Wait for timer thread to exit.
m_timer_thread.join();
}
bool ThreadPlanSingleThreadTimeout::DoPlanExplainsStop(Event *event_ptr) {
bool is_timeout_interrupt = IsTimeoutAsyncInterrupt(event_ptr);
Log *log = GetLog(LLDBLog::Step);
LLDB_LOGF(log,
"ThreadPlanSingleThreadTimeout::DoPlanExplainsStop() returns %d. "
"%" PRIu64 " ms remaining.",
is_timeout_interrupt, GetRemainingTimeoutMilliSeconds());
return is_timeout_interrupt;
}
lldb::StateType ThreadPlanSingleThreadTimeout::GetPlanRunState() {
return GetPreviousPlan()->GetPlanRunState();
}
void ThreadPlanSingleThreadTimeout::TimeoutThreadFunc(
ThreadPlanSingleThreadTimeout *self) {
std::unique_lock<std::mutex> lock(self->m_mutex);
uint64_t timeout_in_ms = self->GetThread().GetSingleThreadPlanTimeout();
// The thread should wakeup either when timeout or
// ThreadPlanSingleThreadTimeout has been popped (not alive).
Log *log = GetLog(LLDBLog::Step);
self->m_timeout_start = std::chrono::steady_clock::now();
LLDB_LOGF(
log,
"ThreadPlanSingleThreadTimeout::TimeoutThreadFunc(), wait for %" PRIu64
" ms",
timeout_in_ms);
self->m_wakeup_cv.wait_for(lock, std::chrono::milliseconds(timeout_in_ms),
[self] { return !self->m_info.m_isAlive; });
LLDB_LOGF(log,
"ThreadPlanSingleThreadTimeout::TimeoutThreadFunc() wake up with "
"m_isAlive(%d).",
self->m_info.m_isAlive);
if (!self->m_info.m_isAlive)
return;
self->HandleTimeout();
}
bool ThreadPlanSingleThreadTimeout::MischiefManaged() {
Log *log = GetLog(LLDBLog::Step);
LLDB_LOGF(log, "ThreadPlanSingleThreadTimeout::MischiefManaged() called.");
// Need to reset timer on each internal stop/execution progress.
return true;
}
bool ThreadPlanSingleThreadTimeout::ShouldStop(Event *event_ptr) {
return HandleEvent(event_ptr);
}
void ThreadPlanSingleThreadTimeout::SetStopOthers(bool new_value) {
// Note: this assumes that the SingleThreadTimeout plan is always going to be
// pushed on behalf of the plan directly above it.
GetPreviousPlan()->SetStopOthers(new_value);
}
bool ThreadPlanSingleThreadTimeout::StopOthers() {
if (m_state == State::Done)
return false;
else
return GetPreviousPlan()->StopOthers();
}
bool ThreadPlanSingleThreadTimeout::IsTimeoutAsyncInterrupt(Event *event_ptr) {
lldb::StateType stop_state =
Process::ProcessEventData::GetStateFromEvent(event_ptr);
Log *log = GetLog(LLDBLog::Step);
LLDB_LOGF(log,
"ThreadPlanSingleThreadTimeout::IsTimeoutAsyncInterrupt(): got "
"event: %s.",
StateAsCString(stop_state));
lldb::StopInfoSP stop_info = GetThread().GetStopInfo();
return (m_state == State::AsyncInterrupt &&
stop_state == lldb::eStateStopped && stop_info &&
stop_info->GetStopReason() == lldb::eStopReasonInterrupt);
}
bool ThreadPlanSingleThreadTimeout::HandleEvent(Event *event_ptr) {
if (IsTimeoutAsyncInterrupt(event_ptr)) {
Log *log = GetLog(LLDBLog::Step);
if (Process::ProcessEventData::GetRestartedFromEvent(event_ptr)) {
// If we were restarted, we just need to go back up to fetch
// another event.
LLDB_LOGF(log,
"ThreadPlanSingleThreadTimeout::HandleEvent(): Got a stop and "
"restart, so we'll continue waiting.");
} else {
LLDB_LOGF(
log,
"ThreadPlanSingleThreadTimeout::HandleEvent(): Got async interrupt "
", so we will resume all threads.");
GetThread().GetCurrentPlan()->SetStopOthers(false);
GetPreviousPlan()->SetStopOthers(false);
m_state = State::Done;
}
}
// Should not report stop.
return false;
}
void ThreadPlanSingleThreadTimeout::HandleTimeout() {
Log *log = GetLog(LLDBLog::Step);
LLDB_LOGF(
log,
"ThreadPlanSingleThreadTimeout::HandleTimeout() send async interrupt.");
m_state = State::AsyncInterrupt;
// Private state thread will only send async interrupt
// in running state so no need to check state here.
m_process.SendAsyncInterrupt(&GetThread());
}

View File

@ -134,6 +134,7 @@ bool ThreadPlanStepInRange::ShouldStop(Event *event_ptr) {
GetTarget().GetArchitecture().GetAddressByteSize());
LLDB_LOGF(log, "ThreadPlanStepInRange reached %s.", s.GetData());
}
ClearNextBranchBreakpointExplainedStop();
if (IsPlanComplete())
return true;

View File

@ -15,6 +15,7 @@
#include "lldb/Target/RegisterContext.h"
#include "lldb/Target/Target.h"
#include "lldb/Target/Thread.h"
#include "lldb/Target/ThreadPlanSingleThreadTimeout.h"
#include "lldb/Target/ThreadPlanStepOut.h"
#include "lldb/Target/ThreadPlanStepThrough.h"
#include "lldb/Utility/LLDBLog.h"
@ -36,7 +37,8 @@ ThreadPlanStepOverRange::ThreadPlanStepOverRange(
: ThreadPlanStepRange(ThreadPlan::eKindStepOverRange,
"Step range stepping over", thread, range,
addr_context, stop_others),
ThreadPlanShouldStopHere(this), m_first_resume(true) {
ThreadPlanShouldStopHere(this), TimeoutResumeAll(thread),
m_first_resume(true), m_run_mode(stop_others) {
SetFlagsToDefault();
SetupAvoidNoDebug(step_out_avoids_code_without_debug_info);
}
@ -124,6 +126,11 @@ bool ThreadPlanStepOverRange::IsEquivalentContext(
return m_addr_context.symbol && m_addr_context.symbol == context.symbol;
}
void ThreadPlanStepOverRange::SetStopOthers(bool stop_others) {
if (!stop_others)
m_stop_others = RunMode::eAllThreads;
}
bool ThreadPlanStepOverRange::ShouldStop(Event *event_ptr) {
Log *log = GetLog(LLDBLog::Step);
Thread &thread = GetThread();
@ -134,6 +141,7 @@ bool ThreadPlanStepOverRange::ShouldStop(Event *event_ptr) {
GetTarget().GetArchitecture().GetAddressByteSize());
LLDB_LOGF(log, "ThreadPlanStepOverRange reached %s.", s.GetData());
}
ClearNextBranchBreakpointExplainedStop();
// If we're out of the range but in the same frame or in our caller's frame
// then we should stop. When stepping out we only stop others if we are
@ -141,6 +149,8 @@ bool ThreadPlanStepOverRange::ShouldStop(Event *event_ptr) {
bool stop_others = (m_stop_others == lldb::eOnlyThisThread);
ThreadPlanSP new_plan_sp;
FrameComparison frame_order = CompareCurrentFrameToStartFrame();
LLDB_LOGF(log, "ThreadPlanStepOverRange compare frame result: %d.",
frame_order);
if (frame_order == eFrameCompareOlder) {
// If we're in an older frame then we should stop.
@ -337,6 +347,12 @@ bool ThreadPlanStepOverRange::ShouldStop(Event *event_ptr) {
return false;
}
void ThreadPlanStepOverRange::DidPush() {
ThreadPlanStepRange::DidPush();
if (m_run_mode == lldb::eOnlyThisThread && IsControllingPlan())
PushNewTimeout();
}
bool ThreadPlanStepOverRange::DoPlanExplainsStop(Event *event_ptr) {
// For crashes, breakpoint hits, signals, etc, let the base plan (or some
// plan above us) handle the stop. That way the user can see the stop, step
@ -414,6 +430,7 @@ bool ThreadPlanStepOverRange::DoWillResume(lldb::StateType resume_state,
}
}
}
if (m_run_mode == lldb::eOnlyThisThread && IsControllingPlan())
ResumeWithTimeout();
return true;
}

View File

@ -293,6 +293,20 @@ InstructionList *ThreadPlanStepRange::GetInstructionsForAddress(
return nullptr;
}
bool ThreadPlanStepRange::IsNextBranchBreakpointStop(StopInfoSP stop_info_sp) {
if (!m_next_branch_bp_sp)
return false;
break_id_t bp_site_id = stop_info_sp->GetValue();
BreakpointSiteSP bp_site_sp =
m_process.GetBreakpointSiteList().FindByID(bp_site_id);
if (!bp_site_sp)
return false;
else if (!bp_site_sp->IsBreakpointAtThisSite(m_next_branch_bp_sp->GetID()))
return false;
return true;
}
void ThreadPlanStepRange::ClearNextBranchBreakpoint() {
if (m_next_branch_bp_sp) {
Log *log = GetLog(LLDBLog::Step);
@ -305,6 +319,11 @@ void ThreadPlanStepRange::ClearNextBranchBreakpoint() {
}
}
void ThreadPlanStepRange::ClearNextBranchBreakpointExplainedStop() {
if (IsNextBranchBreakpointStop(GetPrivateStopInfo()))
ClearNextBranchBreakpoint();
}
bool ThreadPlanStepRange::SetNextBranchBreakpoint() {
if (m_next_branch_bp_sp)
return true;
@ -347,7 +366,9 @@ bool ThreadPlanStepRange::SetNextBranchBreakpoint() {
run_to_address =
instructions->GetInstructionAtIndex(branch_index)->GetAddress();
}
if (branch_index == pc_index)
LLDB_LOGF(log, "ThreadPlanStepRange::SetNextBranchBreakpoint - skipping "
"because current is branch instruction");
if (run_to_address.IsValid()) {
const bool is_internal = true;
m_next_branch_bp_sp =
@ -381,15 +402,16 @@ bool ThreadPlanStepRange::SetNextBranchBreakpoint() {
return true;
} else
return false;
}
} else
LLDB_LOGF(log, "ThreadPlanStepRange::SetNextBranchBreakpoint - skipping "
"invalid run_to_address");
}
return false;
}
bool ThreadPlanStepRange::NextRangeBreakpointExplainsStop(
lldb::StopInfoSP stop_info_sp) {
Log *log = GetLog(LLDBLog::Step);
if (!m_next_branch_bp_sp)
if (!IsNextBranchBreakpointStop(stop_info_sp))
return false;
break_id_t bp_site_id = stop_info_sp->GetValue();
@ -397,30 +419,27 @@ bool ThreadPlanStepRange::NextRangeBreakpointExplainsStop(
m_process.GetBreakpointSiteList().FindByID(bp_site_id);
if (!bp_site_sp)
return false;
else if (!bp_site_sp->IsBreakpointAtThisSite(m_next_branch_bp_sp->GetID()))
return false;
else {
// If we've hit the next branch breakpoint, then clear it.
size_t num_constituents = bp_site_sp->GetNumberOfConstituents();
bool explains_stop = true;
// If all the constituents are internal, then we are probably just stepping
// over this range from multiple threads, or multiple frames, so we want to
// continue. If one is not internal, then we should not explain the stop,
// and let the user breakpoint handle the stop.
for (size_t i = 0; i < num_constituents; i++) {
if (!bp_site_sp->GetConstituentAtIndex(i)->GetBreakpoint().IsInternal()) {
explains_stop = false;
break;
}
// If we've hit the next branch breakpoint, then clear it.
size_t num_constituents = bp_site_sp->GetNumberOfConstituents();
bool explains_stop = true;
// If all the constituents are internal, then we are probably just stepping
// over this range from multiple threads, or multiple frames, so we want to
// continue. If one is not internal, then we should not explain the stop,
// and let the user breakpoint handle the stop.
for (size_t i = 0; i < num_constituents; i++) {
if (!bp_site_sp->GetConstituentAtIndex(i)->GetBreakpoint().IsInternal()) {
explains_stop = false;
break;
}
LLDB_LOGF(log,
"ThreadPlanStepRange::NextRangeBreakpointExplainsStop - Hit "
"next range breakpoint which has %" PRIu64
" constituents - explains stop: %u.",
(uint64_t)num_constituents, explains_stop);
ClearNextBranchBreakpoint();
return explains_stop;
}
Log *log = GetLog(LLDBLog::Step);
LLDB_LOGF(log,
"ThreadPlanStepRange::NextRangeBreakpointExplainsStop - Hit "
"next range breakpoint which has %" PRIu64
" constituents - explains stop: %u.",
(uint64_t)num_constituents, explains_stop);
return explains_stop;
}
bool ThreadPlanStepRange::WillStop() { return true; }

View File

@ -0,0 +1,4 @@
ENABLE_THREADS := YES
CXX_SOURCES := main.cpp
include Makefile.rules

View File

@ -0,0 +1,254 @@
"""
Test that single thread step over deadlock issue can be resolved
after timeout.
"""
import lldb
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil
class SingleThreadStepTimeoutTestCase(TestBase):
NO_DEBUG_INFO_TESTCASE = True
def setUp(self):
TestBase.setUp(self)
self.main_source = "main.cpp"
self.build()
def verify_hit_correct_line(self, pattern):
target_line = line_number(self.main_source, pattern)
self.assertNotEqual(target_line, 0, "Could not find source pattern " + pattern)
cur_line = self.thread.frames[0].GetLineEntry().GetLine()
self.assertEqual(
cur_line,
target_line,
"Stepped to line %d instead of expected %d with pattern '%s'."
% (cur_line, target_line, pattern),
)
def step_over_deadlock_helper(self):
(target, _, self.thread, _) = lldbutil.run_to_source_breakpoint(
self, "// Set breakpoint1 here", lldb.SBFileSpec(self.main_source)
)
signal_main_thread_value = target.FindFirstGlobalVariable("signal_main_thread")
self.assertTrue(signal_main_thread_value.IsValid())
# Change signal_main_thread global variable to 1 so that worker thread loop can
# terminate and move forward to signal main thread
signal_main_thread_value.SetValueFromCString("1")
self.thread.StepOver(lldb.eOnlyThisThread)
self.verify_hit_correct_line("// Finish step-over from breakpoint1")
@skipIfWindows
def test_step_over_deadlock_small_timeout_fast_stepping(self):
"""Test single thread step over deadlock on other threads can be resolved after timeout with small timeout and fast stepping."""
self.dbg.HandleCommand(
"settings set target.process.thread.single-thread-plan-timeout 10"
)
self.dbg.HandleCommand("settings set target.use-fast-stepping true")
self.step_over_deadlock_helper()
@skipIfWindows
def test_step_over_deadlock_small_timeout_slow_stepping(self):
"""Test single thread step over deadlock on other threads can be resolved after timeout with small timeout and slow stepping."""
self.dbg.HandleCommand(
"settings set target.process.thread.single-thread-plan-timeout 10"
)
self.dbg.HandleCommand("settings set target.use-fast-stepping false")
self.step_over_deadlock_helper()
@skipIfWindows
def test_step_over_deadlock_large_timeout_fast_stepping(self):
"""Test single thread step over deadlock on other threads can be resolved after timeout with large timeout and fast stepping."""
self.dbg.HandleCommand(
"settings set target.process.thread.single-thread-plan-timeout 2000"
)
self.dbg.HandleCommand("settings set target.use-fast-stepping true")
self.step_over_deadlock_helper()
@skipIfWindows
def test_step_over_deadlock_large_timeout_slow_stepping(self):
"""Test single thread step over deadlock on other threads can be resolved after timeout with large timeout and slow stepping."""
self.dbg.HandleCommand(
"settings set target.process.thread.single-thread-plan-timeout 2000"
)
self.dbg.HandleCommand("settings set target.use-fast-stepping false")
self.step_over_deadlock_helper()
def step_over_multi_calls_helper(self):
(target, _, self.thread, _) = lldbutil.run_to_source_breakpoint(
self, "// Set breakpoint2 here", lldb.SBFileSpec(self.main_source)
)
self.thread.StepOver(lldb.eOnlyThisThread)
self.verify_hit_correct_line("// Finish step-over from breakpoint2")
@skipIfWindows
def test_step_over_multi_calls_small_timeout_fast_stepping(self):
"""Test step over source line with multiple call instructions works fine with small timeout and fast stepping."""
self.dbg.HandleCommand(
"settings set target.process.thread.single-thread-plan-timeout 10"
)
self.dbg.HandleCommand("settings set target.use-fast-stepping true")
self.step_over_multi_calls_helper()
@skipIfWindows
def test_step_over_multi_calls_small_timeout_slow_stepping(self):
"""Test step over source line with multiple call instructions works fine with small timeout and slow stepping."""
self.dbg.HandleCommand(
"settings set target.process.thread.single-thread-plan-timeout 10"
)
self.dbg.HandleCommand("settings set target.use-fast-stepping false")
self.step_over_multi_calls_helper()
@skipIfWindows
def test_step_over_multi_calls_large_timeout_fast_stepping(self):
"""Test step over source line with multiple call instructions works fine with large timeout and fast stepping."""
self.dbg.HandleCommand(
"settings set target.process.thread.single-thread-plan-timeout 2000"
)
self.dbg.HandleCommand("settings set target.use-fast-stepping true")
self.step_over_multi_calls_helper()
@skipIfWindows
def test_step_over_multi_calls_large_timeout_slow_stepping(self):
"""Test step over source line with multiple call instructions works fine with large timeout and slow stepping."""
self.dbg.HandleCommand(
"settings set target.process.thread.single-thread-plan-timeout 2000"
)
self.dbg.HandleCommand("settings set target.use-fast-stepping false")
self.step_over_multi_calls_helper()
@skipIfWindows
def test_step_over_deadlock_with_inner_breakpoint_continue(self):
"""Test step over deadlock function with inner breakpoint will trigger the breakpoint
and later continue will finish the stepping.
"""
self.dbg.HandleCommand(
"settings set target.process.thread.single-thread-plan-timeout 2000"
)
(target, process, self.thread, _) = lldbutil.run_to_source_breakpoint(
self, "// Set breakpoint1 here", lldb.SBFileSpec(self.main_source)
)
signal_main_thread_value = target.FindFirstGlobalVariable("signal_main_thread")
self.assertTrue(signal_main_thread_value.IsValid())
# Change signal_main_thread global variable to 1 so that worker thread loop can
# terminate and move forward to signal main thread
signal_main_thread_value.SetValueFromCString("1")
# Set breakpoint on inner function call
inner_breakpoint = target.BreakpointCreateByLocation(
lldb.SBFileSpec(self.main_source),
line_number("main.cpp", "// Set interrupt breakpoint here"),
0,
0,
lldb.SBFileSpecList(),
False,
)
# Step over will hit the inner breakpoint and stop
self.thread.StepOver(lldb.eOnlyThisThread)
self.assertStopReason(self.thread.GetStopReason(), lldb.eStopReasonBreakpoint)
thread1 = lldbutil.get_one_thread_stopped_at_breakpoint(
process, inner_breakpoint
)
self.assertTrue(
thread1.IsValid(),
"We are indeed stopped at inner breakpoint inside deadlock_func",
)
# Continue the process should complete the step-over
process.Continue()
self.assertState(process.GetState(), lldb.eStateStopped)
self.assertStopReason(self.thread.GetStopReason(), lldb.eStopReasonPlanComplete)
self.verify_hit_correct_line("// Finish step-over from breakpoint1")
@skipIfWindows
def test_step_over_deadlock_with_inner_breakpoint_step(self):
"""Test step over deadlock function with inner breakpoint will trigger the breakpoint
and later step still works
"""
self.dbg.HandleCommand(
"settings set target.process.thread.single-thread-plan-timeout 2000"
)
(target, process, self.thread, _) = lldbutil.run_to_source_breakpoint(
self, "// Set breakpoint1 here", lldb.SBFileSpec(self.main_source)
)
signal_main_thread_value = target.FindFirstGlobalVariable("signal_main_thread")
self.assertTrue(signal_main_thread_value.IsValid())
# Change signal_main_thread global variable to 1 so that worker thread loop can
# terminate and move forward to signal main thread
signal_main_thread_value.SetValueFromCString("1")
# Set breakpoint on inner function call
inner_breakpoint = target.BreakpointCreateByLocation(
lldb.SBFileSpec(self.main_source),
line_number("main.cpp", "// Set interrupt breakpoint here"),
0,
0,
lldb.SBFileSpecList(),
False,
)
# Step over will hit the inner breakpoint and stop
self.thread.StepOver(lldb.eOnlyThisThread)
self.assertStopReason(self.thread.GetStopReason(), lldb.eStopReasonBreakpoint)
thread1 = lldbutil.get_one_thread_stopped_at_breakpoint(
process, inner_breakpoint
)
self.assertTrue(
thread1.IsValid(),
"We are indeed stopped at inner breakpoint inside deadlock_func",
)
# Step still works
self.thread.StepOver(lldb.eOnlyThisThread)
self.assertState(process.GetState(), lldb.eStateStopped)
self.assertStopReason(self.thread.GetStopReason(), lldb.eStopReasonPlanComplete)
self.verify_hit_correct_line("// Finish step-over from inner breakpoint")
@skipIfWindows
def test_step_over_deadlock_with_user_async_interrupt(self):
"""Test step over deadlock function with large timeout then send async interrupt
should report correct stop reason
"""
self.dbg.HandleCommand(
"settings set target.process.thread.single-thread-plan-timeout 2000000"
)
(target, process, self.thread, _) = lldbutil.run_to_source_breakpoint(
self, "// Set breakpoint1 here", lldb.SBFileSpec(self.main_source)
)
signal_main_thread_value = target.FindFirstGlobalVariable("signal_main_thread")
self.assertTrue(signal_main_thread_value.IsValid())
# Change signal_main_thread global variable to 1 so that worker thread loop can
# terminate and move forward to signal main thread
signal_main_thread_value.SetValueFromCString("1")
self.dbg.SetAsync(True)
# This stepping should block due to large timeout and should be interrupted by the
# async interrupt from the worker thread
self.thread.StepOver(lldb.eOnlyThisThread)
time.sleep(1)
listener = self.dbg.GetListener()
lldbutil.expect_state_changes(self, listener, process, [lldb.eStateRunning])
self.dbg.SetAsync(False)
process.SendAsyncInterrupt()
lldbutil.expect_state_changes(self, listener, process, [lldb.eStateStopped])
self.assertStopReason(self.thread.GetStopReason(), lldb.eStopReasonSignal)

View File

@ -0,0 +1,68 @@
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
std::mutex mtx;
std::condition_variable cv;
int ready_thread_id = 0;
int signal_main_thread = 0;
void worker(int id) {
std::cout << "Worker " << id << " executing..." << std::endl;
// lldb test should change signal_main_thread to true to break the loop.
while (!signal_main_thread) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
// Signal the main thread to continue main thread
{
std::lock_guard<std::mutex> lock(mtx);
ready_thread_id = id; // break worker thread here
}
cv.notify_one();
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Worker " << id << " finished." << std::endl;
}
void deadlock_func(std::unique_lock<std::mutex> &lock) {
int i = 10;
++i; // Set interrupt breakpoint here
printf("%d", i); // Finish step-over from inner breakpoint
auto func = [] { return ready_thread_id == 1; };
cv.wait(lock, func);
}
int simulate_thread() {
std::thread t1(worker, 1);
std::unique_lock<std::mutex> lock(mtx);
deadlock_func(lock); // Set breakpoint1 here
std::thread t2(worker, 2); // Finish step-over from breakpoint1
cv.wait(lock, [] { return ready_thread_id == 2; });
t1.join();
t2.join();
std::cout << "Main thread continues..." << std::endl;
return 0;
}
int bar() { return 54; }
int foo(const std::string p1, int extra) { return p1.size() + extra; }
int main(int argc, char *argv[]) {
std::string ss = "this is a string for testing",
ls = "this is a long string for testing";
foo(ss.size() % 2 == 0 ? ss : ls, bar()); // Set breakpoint2 here
simulate_thread(); // Finish step-over from breakpoint2
return 0;
}

View File

@ -923,6 +923,9 @@ llvm::json::Value CreateThreadStopped(lldb::SBThread &thread,
case lldb::eStopReasonVForkDone:
body.try_emplace("reason", "vforkdone");
break;
case lldb::eStopReasonInterrupt:
body.try_emplace("reason", "async interrupt");
break;
case lldb::eStopReasonThreadExiting:
case lldb::eStopReasonInvalid:
case lldb::eStopReasonNone:

View File

@ -110,6 +110,7 @@ bool ThreadHasStopReason(lldb::SBThread &thread) {
case lldb::eStopReasonFork:
case lldb::eStopReasonVFork:
case lldb::eStopReasonVForkDone:
case lldb::eStopReasonInterrupt:
return true;
case lldb::eStopReasonThreadExiting:
case lldb::eStopReasonInvalid: