-
Notifications
You must be signed in to change notification settings - Fork 341
[lldb] Swift OS plugin #9839
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
[lldb] Swift OS plugin #9839
Conversation
lldb/source/Plugins/OperatingSystem/Swift/OperatingSystemSwift.cpp
Outdated
Show resolved
Hide resolved
lldb/source/Plugins/OperatingSystem/Swift/OperatingSystemSwift.cpp
Outdated
Show resolved
Hide resolved
lldb/source/Plugins/OperatingSystem/Swift/OperatingSystemSwift.cpp
Outdated
Show resolved
Hide resolved
lldb/source/Plugins/OperatingSystem/Swift/OperatingSystemSwift.cpp
Outdated
Show resolved
Hide resolved
} | ||
|
||
// Mask higher bits to avoid conflicts with core thread IDs. | ||
uint64_t masked_task_id = 0xdeadbeef00000000 | *task_id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will be the visible "TID" for running tasks, right? And what I'd have to type if I wanted to set a "task specific" breakpoint? If so, we might not want it to be deadbeef. That's cute if you know the reference, but otherwise somewhat off-putting...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah, I'm actually looking for suggestions here, if we should mask it at all and what to use if we do
Deadbeef was just something to help me grep the logs as I debugged.
ThreadSP swift_thread = [&]() -> ThreadSP { | ||
// If we already had a thread for this Task in the last stop, re-use it. | ||
if (ThreadSP old_thread = old_thread_list.FindThreadByID(masked_task_id); | ||
IsOperatingSystemPluginThread(old_thread)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have to handle the case where when you stopped the first time, this Task was backed by "core thread 1" but the next time you stopped it was backed by a different core thread? I don't see where you are changing the backing thread in this case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see where you are changing the backing thread in this case?
It's just a few lines below, both cases are handled the same way:
swift_thread->SetBackingThread(real_thread);
I note that the only swift specific bits of this are finding the task on a particular thread, and a few checks about whether this should be used that I think belong more properly in the LanguageRuntime that owns the OS plugin... Would it be possible to make an |
969ca8a
to
8aeb614
Compare
@@ -302,6 +302,7 @@ class ThreadPlanRunToAddressOnAsyncCtx : public ThreadPlan { | |||
auto &target = thread.GetProcess()->GetTarget(); | |||
m_funclet_bp = target.CreateBreakpoint(destination_addr, true, false); | |||
m_funclet_bp->SetBreakpointKind("async-run-to-funclet"); | |||
m_funclet_bp->SetThreadID(thread.GetID()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to refresh reviewer's memories: this is the thread plan to "run through a task switch and stop when we schedule this same async function again. This plan now sets thread specific breakpoints, since they are now task breakpoints!
If we somehow push this type of plan without having the OS plugin, we are at a risk of breaking in the wrong task. But if the plugin is not there and there are tasks, we are going to have much bigger problems anyway. For example, this code has been associated with a lot of LLDB crashes because it triggers stack plan migration (this is a downstream-only thing that becomes dead code with this patch, and I'll delete it in a follow up PR)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be this an inline comment in the source file ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would argue that in this case we don't need any comments, because this is just the "natural" thing to do. If anything, it is the old implementation that deserved a comment, as it was essentially making all threads stop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This plan now sets thread specific breakpoints, since they are now task breakpoints!
I'm not sure I understand what you're saying with this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just noticed that the writing was indeed confusing, my bad.
The important thing to note here is that this plan is no longer special, it should work like any other plan: when it sets a breakpoint, it sets a thread specific breakpoint (otherwise it would cause all threads to stop).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A follow up patch will remove this plan in favor of the RunToBreakpointPlan
8aeb614
to
9bd7fa3
Compare
9bd7fa3
to
5142ea2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cool stuff! I left some comments / suggestions :)
@@ -168,6 +191,52 @@ static ModuleSP findRuntime(Process &process, RuntimeKind runtime_kind) { | |||
return runtime_image; | |||
} | |||
|
|||
ModuleSP SwiftLanguageRuntime::findConcurrencyModule(Process &process) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: casing
ModuleSP SwiftLanguageRuntime::findConcurrencyModule(Process &process) { | |
ModuleSP SwiftLanguageRuntime::FindConcurrencyModule(Process &process) { |
} | ||
|
||
std::optional<uint32_t> | ||
SwiftLanguageRuntime::findConcurrencyDebugVersion(Process &process) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops, will change!
@@ -98,6 +98,16 @@ class SwiftLanguageRuntime : public LanguageRuntime { | |||
static SwiftLanguageRuntime *Get(lldb::ProcessSP process_sp) { | |||
return SwiftLanguageRuntime::Get(process_sp.get()); | |||
} | |||
|
|||
/// Returns the Module containing the Swift Concurrency runtime, if it exists. | |||
static lldb::ModuleSP findConcurrencyModule(Process &process); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
/// returned. | ||
/// Returns 0 for versions of the module prior to the introduction | ||
/// of versioning. | ||
static std::optional<uint32_t> findConcurrencyDebugVersion(Process &process); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
@@ -302,6 +302,7 @@ class ThreadPlanRunToAddressOnAsyncCtx : public ThreadPlan { | |||
auto &target = thread.GetProcess()->GetTarget(); | |||
m_funclet_bp = target.CreateBreakpoint(destination_addr, true, false); | |||
m_funclet_bp->SetBreakpointKind("async-run-to-funclet"); | |||
m_funclet_bp->SetThreadID(thread.GetID()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be this an inline comment in the source file ?
} | ||
|
||
// Mask higher bits to avoid conflicts with core thread IDs. | ||
uint64_t masked_task_id = 0x0000000f00000000 | *task_id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice hack :p
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be honest I don't think we need to / should do this, because conflicts are handled without issues... it is just a way to visually know we have a fake thread.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how are the conflicts handled?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a couple of lines down, on line 100
if (ThreadSP old_thread = old_thread_list.FindThreadByID(masked_task_id); | ||
IsOperatingSystemPluginThread(old_thread)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First time I'm seeing this syntax :p Don't you want to run the second part of the if condition only if the first one if valid ?
if (ThreadSP old_thread = old_thread_list.FindThreadByID(masked_task_id); | |
IsOperatingSystemPluginThread(old_thread)) | |
if ((ThreadSP old_thread = old_thread_list.FindThreadByID(masked_task_id)) && IsOperatingSystemPluginThread(old_thread)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IsOperatingSystemPluginThread
is designed to take a pointer argument, so it knows how to handle nullptrs.
5142ea2
to
cdc4b54
Compare
addressed review comments |
size_t ptr_size = process.GetAddressByteSize(); | ||
// Offset of a Task ID inside a Task data structure, guaranteed by the ABI. | ||
// See Job in swift/RemoteInspection/RuntimeInternals.h. | ||
m_task_id_offset = 4 * ptr_size + 4; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this the job id? if so should the name reflect that? also if so, do we know if we can ignore the task's upper 32 bit identifier?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on what I saw in the runtime code, I think it's safe to do so. It is what is being done in other tools anyway.
I'll rename this to "job id"!
ThreadSP swift_thread = [&]() -> ThreadSP { | ||
// If we already had a thread for this Task in the last stop, re-use it. | ||
if (ThreadSP old_thread = old_thread_list.FindThreadByID(masked_task_id); | ||
IsOperatingSystemPluginThread(old_thread)) | ||
return old_thread; | ||
|
||
std::string name = llvm::formatv("Swift Task {0:x}", *task_id); | ||
llvm::StringRef queue_name = ""; | ||
return std::make_shared<ThreadMemory>(*m_process, masked_task_id, name, | ||
queue_name, | ||
/*register_data_addr*/ 0); | ||
}(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lambda through me off. Is there something I'm missing that it can't be an if/else?
ThreadSP swift_thread = [&]() -> ThreadSP { | |
// If we already had a thread for this Task in the last stop, re-use it. | |
if (ThreadSP old_thread = old_thread_list.FindThreadByID(masked_task_id); | |
IsOperatingSystemPluginThread(old_thread)) | |
return old_thread; | |
std::string name = llvm::formatv("Swift Task {0:x}", *task_id); | |
llvm::StringRef queue_name = ""; | |
return std::make_shared<ThreadMemory>(*m_process, masked_task_id, name, | |
queue_name, | |
/*register_data_addr*/ 0); | |
}(); | |
ThreadSP swift_thread; | |
// If we already had a thread for this Task in the last stop, re-use it. | |
if (ThreadSP old_thread = old_thread_list.FindThreadByID(masked_task_id); | |
IsOperatingSystemPluginThread(old_thread)) { | |
swift_thread = old_thread; | |
} else { | |
std::string name = llvm::formatv("Swift Task {0:x}", *task_id); | |
llvm::StringRef queue_name = ""; | |
swift_thread = std::make_shared<ThreadMemory>(*m_process, masked_task_id, name, | |
queue_name, | |
/*register_data_addr*/ 0); | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The missing piece is that this creates an uninitialized variable :)
I'll move this code to a helper function
|
||
if (*concurrency_version <= 1) | ||
return new OperatingSystemSwiftTasks(*process); | ||
LLDB_LOGF(log, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you intend to only log the version in the failure case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, if you've encountered a more recent version of the concurrency library that this lldb recognizes, we should tell the user that. Could there be a Status
path threaded to this Initialize
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I was originally only logging the problematic case, but there is no harm in logging it all the time, so I'll move the log statement before the conditional
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, if you've encountered a more recent version of the concurrency library that this lldb recognizes, we should tell the user that. Could there be a Status path threaded to this Initialize.
Is this how we do user warnings? I was grepping around and found Debugger::ReportWarning
, with a matching std::once_flag
flag. If this is the standard way of doing things, and to make sure a warning for this plugin is only reported once, it seems like the implementation would be:
- call ReportWarning inside
OperatingSystemSwiftTasks
directly, - have that class own the std::once flag. (Note that the "CreateInstance" methods are called many times, e.g. every time libraries are loaded)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What you've added here looks right. We only warn if we found the concurrence library but it looks funny to us.
return old_thread; | ||
|
||
std::string name = llvm::formatv("Swift Task {0:x}", *task_id); | ||
llvm::StringRef queue_name = ""; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the core thread had a queue name, would we want to use it here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to register an offline conversation, an empty queue name will make ThreadMemory propagate the backing thread queue name once this fix gets merged: llvm#126128
f1a3807
to
bf4eb62
Compare
bf4eb62
to
4de0f0c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This LGTM. This worked more easily than I expected it would, that's great.
I don't see why the tests shouldn't also work on Linux/Windows but we don't need to hold up this patch to resolve that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
@swift-ci test |
4de0f0c
to
33da3aa
Compare
Update the commit message |
@swift-ci test |
1 similar comment
@swift-ci test |
This is here mostly out of an abundance of caution, the default is to use the plugins.
This commit introduces an OS plugin converting Process threads into OperatingSystem threads that represent a Task. This is done by inspecting ThreadLocalStorage for a specific location where the swift runtime writes information about the current Task. With this plugin, ThreadPlans now work properly in async code: a step action on a Thread is now effectively an action on a Task, and LLDB will correctly track that. ThreadPlans are effectively "Task Plans".
33da3aa
to
dd46e67
Compare
Rebased |
@swift-ci test |
With [1], stepping should work reliably even in the multithreaded case. With [2], the issue of multiple breakpoints on the same should no longer affect this test. [1]: swiftlang#9839 [2]: swiftlang#9112
The initial implementation of swift async stepping involved migrating ThreadPlans from thread to thread. With the OS plugins introduced in [1], this is no longer needed and the associated code is dead. To find the relevant diffs, files were compared with their upstream counterparts, and code mentioning the following concepts was deleted: * "synchronizing plans", * "detached plans", * "setting TIDs on a thread plan", * "activating a thread plan stack". The storage for thread plan stacks had been substantially changed downstream to allow for the concepts above. This patch restores the original storage data structures. These were the files compared: * ThreadPlanStack{.cpp, .h} * ThreadPlan{.cpp, .h} * Process{.cpp, .h} [1]: swiftlang#9839
The initial implementation of swift async stepping involved migrating ThreadPlans from thread to thread. With the OS plugins introduced in [1], this is no longer needed and the associated code is dead. To find the relevant diffs, files were compared with their upstream counterparts, and code mentioning the following concepts was deleted: * "synchronizing plans", * "detached plans", * "setting TIDs on a thread plan", * "activating a thread plan stack". The storage for thread plan stacks had been substantially changed downstream to allow for the concepts above. This patch restores the original storage data structures. These were the files compared: * ThreadPlanStack{.cpp, .h} * ThreadPlan{.cpp, .h} * Process{.cpp, .h} [1]: #9839 (cherry picked from commit c8c7df7)
This PR implements the required functionality for a swift OS plugins that converts Tasks into Threads, allowing LLDB to transparently work with Tasks as if they were threads.
In particular, this addresses all (famous last words?) the outstanding stepping issues in swift async code.