Skip to content

[lldb] Introduce backtracing of Swift Tasks #9787

New issue

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

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

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions lldb/source/Plugins/Language/Swift/SwiftFormatters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "Plugins/Language/Swift/SwiftStringIndex.h"
#include "Plugins/LanguageRuntime/Swift/ReflectionContextInterface.h"
#include "Plugins/LanguageRuntime/Swift/SwiftLanguageRuntime.h"
#include "Plugins/LanguageRuntime/Swift/SwiftTask.h"
#include "Plugins/TypeSystem/Clang/TypeSystemClang.h"
#include "lldb/DataFormatters/FormattersHelpers.h"
#include "lldb/DataFormatters/StringPrinter.h"
Expand All @@ -23,6 +24,7 @@
#include "lldb/Utility/LLDBLog.h"
#include "lldb/Utility/Log.h"
#include "lldb/Utility/Status.h"
#include "lldb/Utility/StreamString.h"
#include "lldb/Utility/Timer.h"
#include "lldb/ValueObject/ValueObject.h"
#include "lldb/lldb-enumerations.h"
Expand Down Expand Up @@ -818,6 +820,16 @@ class TaskSyntheticFrontEnd : public SyntheticChildrenFrontEnd {
"could not get info for async task {0:x}: {1}", task_ptr,
fmt_consume(std::move(err)));
} else {

// Print a backtrace of the Task to stdout.
ExecutionContext exe_ctx{m_backend.GetExecutionContextRef()};
auto tt = std::make_shared<ThreadTask>(
3000, task_info->resumeAsyncContext, exe_ctx);
StreamString ss;
tt->GetStatus(ss, 0, 100, 0, false, true);
auto desc = ss.GetString();
printf("%.*s\n", (int)desc.size(), desc.data());
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is the reason this PR is in draft state. This code prints the backtrace of a task to stdout, but that will be migrated to a separate command. Initially something like language swift task backtrace.


m_task_info = *task_info;
for (auto child :
{m_is_child_task_sp, m_is_future_sp, m_is_group_child_task_sp,
Expand Down
1 change: 1 addition & 0 deletions lldb/source/Plugins/LanguageRuntime/Swift/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ add_lldb_library(lldbPluginSwiftLanguageRuntime PLUGIN
SwiftLanguageRuntimeNames.cpp
SwiftLanguageRuntimeRemoteAST.cpp
SwiftMetadataCache.cpp
SwiftTask.cpp

LINK_LIBS
swiftAST
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ class TargetReflectionContext : public ReflectionContextInterface {
result.hasIsRunning = task_info.HasIsRunning;
result.isRunning = task_info.IsRunning;
result.isEnqueued = task_info.IsEnqueued;
result.resumeAsyncContext = task_info.ResumeAsyncContext;
return result;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

#include <mutex>

#include "lldb/lldb-defines.h"
#include "lldb/lldb-types.h"
#include "swift/ABI/ObjectFile.h"
#include "swift/Remote/RemoteAddress.h"
Expand Down Expand Up @@ -163,6 +164,7 @@ class ReflectionContextInterface {
bool hasIsRunning = false;
bool isRunning = false;
bool isEnqueued = false;
lldb::addr_t resumeAsyncContext = LLDB_INVALID_ADDRESS;
};
// The default limits are copied from swift-inspect.
virtual llvm::Expected<AsyncTaskInfo>
Expand Down
53 changes: 53 additions & 0 deletions lldb/source/Plugins/LanguageRuntime/Swift/SwiftTask.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#include "SwiftTask.h"
#include "SwiftLanguageRuntime.h"
#include "lldb/Target/Process.h"

using namespace llvm;
using namespace lldb;

lldb_private::ThreadTask::ThreadTask(tid_t tid, addr_t async_ctx,
ExecutionContext &exe_ctx)
: Thread(exe_ctx.GetProcessRef(), tid, true),
m_concrete_reg_ctx_sp(exe_ctx.GetFrameSP()->GetRegisterContext()) {
m_async_ctx = async_ctx;
auto ptr_size = exe_ctx.GetTargetRef().GetArchitecture().GetAddressByteSize();
// A simplified description of AsyncContext. See swift/Task/ABI.h
// struct AsyncContext {
// AsyncContext *Parent; // offset 0
// TaskContinuationFunction *ResumeParent; // offset 8
// };
auto resume_offset = ptr_size; // offsetof(AsyncContext, ResumeParent)
auto resume_ptr = async_ctx + resume_offset;
Status status;
m_pc = exe_ctx.GetProcessRef().ReadPointerFromMemory(resume_ptr, status);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm moderately concerned that this ctor may fail.
Should we make the constructor take the continuation ptr directly and instead create a "make thread function" that can fail?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems fine to me. Note that if ReadPointerFromMemory fails, the resulting address is LLDB_INVALID_ADDRESS, which subsequent code should handle.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the code where ThreadTask's are created, I don't see any handling of invalid pcs. IIUC we will just print a thread with a weird PC?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's no handling of invalid pcs, what do you think should be done? I assumed the async context's resume function can be trusted to be valid, but you must see some ways in which it would be invalid?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's now a failable Create function.

}

RegisterContextSP lldb_private::ThreadTask::GetRegisterContext() {
if (!m_async_reg_ctx_sp)
m_async_reg_ctx_sp = std::make_shared<RegisterContextTask>(
*this, m_concrete_reg_ctx_sp, m_pc, m_async_ctx);
return m_async_reg_ctx_sp;
}

lldb_private::RegisterContextTask::RegisterContextTask(
Thread &thread, RegisterContextSP reg_info_sp, addr_t pc, addr_t async_ctx)
: RegisterContext(thread, 0), m_reg_info_sp(reg_info_sp),
m_async_ctx(async_ctx), m_pc(pc) {
auto &target = thread.GetProcess()->GetTarget();
auto triple = target.GetArchitecture().GetTriple();
if (auto regnums = GetAsyncUnwindRegisterNumbers(triple.getArch()))
m_async_ctx_regnum = regnums->async_ctx_regnum;
}

bool lldb_private::RegisterContextTask::ReadRegister(
const RegisterInfo *reg_info, RegisterValue &reg_value) {
if (reg_info->kinds[eRegisterKindGeneric] == LLDB_REGNUM_GENERIC_PC) {
reg_value = m_pc;
return true;
}
if (reg_info->kinds[eRegisterKindLLDB] == m_async_ctx_regnum) {
reg_value = m_async_ctx;
return true;
}
return false;
}
93 changes: 93 additions & 0 deletions lldb/source/Plugins/LanguageRuntime/Swift/SwiftTask.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@

#include "lldb/Target/RegisterContext.h"
#include "lldb/Target/Thread.h"
#include "lldb/Utility/RegisterValue.h"
#include "lldb/lldb-forward.h"

namespace lldb_private {

using namespace lldb;

/// Provides a subset of Thread operations for Swift Tasks.
///
/// Currently, this supports backtraces of Tasks, and selecting frames in the
/// backtrace. Async frames make available the variables that are stored in the
/// Task's "async context" (instead of the stack).
///
/// See `Task<Success, Failure>` and `UnsafeCurrentTask`
class ThreadTask : public Thread {
public:
ThreadTask(tid_t tid, addr_t async_ctx, ExecutionContext &exe_ctx);

/// Returns a Task specific register context (RegisterContextTask).
RegisterContextSP GetRegisterContext() override;

~ThreadTask() override { DestroyThread(); }

// No-op overrides.
void RefreshStateAfterStop() override {}
lldb::RegisterContextSP
CreateRegisterContextForFrame(StackFrame *frame) override {
return {};
}
bool CalculateStopInfo() override { return false; }

private:
/// A register context that is the source of `RegisterInfo` data.
RegisterContextSP m_concrete_reg_ctx_sp;
/// Lazily initialized `RegisterContextTask`.
RegisterContextSP m_async_reg_ctx_sp;
/// The Task's async context.
addr_t m_async_ctx = LLDB_INVALID_ADDRESS;
/// The address of the async context's resume function.
addr_t m_pc = LLDB_INVALID_ADDRESS;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name pc here is a bit misleading: it would be natural to think this contains the PC of the top frame of the thread, which is not what this variable contains.
Maybe it would be better to call this m_continuation_address?

When we are unwinding, we say "pc" because we are producing the register context of the frame above the current frame. Here, we just have a Thread class with a member pc, so I don't think the same interpretation would apply.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I am confused now, is this really printing frame 0 correctly?
In the register context class, there is this code:

  if (reg_info->kinds[eRegisterKindGeneric] == LLDB_REGNUM_GENERIC_PC) {
     reg_value = m_pc;
     return true;
   }

I could be missing something, but this looks like it prints the PC of frame 1 instead of frame 0.

Copy link
Author

@kastiglione kastiglione Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe m_resume_address? since "resume" is the term used in AsyncContext.

I'm not sure where the confusion stems from, but yes this prints "frame 0" correctly.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to m_resume_fn, how does that sound?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I understand 2024 Felipe's confusion now. Usually, the "resume address" (to use the terminology of the code here) means the PC of the "frame above" or the "next funclet to execute after the current frame". However, here we have Tasks which are suspended, so there is no "current frame"; in other words, the "next funclet to execute" is going to be frame zero.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have Tasks which are suspended, so there is no "current frame"; in other words, the "next funclet to execute" is going to be frame zero.

👍

};

/// A Swift Task specific register context. Supporting class for `ThreadTask`,
/// see its documentation for details.
class RegisterContextTask : public RegisterContext {
public:
RegisterContextTask(Thread &thread, RegisterContextSP reg_info_sp, addr_t pc,
addr_t async_ctx);

/// RegisterContextTask supports readonly from only two (necessary)
/// registers. Namely, the pc and the async context registers.
bool ReadRegister(const RegisterInfo *reg_info,
RegisterValue &reg_value) override;

// Pass through overrides.
size_t GetRegisterCount() override {
return m_reg_info_sp->GetRegisterCount();
}
const RegisterInfo *GetRegisterInfoAtIndex(size_t idx) override {
return m_reg_info_sp->GetRegisterInfoAtIndex(idx);
}
size_t GetRegisterSetCount() override {
return m_reg_info_sp->GetRegisterSetCount();
}
const RegisterSet *GetRegisterSet(size_t reg_set) override {
return m_reg_info_sp->GetRegisterSet(reg_set);
}
lldb::ByteOrder GetByteOrder() override {
return m_reg_info_sp->GetByteOrder();
}

// No-op overrides.
void InvalidateAllRegisters() override {}
bool WriteRegister(const RegisterInfo *reg_info,
const RegisterValue &reg_value) override {
return false;
}

private:
/// A register context that is the source of `RegisterInfo` data.
RegisterContextSP m_reg_info_sp;
/// The architecture specific regnum (LLDB) which holds the async context.
uint32_t m_async_ctx_regnum = LLDB_INVALID_REGNUM;
/// The Task's async context.
RegisterValue m_async_ctx;
/// The address of the async context's resume function.
RegisterValue m_pc;
};

} // namespace lldb_private