Skip to content
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] Introduce backtracing of Swift Tasks #9787

Merged

Conversation

kastiglione
Copy link

@kastiglione kastiglione commented Dec 20, 2024

Introduces the first of a new group of commands for working with Swift Task instances.

The command group is named language swift task, and the first command is backtrace. This backtrace command takes the name of a variable of type Task<Success, Failure> type, and prints the task's backtrace. The output is similar to the builtin thread backtrace (bt) command.

@kastiglione kastiglione requested a review from a team as a code owner December 20, 2024 23:34
@kastiglione kastiglione marked this pull request as draft December 20, 2024 23:35
Comment on lines 824 to 831
// 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.

lldb/source/Plugins/LanguageRuntime/Swift/SwiftTask.cpp Outdated Show resolved Hide resolved
lldb/source/Plugins/LanguageRuntime/Swift/SwiftTask.h Outdated Show resolved Hide resolved
/// 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.

👍

lldb/source/Plugins/Language/Swift/SwiftFormatters.cpp Outdated Show resolved Hide resolved
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.

uint32_t resume_ptr = async_ctx + resume_offset;
Status status;
m_resume_fn =
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.

IIUC to apply the skip prologue trick, you would need to factor out the fragment below of SwiftLanguageRuntime::TrySkipVirtualParentProlog into a separate function, and say that pc_value = m_resume_fn:

  Address pc;
  Target &target = process.GetTarget();
  pc.SetLoadAddress(pc_value, &target);
  if (!pc.IsValid())
    return {};

  SymbolContext sc;
  if (!pc.CalculateSymbolContext(&sc,
                                 eSymbolContextFunction | eSymbolContextSymbol))
    return {};
  if (!sc.symbol && !sc.function)
    return {};

  auto prologue_size = sc.symbol ? sc.symbol->GetPrologueByteSize()
                                 : sc.function->GetPrologueByteSize();
  return pc_value + prologue_size;

Copy link
Author

Choose a reason for hiding this comment

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

This will be added in a follow up change, which adds language swift task select <task>.

// TaskContinuationFunction *ResumeParent; // offset 8
// };
uint32_t resume_offset = ptr_size; // offsetof(AsyncContext, ResumeParent)
uint32_t resume_ptr = async_ctx + resume_offset;

Choose a reason for hiding this comment

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

Given that you show the offsets of Parent and ResumeParent in the comment, is it clarifying to have resume_offset. Why not just addr_t resume_ptr = async_ctx + ptr_size. (also why is resume_ptr uint32_t, it's adding an addr_t and an offset isn't it?)

Copy link
Author

Choose a reason for hiding this comment

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

also why is resume_ptr uint32_t, it's adding an addr_t and an offset isn't it?

yep you caught a bug, it was after I switched these variables away from auto types.

Choose a reason for hiding this comment

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

That's one of the reasons I will die on the proverbial auto hill 👀

Copy link
Author

Choose a reason for hiding this comment

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

I'm with you

@kastiglione kastiglione marked this pull request as ready for review January 14, 2025 23:41
@kastiglione
Copy link
Author

@swift-ci test

@kastiglione
Copy link
Author

@swift-ci test windows

Copy link

@adrian-prantl adrian-prantl left a comment

Choose a reason for hiding this comment

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

Looks good with some minor comments inline.

return;
}

if (command[0].ref().empty()) {

Choose a reason for hiding this comment

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

Is command always guaranteed to be non-empty?

Copy link
Author

@kastiglione kastiglione Jan 15, 2025

Choose a reason for hiding this comment

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

thanks for catching this… it is not.

return;

ValueObjectSP task_obj_sp = valobj_sp->GetChildMemberWithName("_task");
if (!task_obj_sp)

Choose a reason for hiding this comment

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

Shouldn't there something being logged to the command's error stream for all of these failures, so users see what's wrong?

if (auto err = task_info.takeError()) {
LLDB_LOG(GetLog(LLDBLog::DataFormatters | LLDBLog::Types),
"could not get info for async task {0:x}: {1}", task_ptr,
fmt_consume(std::move(err)));

Choose a reason for hiding this comment

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

same here, I think this should be surfaced to the user instead of the log?

"`second() at main.swift:6",
"`first() at main.swift:2",
"`closure #1 in static Main.main() at main.swift:12",
],

Choose a reason for hiding this comment

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

does (lldb) help language swift task (backtrace) work?

Copy link
Author

Choose a reason for hiding this comment

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

yes, the help is here in the definitions of these commands.

@kastiglione
Copy link
Author

I've applied your advice @adrian-prantl, thanks.

@kastiglione
Copy link
Author

@swift-ci test

Copy link

@adrian-prantl adrian-prantl left a comment

Choose a reason for hiding this comment

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

Awesome. The error handling will pay off big time when we get bug reports :-)

@adrian-prantl adrian-prantl merged commit 94fd311 into swift/release/6.1 Jan 16, 2025
3 checks passed
@adrian-prantl adrian-prantl deleted the dl/lldb-Introduce-backtracing-of-Swift-Tasks branch January 16, 2025 17:47
kastiglione added a commit that referenced this pull request Jan 17, 2025
Introduces the first of a new group of commands for working with Swift Task instances.

The new command group is `language swift task`, and the first command is `backtrace`. This `backtrace` command takes the name of a task variable and prints the task's backtrace. The variable can be either `Task<Success, Failure>` or `UnsafeCurrentTask`. The output is similar to the builtin `thread backtrace` (`bt`) command.

See the original PR: #9787
kastiglione added a commit that referenced this pull request Jan 22, 2025
Introduces the first of a new group of commands for working with Swift Task instances.

The new command group is `language swift task`, and the first command is `backtrace`. This `backtrace` command takes the name of a task variable and prints the task's backtrace. The variable can be either `Task<Success, Failure>` or `UnsafeCurrentTask`. The output is similar to the builtin `thread backtrace` (`bt`) command.

See the original PR: #9787
kastiglione added a commit that referenced this pull request Jan 22, 2025
Introduces the first of a new group of commands for working with Swift Task instances.

The new command group is `language swift task`, and the first command is `backtrace`. This `backtrace` command takes the name of a task variable and prints the task's backtrace. The variable can be either `Task<Success, Failure>` or `UnsafeCurrentTask`. The output is similar to the builtin `thread backtrace` (`bt`) command.

See the original PR: #9787

(cherry picked from commit 2c3335d)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants