================ @@ -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): ---------------- zwuis wrote:
Is this typo? 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