mirror of
https://github.com/llvm/llvm-project.git
synced 2025-04-27 05:56:08 +00:00
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:
parent
84cc1865ef
commit
f838fa820f
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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>,
|
||||
|
@ -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;
|
||||
|
107
lldb/include/lldb/Target/ThreadPlanSingleThreadTimeout.h
Normal file
107
lldb/include/lldb/Target/ThreadPlanSingleThreadTimeout.h
Normal 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
|
@ -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;
|
||||
|
@ -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 &
|
||||
|
@ -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;
|
||||
|
40
lldb/include/lldb/Target/TimeoutResumeAll.h
Normal file
40
lldb/include/lldb/Target/TimeoutResumeAll.h
Normal 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
|
@ -253,6 +253,7 @@ enum StopReason {
|
||||
eStopReasonFork,
|
||||
eStopReasonVFork,
|
||||
eStopReasonVForkDone,
|
||||
eStopReasonInterrupt, ///< Thread requested interrupt
|
||||
};
|
||||
|
||||
/// Command Return Status Types.
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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:
|
||||
|
@ -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");
|
||||
|
@ -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.
|
||||
|
@ -59,6 +59,7 @@ add_lldb_library(lldbTarget
|
||||
ThreadPlanCallOnFunctionExit.cpp
|
||||
ThreadPlanCallUserExpression.cpp
|
||||
ThreadPlanRunToAddress.cpp
|
||||
ThreadPlanSingleThreadTimeout.cpp
|
||||
ThreadPlanShouldStopHere.cpp
|
||||
ThreadPlanStepInRange.cpp
|
||||
ThreadPlanStepInstruction.cpp
|
||||
|
@ -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,
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -174,6 +174,7 @@ bool ThreadPlan::IsUsuallyUnexplainedStopReason(lldb::StopReason reason) {
|
||||
case eStopReasonFork:
|
||||
case eStopReasonVFork:
|
||||
case eStopReasonVForkDone:
|
||||
case eStopReasonInterrupt:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
250
lldb/source/Target/ThreadPlanSingleThreadTimeout.cpp
Normal file
250
lldb/source/Target/ThreadPlanSingleThreadTimeout.cpp
Normal 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());
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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; }
|
||||
|
@ -0,0 +1,4 @@
|
||||
ENABLE_THREADS := YES
|
||||
CXX_SOURCES := main.cpp
|
||||
|
||||
include Makefile.rules
|
@ -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)
|
68
lldb/test/API/functionalities/single-thread-step/main.cpp
Normal file
68
lldb/test/API/functionalities/single-thread-step/main.cpp
Normal 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;
|
||||
}
|
@ -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:
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user