================ @@ -301,177 +735,284 @@ optimized to the equivalent of: std::cout << a+5 << "\n"; } -It should now be obvious why the value of `__int_32_0` remains unchanged -throughout the function. It is important to recognize that `__int_32_0` -does not directly correspond to `a`, but is instead a variable generated -to assist the compiler in code generation. The variables in an optimized -coroutine frame should not be thought of as directly representing the -variables in the C++ source. - -Get the suspended points -======================== - -An important requirement for debugging coroutines is to understand suspended -points, which are where the coroutine is currently suspended and awaiting. - -For simple cases like the above, inspecting the value of the `__coro_index` -variable in the coroutine frame works well. +It should now be obvious why the value of ``__int_32_0`` remains unchanged +throughout the function. It is important to recognize that ``__int_32_0`` does +not directly correspond to ``a``, but is instead a variable generated to assist +the compiler in code generation. The variables in an optimized coroutine frame +should not be thought of as directly representing the variables in the C++ +source. -However, it is not quite so simple in really complex situations. In these -cases, it is necessary to use the coroutine libraries to insert the -line-number. -For example: - -.. code-block:: c++ +Resources +========= - // For all the promise_type we want: - class promise_type { - ... - + unsigned line_number = 0xffffffff; - }; +.. _lldb-script: - #include <source_location> +LLDB Debugger Script +-------------------- - // For all the awaiter types we need: - class awaiter { - ... - template <typename Promise> - void await_suspend(std::coroutine_handle<Promise> handle, - std::source_location sl = std::source_location::current()) { - ... - handle.promise().line_number = sl.line(); - } - }; +The following script provides the ``coro bt`` and ``coro in-flight`` commands +discussed above. It can be loaded into LLDB using ``command script import +lldb_coro_debugging.py``. To load this by default, add this command to your +``~/.lldbinit`` file. -In this case, we use `std::source_location` to store the line number of the -await inside the `promise_type`. Since we can locate the coroutine function -from the address of the coroutine, we can identify suspended points this way -as well. +Note that this script requires LLDB 21.0 or newer. -The downside here is that this comes at the price of additional runtime cost. -This is consistent with the C++ philosophy of "Pay for what you use". - -Get the asynchronous stack -========================== - -Another important requirement to debug a coroutine is to print the asynchronous -stack to identify the asynchronous caller of the coroutine. As many -implementations of coroutine types store `std::coroutine_handle<> continuation` -in the promise type, identifying the caller should be trivial. The -`continuation` is typically the awaiting coroutine for the current coroutine. -That is, the asynchronous parent. - -Since the `promise_type` is obtainable from the address of a coroutine and -contains the corresponding continuation (which itself is a coroutine with a -`promise_type`), it should be trivial to print the entire asynchronous stack. - -This logic should be quite easily captured in a debugger script. - -Examples to print asynchronous stack ------------------------------------- - -Here is an example to print the asynchronous stack for the normal task implementation. +.. code-block:: python -.. code-block:: c++ + # lldb_coro_debugging.py + import lldb + from lldb.plugins.parsed_cmd import ParsedCommand + + def _get_first_var_path(v, paths): + """ + Tries multiple variable paths via `GetValueForExpressionPath` + and returns the first one that succeeds, or None if none succeed. + """ + for path in paths: + var = v.GetValueForExpressionPath(path) + if var.error.Success(): + return var + return None + + + def _print_async_bt(coro_hdl, result, *, curr_idx, start, limit, continuation_paths, prefix=""): + """ + Prints a backtrace for an async coroutine stack starting from `coro_hdl`, + using the given `continuation_paths` to get the next coroutine from the promise. + """ + target = coro_hdl.GetTarget() + while curr_idx < limit and coro_hdl is not None and coro_hdl.error.Success(): + # Print the stack frame, if in range + if curr_idx >= start: + # Figure out the function name + destroy_func_var = coro_hdl.GetValueForExpressionPath(".destroy") + destroy_addr = target.ResolveLoadAddress(destroy_func_var.GetValueAsAddress()) + func_name = destroy_addr.function.name + # Figure out the line entry to show + suspension_addr_var = coro_hdl.GetValueForExpressionPath(".promise._coro_suspension_point_addr") + if suspension_addr_var.error.Success(): + line_entry = target.ResolveLoadAddress(suspension_addr_var.GetValueAsAddress()).line_entry + print(f"{prefix} frame #{curr_idx}: {func_name} at {line_entry}", file=result) + else: + # We don't know the exact line, print the suspension point ID, so we at least show + # the id of the current suspension point + suspension_point_var = coro_hdl.GetValueForExpressionPath(".coro_frame.__coro_index") + if suspension_point_var.error.Success(): + suspension_point = suspension_point_var.GetValueAsUnsigned() + else: + suspension_point = "unknown" + line_entry = destroy_addr.line_entry + print(f"{prefix} frame #{curr_idx}: {func_name} at {line_entry}, suspension point {suspension_point}", file=result) + # Move to the next stack frame + curr_idx += 1 + promise_var = coro_hdl.GetChildMemberWithName("promise") + coro_hdl = _get_first_var_path(promise_var, continuation_paths) + return curr_idx + + def _print_combined_bt(frame, result, *, unfiltered, curr_idx, start, limit, continuation_paths): + """ + Prints a backtrace starting from `frame`, interleaving async coroutine frames + with regular frames. + """ + while curr_idx < limit and frame.IsValid(): + if curr_idx >= start and (unfiltered or not frame.IsHidden()): + print(f"frame #{curr_idx}: {frame.name} at {frame.line_entry}", file=result) + curr_idx += 1 + coro_var = _get_first_var_path(frame.GetValueForVariablePath("__promise"), continuation_paths) + if coro_var: + curr_idx = _print_async_bt(coro_var, result, + curr_idx=curr_idx, start=start, limit=limit, + continuation_paths=continuation_paths, prefix="[async]") + frame = frame.parent + + + class CoroBacktraceCommand(ParsedCommand): + def get_short_help(self): + return "Create a backtrace for C++-20 coroutines" + + def get_flags(self): + return lldb.eCommandRequiresFrame | lldb.eCommandProcessMustBePaused + + def setup_command_definition(self): + ov_parser = self.get_parser() + ov_parser.add_option( + "e", + "continuation-expr", + help = ( + "Semi-colon-separated list of expressions evaluated against the promise object" + "to get the next coroutine (e.g. `.continuation;.coro_parent`)" + ), + value_type = lldb.eArgTypeNone, + dest = "continuation_expr_arg", + default = ".continuation", + ) + ov_parser.add_option( + "c", + "count", + help = "How many frames to display (0 for all)", + value_type = lldb.eArgTypeCount, + dest = "count_arg", + default = 20, + ) + ov_parser.add_option( + "s", + "start", + help = "Frame in which to start the backtrace", + value_type = lldb.eArgTypeIndex, + dest = "frame_index_arg", + default = 0, + ) + ov_parser.add_option( + "u", + "unfiltered", + help = "Do not filter out frames according to installed frame recognizers", + value_type = lldb.eArgTypeBoolean, + dest = "unfiltered_arg", + default = False, + ) + ov_parser.add_argument_set([ + ov_parser.make_argument_element( + lldb.eArgTypeExpression, + repeat="optional" + ) + ]) + + def __call__(self, debugger, args_array, exe_ctx, result): + ov_parser = self.get_parser() + continuation_paths = ov_parser.continuation_expr_arg.split(";") + count = ov_parser.count_arg + if count == 0: + count = 99999 + frame_index = ov_parser.frame_index_arg + unfiltered = ov_parser.unfiltered_arg + + frame = exe_ctx.GetFrame() + if not frame.IsValid(): + result.SetError("invalid frame") + return - // debugging-example.cpp - #include <coroutine> - #include <iostream> - #include <utility> + if len(args_array) > 1: + result.SetError("At most one expression expected") + return + elif len(args_array) == 1: + expr = args_array.GetItemAtIndex(0).GetStringValue(9999) + coro_hdl = frame.EvaluateExpression(expr) + if not coro_hdl.error.Success(): + result.AppendMessage( + f'error: expression failed {expr} => {async_root.error}' + ) + result.SetError(f"Expression `{expr}` failed to evaluate") + return + _print_async_bt(coro_hdl, result, + curr_idx = 0, start = frame_index, limit = frame_index + count, + continuation_paths = continuation_paths) + else: + _print_combined_bt(frame, result, unfiltered=unfiltered, + curr_idx = 0, start = frame_index, limit = frame_index + count, + continuation_paths = continuation_paths) + + + class Coroin-flightCommand(ParsedCommand): + def get_short_help(self): + return "Identify all in-flight coroutines" + + def get_flags(self): + return lldb.eCommandRequiresTarget | lldb.eCommandProcessMustBePaused + + def setup_command_definition(self): + ov_parser = self.get_parser() + ov_parser.add_option( + "e", + "continuation-expr", + help = ( + "Semi-colon-separated list of expressions evaluated against the promise object" + "to get the next coroutine (e.g. `.continuation;.coro_parent`)" + ), + value_type = lldb.eArgTypeNone, + dest = "continuation_expr_arg", + default = ".continuation", + ) + ov_parser.add_option( + "c", + "count", + help = "How many frames to display (0 for all)", + value_type = lldb.eArgTypeCount, + dest = "count_arg", + default = 5, + ) + ov_parser.add_argument_set([ + ov_parser.make_argument_element( + lldb.eArgTypeExpression, + repeat="plus" + ) + ]) + + def __call__(self, debugger, args_array, exe_ctx, result): + ov_parser = self.get_parser() + continuation_paths = ov_parser.continuation_expr_arg.split(";") + count = ov_parser.count_arg + + # Collect all coroutine_handles from the provided containers + all_coros = [] + for entry in args_array: + expr = entry.GetStringValue(9999) + if exe_ctx.frame.IsValid(): + coro_container = exe_ctx.frame.EvaluateExpression(expr) + else: + coro_container = exe_ctx.target.EvaluateExpression(expr) + if not coro_container.error.Success(): + result.AppendMessage( + f'error: expression failed {expr} => {coro_container.error}' + ) + result.SetError(f"Expression `{expr}` failed to evaluate") + return + for entry in coro_container.children: + if "coroutine_handle" not in entry.GetType().name: + result.SetError(f"Found entry of type {entry.GetType().name} in {expr}," + " expected a coroutine handle") + return + all_coros.append(entry) - struct task { - struct promise_type { - task get_return_object(); - std::suspend_always initial_suspend() { return {}; } + # Remove all coroutines that have are currently waiting for other coroutines to finish + coro_roots = {c.GetChildMemberWithName("coro_frame").GetValueAsAddress(): c for c in all_coros} + for coro_hdl in all_coros: + parent_coro = _get_first_var_path(coro_hdl.GetChildMemberWithName("promise"), continuation_paths) + parent_addr = parent_coro.GetChildMemberWithName("coro_frame").GetValueAsAddress() + if parent_addr in coro_roots: + del coro_roots[parent_addr] - void unhandled_exception() noexcept {} + # Print all remaining coroutines + for addr, root_hdl in coro_roots.items(): + print(f"coroutine root 0x{addr:x}", file=result) + _print_async_bt(root_hdl, result, + curr_idx=0, start=0, limit=count, + continuation_paths=continuation_paths, prefix=" ") - struct FinalSuspend { - std::coroutine_handle<> continuation; - auto await_ready() noexcept { return false; } - auto await_suspend(std::coroutine_handle<> handle) noexcept { - return continuation; - } - void await_resume() noexcept {} - }; - FinalSuspend final_suspend() noexcept { return {continuation}; } - void return_value(int res) { result = res; } + def __lldb_init_module(debugger, internal_dict): + debugger.HandleCommand("command container add -h 'Debugging utilities for C++20 coroutines' coro") + debugger.HandleCommand(f"command script add -o -p -c {__name__}.CoroBacktraceCommand coro bt") + debugger.HandleCommand(f"command script add -o -p -c {__name__}.Coroin-flightCommand coro in-flight") + print("Coro debugging utilities installed. Use `help coro` to see available commands.") - std::coroutine_handle<> continuation = std::noop_coroutine(); - int result = 0; - }; + if __name__ == '__main__': + print("This script should be loaded from LLDB using `command script import <filename>`") - task(std::coroutine_handle<promise_type> handle) : handle(handle) {} - ~task() { - if (handle) - handle.destroy(); - } +.. _gdb-script: - auto operator co_await() { - struct Awaiter { - std::coroutine_handle<promise_type> handle; - auto await_ready() { return false; } - auto await_suspend(std::coroutine_handle<> continuation) { - handle.promise().continuation = continuation; - return handle; - } - int await_resume() { - int ret = handle.promise().result; - handle.destroy(); - return ret; - } - }; - return Awaiter{std::exchange(handle, nullptr)}; - } +GDB Debugger Script +------------------- - int syncStart() { - handle.resume(); - return handle.promise().result; - } +For GDB, the following script provides a couple of useful commands: - private: - std::coroutine_handle<promise_type> handle; - }; - - task task::promise_type::get_return_object() { - return std::coroutine_handle<promise_type>::from_promise(*this); - } - - namespace detail { - template <int N> - task chain_fn() { - co_return N + co_await chain_fn<N - 1>(); - } - - template <> - task chain_fn<0>() { - // This is the default breakpoint. - __builtin_debugtrap(); - co_return 0; - } - } // namespace detail - - task chain() { - co_return co_await detail::chain_fn<30>(); - } - - int main() { - std::cout << chain().syncStart() << "\n"; - return 0; - } - -In the example, the ``task`` coroutine holds a ``continuation`` field, -which would be resumed once the ``task`` completes. -In another word, the ``continuation`` is the asynchronous caller for the ``task``. -Just like the normal function returns to its caller when the function completes. - -So we can use the ``continuation`` field to construct the asynchronous stack: +* ``async-bt`` to print the stack trace of a coroutine +* ``show-coro-frame`` to print the coroutine frame, similar to + LLDB's builtin pretty-printer for coroutine frames .. code-block:: python - # debugging-helper.py + # debugging-helper.py ---------------- zwuis wrote:
I think this should be reverted. https://github.com/llvm/llvm-project/pull/142651 _______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits