-
Notifications
You must be signed in to change notification settings - Fork 190
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
Add initial support for async stack traces to Unifex #616
Conversation
b578556
to
4f975a7
Compare
This isn't really ready for review; I'm marking it as such to trigger some Meta-internal automation. |
c357920
to
9d3358f
Compare
This diff, originally by @janondrusek and @jesswong, copies the core of Folly's [async stack trace support](https://github.com/facebook/folly/tree/main/folly/tracing) into `include/unifex/tracing`.
9d3358f
to
711a18e
Compare
Stop using `void*` to represent both instruction pointers and stack frame pointers and start using `unifex::instruction_ptr` and `unifex::frame_ptr`.
ac17292
to
a37253f
Compare
We need a way to restore a `ScopedAsyncStackRoot` to the "no active frame" state before destroying it on the way out of a customization of `unifex::start` but the frame we want to deactivate is a member of the operation state, which means it's likely already been destroyed. This diff adds `ScopedAsyncStackRoot::ensureFrameDeactivated()`, which performs most of the same actions as `deactivateAsyncStackFrame()` but without touching the frame. I think this still technically invokes UB by copying and comparing a zapped pointer, but it's better than what we had before.
This diff adds a new receiver query CPO that is expected to return the address of the `AsyncStackFrame` associated with the receiver's operation.
This diff adds a new sender query CPO that is expected to return the instruction pointer best representing the "return address" for the sender; the default implementation returns the return address of a function template instantiation that includes the sender's type in its signature as a kind of "better than nothing" result.
The `instruction_ptr` type is best rendered by the debugger as an "address", which will render as a symbol + offset rather than an arbitrary hexadecimal value. This diff adds a comment to the type documenting this fact.
This diff modifies `unifex::sync_wait()` to establish an `AsyncStackRoot` on the stack while the awaited operation is running.
This diff modifies `unifex::connect` to inject async stack tracking into every operation state is it's built.
The Unifex unit test suite won't build for Windows with async stack injection enabled *unless* PR #619 (Make any_sender_of<> play nicer with MSVC) is also merged, but that PR causes Windows + Clang + ASAN errors in Meta-internal builds. This diff works around the above conflict by disabling async stack injection in Windows builds by default so we don't need PR #619. We can change the default once we figure out a proper resolution to the ASAN problem.
a37253f
to
2d1d85b
Compare
cool! would be extra super duper cool if it came with debugger scripts for dumping the backtrace. but maybe that belongs in a separate PR. do you have an example of such a backtrace? i'm curious what it looks like. |
This PR makes Unifex's runtime representation of async stacks compatible with Folly's so Folly's
Here's an
Things to note:
|
I'm looking for a frame that represents the transition from thread 1 to thread 5, something like a transfer. I'm not seeing it tho. Why? |
Because the sender that did that has already completed by the time the breakpoint I selected hits. Completed operations are, in this respect, analogous to functions that have returned—they're not on the stack because the stack represents the list of suspended operations waiting to be completed. |
This change extends the work in #616 to support async stack frames in `task<>` coroutines, including those that invoke `at_coroutine_exit()`. In `task<>`, when `UNIFEX_NO_ASYNC_STACKS` is falsey, the awaiter returned from `task<>`'s customization of `unifex::await_transform` stores an `AsyncStackFrame`. The awaiter pushes its frame onto the current async stack in `await_suspend()` and pops it again in `await_resume()`; since `await_resume()` is only invoked for value and error completions, this arrangement leaves it up to the waiting task to pop the awaiter's frame when the awaited task completes with done. This can be expressed as a new rule: - when a coroutine completes with a value or an error, it is responsible for popping its own `AsyncStackFrame`; but - when a coroutine completes with done, the *caller* is responsible for popping the callee's `AsyncStackFrame` as a part of the caller's `unhandled_done()` coroutine. To support this new requirement of `unhandled_done()` (that it is responsible for popping the callee's stack frame), this change introduces `popAsyncStackFrameFromCaller`, which takes the caller's stack frame by reference so that it can assert that, after popping the current async frame (whatever it is), the new top frame is the caller's frame. A `task<>` promise has an `AsyncStackFrame*` that, when it's not `nullptr`, points to the `AsyncStackFrame` in the awaiter waiting for the task. This pointer exists even when `UNIFEX_NO_ASYNC_STACKS` is truthy to help mitigate against ODR violations; linking together two TUs with `UNIFEX_NO_ASYNC_STACKS` set differently is not explicitly supported but, by ensuring this pointer always exists, some ODR problems are avoided. When a `task<>` is awaited from a TU with async stack support enabled, the awaited task's awaiter sets the promise's `AsyncStackFrame*` to point to the awaiter's frame; when a `task<>` is awaited from a TU with async stack support disabled, this assignment never happens and the promise's pointer remains null. The above description of `task<>`'s async stack maintenance only covers the recursive case of on coroutine awaiting another. The base case is handled in `connect_awaitable()`, where an `AsyncStackRoot` is set up before starting the connected awaitable. `stop_if_requested` used to model both `sender` and `awaitable` so that `co_await stop_if_requested();` could take advantage of symmetric transfer. The `stop_if_requested` sender now customizes `await_transform` to express its participation in async stack management. This means of expressing async stack awareness is unsatisfying but I don't have any better ideas right now. Lastly, `unifex::await_transform()` now wraps naturally-awaitable arguments in an `awaiter_wrapper` that ensures the `coroutine_handle<>` passed to the wrapped awaitable is one that establishes an active `AsyncStackRoot` before resuming the real waiting coroutine.
This change extends the work in #616 to support async stack frames in `task<>` coroutines, including those that invoke `at_coroutine_exit()`. In `task<>`, when `UNIFEX_NO_ASYNC_STACKS` is falsey, the awaiter returned from `task<>`'s customization of `unifex::await_transform` stores an `AsyncStackFrame`. The awaiter pushes its frame onto the current async stack in `await_suspend()` and pops it again in `await_resume()`; since `await_resume()` is only invoked for value and error completions, this arrangement leaves it up to the waiting task to pop the awaiter's frame when the awaited task completes with done. This can be expressed as a new rule: - when a coroutine completes with a value or an error, it is responsible for popping its own `AsyncStackFrame`; but - when a coroutine completes with done, the *caller* is responsible for popping the callee's `AsyncStackFrame` as a part of the caller's `unhandled_done()` coroutine. To support this new requirement of `unhandled_done()` (that it is responsible for popping the callee's stack frame), this change introduces `popAsyncStackFrameFromCaller`, which takes the caller's stack frame by reference so that it can assert that, after popping the current async frame (whatever it is), the new top frame is the caller's frame. A `task<>` promise has an `AsyncStackFrame*` that, when it's not `nullptr`, points to the `AsyncStackFrame` in the awaiter waiting for the task. This pointer exists even when `UNIFEX_NO_ASYNC_STACKS` is truthy to help mitigate against ODR violations; linking together two TUs with `UNIFEX_NO_ASYNC_STACKS` set differently is not explicitly supported but, by ensuring this pointer always exists, some ODR problems are avoided. When a `task<>` is awaited from a TU with async stack support enabled, the awaited task's awaiter sets the promise's `AsyncStackFrame*` to point to the awaiter's frame; when a `task<>` is awaited from a TU with async stack support disabled, this assignment never happens and the promise's pointer remains null. The above description of `task<>`'s async stack maintenance only covers the recursive case of on coroutine awaiting another. The base case is handled in `connect_awaitable()`, where an `AsyncStackRoot` is set up before starting the connected awaitable. `stop_if_requested` used to model both `sender` and `awaitable` so that `co_await stop_if_requested();` could take advantage of symmetric transfer. The `stop_if_requested` sender now customizes `await_transform` to express its participation in async stack management. This means of expressing async stack awareness is unsatisfying but I don't have any better ideas right now. Lastly, `unifex::await_transform()` now wraps naturally-awaitable arguments in an `awaiter_wrapper` that ensures the `coroutine_handle<>` passed to the wrapped awaitable is one that establishes an active `AsyncStackRoot` before resuming the real waiting coroutine.
This change extends the work in #616 to support async stack frames in `task<>` coroutines, including those that invoke `at_coroutine_exit()`. In `task<>`, when `UNIFEX_NO_ASYNC_STACKS` is falsey, the awaiter returned from `task<>`'s customization of `unifex::await_transform` stores an `AsyncStackFrame`. The awaiter pushes its frame onto the current async stack in `await_suspend()` and pops it again in `await_resume()`; since `await_resume()` is only invoked for value and error completions, this arrangement leaves it up to the waiting task to pop the awaiter's frame when the awaited task completes with done. This can be expressed as a new rule: - when a coroutine completes with a value or an error, it is responsible for popping its own `AsyncStackFrame`; but - when a coroutine completes with done, the *caller* is responsible for popping the callee's `AsyncStackFrame` as a part of the caller's `unhandled_done()` coroutine. To support this new requirement of `unhandled_done()` (that it is responsible for popping the callee's stack frame), this change introduces `popAsyncStackFrameFromCaller`, which takes the caller's stack frame by reference so that it can assert that, after popping the current async frame (whatever it is), the new top frame is the caller's frame. A `task<>` promise has an `AsyncStackFrame*` that, when it's not `nullptr`, points to the `AsyncStackFrame` in the awaiter waiting for the task. This pointer exists even when `UNIFEX_NO_ASYNC_STACKS` is truthy to help mitigate against ODR violations; linking together two TUs with `UNIFEX_NO_ASYNC_STACKS` set differently is not explicitly supported but, by ensuring this pointer always exists, some ODR problems are avoided. When a `task<>` is awaited from a TU with async stack support enabled, the awaited task's awaiter sets the promise's `AsyncStackFrame*` to point to the awaiter's frame; when a `task<>` is awaited from a TU with async stack support disabled, this assignment never happens and the promise's pointer remains null. The above description of `task<>`'s async stack maintenance only covers the recursive case of on coroutine awaiting another. The base case is handled in `connect_awaitable()`, where an `AsyncStackRoot` is set up before starting the connected awaitable. `stop_if_requested` used to model both `sender` and `awaitable` so that `co_await stop_if_requested();` could take advantage of symmetric transfer. The `stop_if_requested` sender now customizes `await_transform` to express its participation in async stack management. This means of expressing async stack awareness is unsatisfying but I don't have any better ideas right now. Lastly, `unifex::await_transform()` now wraps naturally-awaitable arguments in an `awaiter_wrapper` that ensures the `coroutine_handle<>` passed to the wrapped awaitable is one that establishes an active `AsyncStackRoot` before resuming the real waiting coroutine.
This change extends the work in #616 to support async stack frames in `task<>` coroutines, including those that invoke `at_coroutine_exit()`. In `task<>`, when `UNIFEX_NO_ASYNC_STACKS` is falsey, the awaiter returned from `task<>`'s customization of `unifex::await_transform` stores an `AsyncStackFrame`. The awaiter pushes its frame onto the current async stack in `await_suspend()` and pops it again in `await_resume()`; since `await_resume()` is only invoked for value and error completions, this arrangement leaves it up to the waiting task to pop the awaiter's frame when the awaited task completes with done. This can be expressed as a new rule: - when a coroutine completes with a value or an error, it is responsible for popping its own `AsyncStackFrame`; but - when a coroutine completes with done, the *caller* is responsible for popping the callee's `AsyncStackFrame` as a part of the caller's `unhandled_done()` coroutine. To support this new requirement of `unhandled_done()` (that it is responsible for popping the callee's stack frame), this change introduces `popAsyncStackFrameFromCaller`, which takes the caller's stack frame by reference so that it can assert that, after popping the current async frame (whatever it is), the new top frame is the caller's frame. A `task<>` promise has an `AsyncStackFrame*` that, when it's not `nullptr`, points to the `AsyncStackFrame` in the awaiter waiting for the task. This pointer exists even when `UNIFEX_NO_ASYNC_STACKS` is truthy to help mitigate against ODR violations; linking together two TUs with `UNIFEX_NO_ASYNC_STACKS` set differently is not explicitly supported but, by ensuring this pointer always exists, some ODR problems are avoided. When a `task<>` is awaited from a TU with async stack support enabled, the awaited task's awaiter sets the promise's `AsyncStackFrame*` to point to the awaiter's frame; when a `task<>` is awaited from a TU with async stack support disabled, this assignment never happens and the promise's pointer remains null. The above description of `task<>`'s async stack maintenance only covers the recursive case of on coroutine awaiting another. The base case is handled in `connect_awaitable()`, where an `AsyncStackRoot` is set up before starting the connected awaitable. `stop_if_requested` used to model both `sender` and `awaitable` so that `co_await stop_if_requested();` could take advantage of symmetric transfer. The `stop_if_requested` sender now customizes `await_transform` to express its participation in async stack management. This means of expressing async stack awareness is unsatisfying but I don't have any better ideas right now. Lastly, `unifex::await_transform()` now wraps naturally-awaitable arguments in an `awaiter_wrapper` that ensures the `coroutine_handle<>` passed to the wrapped awaitable is one that establishes an active `AsyncStackRoot` before resuming the real waiting coroutine.
This change extends the work in #616 to support async stack frames in `task<>` coroutines, including those that invoke `at_coroutine_exit()`. In `task<>`, when `UNIFEX_NO_ASYNC_STACKS` is falsey, the awaiter returned from `task<>`'s customization of `unifex::await_transform` stores an `AsyncStackFrame`. The awaiter pushes its frame onto the current async stack in `await_suspend()` and pops it again in `await_resume()`; since `await_resume()` is only invoked for value and error completions, this arrangement leaves it up to the waiting task to pop the awaiter's frame when the awaited task completes with done. This can be expressed as a new rule: - when a coroutine completes with a value or an error, it is responsible for popping its own `AsyncStackFrame`; but - when a coroutine completes with done, the *caller* is responsible for popping the callee's `AsyncStackFrame` as a part of the caller's `unhandled_done()` coroutine. To support this new requirement of `unhandled_done()` (that it is responsible for popping the callee's stack frame), this change introduces `popAsyncStackFrameFromCaller`, which takes the caller's stack frame by reference so that it can assert that, after popping the current async frame (whatever it is), the new top frame is the caller's frame. A `task<>` promise has an `AsyncStackFrame*` that, when it's not `nullptr`, points to the `AsyncStackFrame` in the awaiter waiting for the task. This pointer exists even when `UNIFEX_NO_ASYNC_STACKS` is truthy to help mitigate against ODR violations; linking together two TUs with `UNIFEX_NO_ASYNC_STACKS` set differently is not explicitly supported but, by ensuring this pointer always exists, some ODR problems are avoided. When a `task<>` is awaited from a TU with async stack support enabled, the awaited task's awaiter sets the promise's `AsyncStackFrame*` to point to the awaiter's frame; when a `task<>` is awaited from a TU with async stack support disabled, this assignment never happens and the promise's pointer remains null. The above description of `task<>`'s async stack maintenance only covers the recursive case of on coroutine awaiting another. The base case is handled in `connect_awaitable()`, where an `AsyncStackRoot` is set up before starting the connected awaitable. `stop_if_requested` used to model both `sender` and `awaitable` so that `co_await stop_if_requested();` could take advantage of symmetric transfer. The `stop_if_requested` sender now customizes `await_transform` to express its participation in async stack management. This means of expressing async stack awareness is unsatisfying but I don't have any better ideas right now. Lastly, `unifex::await_transform()` now wraps naturally-awaitable arguments in an `awaiter_wrapper` that ensures the `coroutine_handle<>` passed to the wrapped awaitable is one that establishes an active `AsyncStackRoot` before resuming the real waiting coroutine.
This change extends the work in #616 to support async stack frames in `task<>` coroutines, including those that invoke `at_coroutine_exit()`. In `task<>`, when `UNIFEX_NO_ASYNC_STACKS` is falsey, the awaiter returned from `task<>`'s customization of `unifex::await_transform` stores an `AsyncStackFrame`. The awaiter pushes its frame onto the current async stack in `await_suspend()` and pops it again in `await_resume()`; since `await_resume()` is only invoked for value and error completions, this arrangement leaves it up to the waiting task to pop the awaiter's frame when the awaited task completes with done. This can be expressed as a new rule: - when a coroutine completes with a value or an error, it is responsible for popping its own `AsyncStackFrame`; but - when a coroutine completes with done, the *caller* is responsible for popping the callee's `AsyncStackFrame` as a part of the caller's `unhandled_done()` coroutine. To support this new requirement of `unhandled_done()` (that it is responsible for popping the callee's stack frame), this change introduces `popAsyncStackFrameFromCaller`, which takes the caller's stack frame by reference so that it can assert that, after popping the current async frame (whatever it is), the new top frame is the caller's frame. A `task<>` promise has an `AsyncStackFrame*` that, when it's not `nullptr`, points to the `AsyncStackFrame` in the awaiter waiting for the task. This pointer exists even when `UNIFEX_NO_ASYNC_STACKS` is truthy to help mitigate against ODR violations; linking together two TUs with `UNIFEX_NO_ASYNC_STACKS` set differently is not explicitly supported but, by ensuring this pointer always exists, some ODR problems are avoided. When a `task<>` is awaited from a TU with async stack support enabled, the awaited task's awaiter sets the promise's `AsyncStackFrame*` to point to the awaiter's frame; when a `task<>` is awaited from a TU with async stack support disabled, this assignment never happens and the promise's pointer remains null. The above description of `task<>`'s async stack maintenance only covers the recursive case of on coroutine awaiting another. The base case is handled in `connect_awaitable()`, where an `AsyncStackRoot` is set up before starting the connected awaitable. `stop_if_requested` used to model both `sender` and `awaitable` so that `co_await stop_if_requested();` could take advantage of symmetric transfer. The `stop_if_requested` sender now customizes `await_transform` to express its participation in async stack management. This means of expressing async stack awareness is unsatisfying but I don't have any better ideas right now. Lastly, `unifex::await_transform()` now wraps naturally-awaitable arguments in an `awaiter_wrapper` that ensures the `coroutine_handle<>` passed to the wrapped awaitable is one that establishes an active `AsyncStackRoot` before resuming the real waiting coroutine.
This change extends the work in #616 to support async stack frames in `task<>` coroutines, including those that invoke `at_coroutine_exit()`. In `task<>`, when `UNIFEX_NO_ASYNC_STACKS` is falsey, the awaiter returned from `task<>`'s customization of `unifex::await_transform` stores an `AsyncStackFrame`. The awaiter pushes its frame onto the current async stack in `await_suspend()` and pops it again in `await_resume()`; since `await_resume()` is only invoked for value and error completions, this arrangement leaves it up to the waiting task to pop the awaiter's frame when the awaited task completes with done. This can be expressed as a new rule: - when a coroutine completes with a value or an error, it is responsible for popping its own `AsyncStackFrame`; but - when a coroutine completes with done, the *caller* is responsible for popping the callee's `AsyncStackFrame` as a part of the caller's `unhandled_done()` coroutine. To support this new requirement of `unhandled_done()` (that it is responsible for popping the callee's stack frame), this change introduces `popAsyncStackFrameFromCaller`, which takes the caller's stack frame by reference so that it can assert that, after popping the current async frame (whatever it is), the new top frame is the caller's frame. A `task<>` promise has an `AsyncStackFrame*` that, when it's not `nullptr`, points to the `AsyncStackFrame` in the awaiter waiting for the task. This pointer exists even when `UNIFEX_NO_ASYNC_STACKS` is truthy to help mitigate against ODR violations; linking together two TUs with `UNIFEX_NO_ASYNC_STACKS` set differently is not explicitly supported but, by ensuring this pointer always exists, some ODR problems are avoided. When a `task<>` is awaited from a TU with async stack support enabled, the awaited task's awaiter sets the promise's `AsyncStackFrame*` to point to the awaiter's frame; when a `task<>` is awaited from a TU with async stack support disabled, this assignment never happens and the promise's pointer remains null. The above description of `task<>`'s async stack maintenance only covers the recursive case of on coroutine awaiting another. The base case is handled in `connect_awaitable()`, where an `AsyncStackRoot` is set up before starting the connected awaitable. `stop_if_requested` used to model both `sender` and `awaitable` so that `co_await stop_if_requested();` could take advantage of symmetric transfer. The `stop_if_requested` sender now customizes `await_transform` to express its participation in async stack management. This means of expressing async stack awareness is unsatisfying but I don't have any better ideas right now. Lastly, `unifex::await_transform()` now wraps naturally-awaitable arguments in an `awaiter_wrapper` that ensures the `coroutine_handle<>` passed to the wrapped awaitable is one that establishes an active `AsyncStackRoot` before resuming the real waiting coroutine.
This change extends the work in #616 to support async stack frames in `task<>` coroutines, including those that invoke `at_coroutine_exit()`. In `task<>`, when `UNIFEX_NO_ASYNC_STACKS` is falsey, the awaiter returned from `task<>`'s customization of `unifex::await_transform` stores an `AsyncStackFrame`. The awaiter pushes its frame onto the current async stack in `await_suspend()` and pops it again in `await_resume()`; since `await_resume()` is only invoked for value and error completions, this arrangement leaves it up to the waiting task to pop the awaiter's frame when the awaited task completes with done. This can be expressed as a new rule: - when a coroutine completes with a value or an error, it is responsible for popping its own `AsyncStackFrame`; but - when a coroutine completes with done, the *caller* is responsible for popping the callee's `AsyncStackFrame` as a part of the caller's `unhandled_done()` coroutine. To support this new requirement of `unhandled_done()` (that it is responsible for popping the callee's stack frame), this change introduces `popAsyncStackFrameFromCaller`, which takes the caller's stack frame by reference so that it can assert that, after popping the current async frame (whatever it is), the new top frame is the caller's frame. A `task<>` promise has an `AsyncStackFrame*` that, when it's not `nullptr`, points to the `AsyncStackFrame` in the awaiter waiting for the task. This pointer exists even when `UNIFEX_NO_ASYNC_STACKS` is truthy to help mitigate against ODR violations; linking together two TUs with `UNIFEX_NO_ASYNC_STACKS` set differently is not explicitly supported but, by ensuring this pointer always exists, some ODR problems are avoided. When a `task<>` is awaited from a TU with async stack support enabled, the awaited task's awaiter sets the promise's `AsyncStackFrame*` to point to the awaiter's frame; when a `task<>` is awaited from a TU with async stack support disabled, this assignment never happens and the promise's pointer remains null. The above description of `task<>`'s async stack maintenance only covers the recursive case of on coroutine awaiting another. The base case is handled in `connect_awaitable()`, where an `AsyncStackRoot` is set up before starting the connected awaitable. `stop_if_requested` used to model both `sender` and `awaitable` so that `co_await stop_if_requested();` could take advantage of symmetric transfer. The `stop_if_requested` sender now customizes `await_transform` to express its participation in async stack management. This means of expressing async stack awareness is unsatisfying but I don't have any better ideas right now. Lastly, `unifex::await_transform()` now wraps naturally-awaitable arguments in an `awaiter_wrapper` that ensures the `coroutine_handle<>` passed to the wrapped awaitable is one that establishes an active `AsyncStackRoot` before resuming the real waiting coroutine.
This change extends the work in #616 to support async stack frames in `task<>` coroutines, including those that invoke `at_coroutine_exit()`. In `task<>`, when `UNIFEX_NO_ASYNC_STACKS` is falsey, the awaiter returned from `task<>`'s customization of `unifex::await_transform` stores an `AsyncStackFrame`. The awaiter pushes its frame onto the current async stack in `await_suspend()` and pops it again in `await_resume()`; since `await_resume()` is only invoked for value and error completions, this arrangement leaves it up to the waiting task to pop the awaiter's frame when the awaited task completes with done. This can be expressed as a new rule: - when a coroutine completes with a value or an error, it is responsible for popping its own `AsyncStackFrame`; but - when a coroutine completes with done, the *caller* is responsible for popping the callee's `AsyncStackFrame` as a part of the caller's `unhandled_done()` coroutine. To support this new requirement of `unhandled_done()` (that it is responsible for popping the callee's stack frame), this change introduces `popAsyncStackFrameFromCaller`, which takes the caller's stack frame by reference so that it can assert that, after popping the current async frame (whatever it is), the new top frame is the caller's frame. A `task<>` promise has an `AsyncStackFrame*` that, when it's not `nullptr`, points to the `AsyncStackFrame` in the awaiter waiting for the task. This pointer exists even when `UNIFEX_NO_ASYNC_STACKS` is truthy to help mitigate against ODR violations; linking together two TUs with `UNIFEX_NO_ASYNC_STACKS` set differently is not explicitly supported but, by ensuring this pointer always exists, some ODR problems are avoided. When a `task<>` is awaited from a TU with async stack support enabled, the awaited task's awaiter sets the promise's `AsyncStackFrame*` to point to the awaiter's frame; when a `task<>` is awaited from a TU with async stack support disabled, this assignment never happens and the promise's pointer remains null. The above description of `task<>`'s async stack maintenance only covers the recursive case of on coroutine awaiting another. The base case is handled in `connect_awaitable()`, where an `AsyncStackRoot` is set up before starting the connected awaitable. `stop_if_requested` used to model both `sender` and `awaitable` so that `co_await stop_if_requested();` could take advantage of symmetric transfer. The `stop_if_requested` sender now customizes `await_transform` to express its participation in async stack management. This means of expressing async stack awareness is unsatisfying but I don't have any better ideas right now. Lastly, `unifex::await_transform()` now wraps naturally-awaitable arguments in an `awaiter_wrapper` that ensures the `coroutine_handle<>` passed to the wrapped awaitable is one that establishes an active `AsyncStackRoot` before resuming the real waiting coroutine.
This change extends the work in #616 to support async stack frames in `task<>` coroutines, including those that invoke `at_coroutine_exit()`. In `task<>`, when `UNIFEX_NO_ASYNC_STACKS` is falsey, the awaiter returned from `task<>`'s customization of `unifex::await_transform` stores an `AsyncStackFrame`. The awaiter pushes its frame onto the current async stack in `await_suspend()` and pops it again in `await_resume()`; since `await_resume()` is only invoked for value and error completions, this arrangement leaves it up to the waiting task to pop the awaiter's frame when the awaited task completes with done. This can be expressed as a new rule: - when a coroutine completes with a value or an error, it is responsible for popping its own `AsyncStackFrame`; but - when a coroutine completes with done, the *caller* is responsible for popping the callee's `AsyncStackFrame` as a part of the caller's `unhandled_done()` coroutine. To support this new requirement of `unhandled_done()` (that it is responsible for popping the callee's stack frame), this change introduces `popAsyncStackFrameFromCaller`, which takes the caller's stack frame by reference so that it can assert that, after popping the current async frame (whatever it is), the new top frame is the caller's frame. A `task<>` promise has an `AsyncStackFrame*` that, when it's not `nullptr`, points to the `AsyncStackFrame` in the awaiter waiting for the task. This pointer exists even when `UNIFEX_NO_ASYNC_STACKS` is truthy to help mitigate against ODR violations; linking together two TUs with `UNIFEX_NO_ASYNC_STACKS` set differently is not explicitly supported but, by ensuring this pointer always exists, some ODR problems are avoided. When a `task<>` is awaited from a TU with async stack support enabled, the awaited task's awaiter sets the promise's `AsyncStackFrame*` to point to the awaiter's frame; when a `task<>` is awaited from a TU with async stack support disabled, this assignment never happens and the promise's pointer remains null. The above description of `task<>`'s async stack maintenance only covers the recursive case of on coroutine awaiting another. The base case is handled in `connect_awaitable()`, where an `AsyncStackRoot` is set up before starting the connected awaitable. `stop_if_requested` used to model both `sender` and `awaitable` so that `co_await stop_if_requested();` could take advantage of symmetric transfer. The `stop_if_requested` sender now customizes `await_transform` to express its participation in async stack management. This means of expressing async stack awareness is unsatisfying but I don't have any better ideas right now. Lastly, `unifex::await_transform()` now wraps naturally-awaitable arguments in an `awaiter_wrapper` that ensures the `coroutine_handle<>` passed to the wrapped awaitable is one that establishes an active `AsyncStackRoot` before resuming the real waiting coroutine.
This change extends the work in #616 to support async stack frames in `task<>` coroutines, including those that invoke `at_coroutine_exit()`. In `task<>`, when `UNIFEX_NO_ASYNC_STACKS` is falsey, the awaiter returned from `task<>`'s customization of `unifex::await_transform` stores an `AsyncStackFrame`. The awaiter pushes its frame onto the current async stack in `await_suspend()` and pops it again in `await_resume()`; since `await_resume()` is only invoked for value and error completions, this arrangement leaves it up to the waiting task to pop the awaiter's frame when the awaited task completes with done. This can be expressed as a new rule: - when a coroutine completes with a value or an error, it is responsible for popping its own `AsyncStackFrame`; but - when a coroutine completes with done, the *caller* is responsible for popping the callee's `AsyncStackFrame` as a part of the caller's `unhandled_done()` coroutine. To support this new requirement of `unhandled_done()` (that it is responsible for popping the callee's stack frame), this change introduces `popAsyncStackFrameFromCaller`, which takes the caller's stack frame by reference so that it can assert that, after popping the current async frame (whatever it is), the new top frame is the caller's frame. A `task<>` promise has an `AsyncStackFrame*` that, when it's not `nullptr`, points to the `AsyncStackFrame` in the awaiter waiting for the task. This pointer exists even when `UNIFEX_NO_ASYNC_STACKS` is truthy to help mitigate against ODR violations; linking together two TUs with `UNIFEX_NO_ASYNC_STACKS` set differently is not explicitly supported but, by ensuring this pointer always exists, some ODR problems are avoided. When a `task<>` is awaited from a TU with async stack support enabled, the awaited task's awaiter sets the promise's `AsyncStackFrame*` to point to the awaiter's frame; when a `task<>` is awaited from a TU with async stack support disabled, this assignment never happens and the promise's pointer remains null. The above description of `task<>`'s async stack maintenance only covers the recursive case of on coroutine awaiting another. The base case is handled in `connect_awaitable()`, where an `AsyncStackRoot` is set up before starting the connected awaitable. `stop_if_requested` used to model both `sender` and `awaitable` so that `co_await stop_if_requested();` could take advantage of symmetric transfer. The `stop_if_requested` sender now customizes `await_transform` to express its participation in async stack management. This means of expressing async stack awareness is unsatisfying but I don't have any better ideas right now. Lastly, `unifex::await_transform()` now wraps naturally-awaitable arguments in an `awaiter_wrapper` that ensures the `coroutine_handle<>` passed to the wrapped awaitable is one that establishes an active `AsyncStackRoot` before resuming the real waiting coroutine.
This change extends the work in #616 to support async stack frames in `task<>` coroutines, including those that invoke `at_coroutine_exit()`. In `task<>`, when `UNIFEX_NO_ASYNC_STACKS` is falsey, the awaiter returned from `task<>`'s customization of `unifex::await_transform` stores an `AsyncStackFrame`. The awaiter pushes its frame onto the current async stack in `await_suspend()` and pops it again in `await_resume()`; since `await_resume()` is only invoked for value and error completions, this arrangement leaves it up to the waiting task to pop the awaiter's frame when the awaited task completes with done. This can be expressed as a new rule: - when a coroutine completes with a value or an error, it is responsible for popping its own `AsyncStackFrame`; but - when a coroutine completes with done, the *caller* is responsible for popping the callee's `AsyncStackFrame` as a part of the caller's `unhandled_done()` coroutine. To support this new requirement of `unhandled_done()` (that it is responsible for popping the callee's stack frame), this change introduces `popAsyncStackFrameFromCaller`, which takes the caller's stack frame by reference so that it can assert that, after popping the current async frame (whatever it is), the new top frame is the caller's frame. A `task<>` promise has an `AsyncStackFrame*` that, when it's not `nullptr`, points to the `AsyncStackFrame` in the awaiter waiting for the task. This pointer exists even when `UNIFEX_NO_ASYNC_STACKS` is truthy to help mitigate against ODR violations; linking together two TUs with `UNIFEX_NO_ASYNC_STACKS` set differently is not explicitly supported but, by ensuring this pointer always exists, some ODR problems are avoided. When a `task<>` is awaited from a TU with async stack support enabled, the awaited task's awaiter sets the promise's `AsyncStackFrame*` to point to the awaiter's frame; when a `task<>` is awaited from a TU with async stack support disabled, this assignment never happens and the promise's pointer remains null. The above description of `task<>`'s async stack maintenance only covers the recursive case of on coroutine awaiting another. The base case is handled in `connect_awaitable()`, where an `AsyncStackRoot` is set up before starting the connected awaitable. `stop_if_requested` used to model both `sender` and `awaitable` so that `co_await stop_if_requested();` could take advantage of symmetric transfer. The `stop_if_requested` sender now customizes `await_transform` to express its participation in async stack management. This means of expressing async stack awareness is unsatisfying but I don't have any better ideas right now. Lastly, `unifex::await_transform()` now wraps naturally-awaitable arguments in an `awaiter_wrapper` that ensures the `coroutine_handle<>` passed to the wrapped awaitable is one that establishes an active `AsyncStackRoot` before resuming the real waiting coroutine.
This change extends the work in #616 to support async stack frames in `task<>` coroutines, including those that invoke `at_coroutine_exit()`. In `task<>`, when `UNIFEX_NO_ASYNC_STACKS` is falsey, the awaiter returned from `task<>`'s customization of `unifex::await_transform` stores an `AsyncStackFrame`. The awaiter pushes its frame onto the current async stack in `await_suspend()` and pops it again in `await_resume()`; since `await_resume()` is only invoked for value and error completions, this arrangement leaves it up to the waiting task to pop the awaiter's frame when the awaited task completes with done. This can be expressed as a new rule: - when a coroutine completes with a value or an error, it is responsible for popping its own `AsyncStackFrame`; but - when a coroutine completes with done, the *caller* is responsible for popping the callee's `AsyncStackFrame` as a part of the caller's `unhandled_done()` coroutine. To support this new requirement of `unhandled_done()` (that it is responsible for popping the callee's stack frame), this change introduces `popAsyncStackFrameFromCaller`, which takes the caller's stack frame by reference so that it can assert that, after popping the current async frame (whatever it is), the new top frame is the caller's frame. A `task<>` promise has an `AsyncStackFrame*` that, when it's not `nullptr`, points to the `AsyncStackFrame` in the awaiter waiting for the task. This pointer exists even when `UNIFEX_NO_ASYNC_STACKS` is truthy to help mitigate against ODR violations; linking together two TUs with `UNIFEX_NO_ASYNC_STACKS` set differently is not explicitly supported but, by ensuring this pointer always exists, some ODR problems are avoided. When a `task<>` is awaited from a TU with async stack support enabled, the awaited task's awaiter sets the promise's `AsyncStackFrame*` to point to the awaiter's frame; when a `task<>` is awaited from a TU with async stack support disabled, this assignment never happens and the promise's pointer remains null. The above description of `task<>`'s async stack maintenance only covers the recursive case of on coroutine awaiting another. The base case is handled in `connect_awaitable()`, where an `AsyncStackRoot` is set up before starting the connected awaitable. `stop_if_requested` used to model both `sender` and `awaitable` so that `co_await stop_if_requested();` could take advantage of symmetric transfer. The `stop_if_requested` sender now customizes `await_transform` to express its participation in async stack management. This means of expressing async stack awareness is unsatisfying but I don't have any better ideas right now. Lastly, `unifex::await_transform()` now wraps naturally-awaitable arguments in an `awaiter_wrapper` that ensures the `coroutine_handle<>` passed to the wrapped awaitable is one that establishes an active `AsyncStackRoot` before resuming the real waiting coroutine.
This PR copies the core of Folly's async stack trace support into
include/unifex/tracing
and builds on it to add support for generalized Senders.When
UNIFEX_NO_ASYNC_STACKS
is falsey,unifex::connect
returns a wrapped operation state that injects async stack tracing into the operation tree.AsyncStackFrame
for the wrapped operation; andunifex::start
we:AsyncStackRoot
on the stack;AsyncStackFrame
onto the current async stack;AsyncStackFrame
on the currentAsyncStackRoot
; andAsyncStackRoot
on the stack;AsyncStackFrame
to the stack;AsyncStackFrame
on the currentAsyncStackRoot
; andThe effect is that we build up a linked list (technically a DAG) of
AsyncStackFrame
s pointing "up" toward the start of the operation asunifex::start
recurses into the nested operation state and then unwind it on the way back out as the receiver completion methods are invoked. At any given time, the current thread'sAsyncStackRoot
is sitting on the most recently-activated "normal" stack frame that is participating in async stack management, allowing Folly'sco_bt.py
debugger extension to figure out when it should stop walking normal stack frames and start walking async stack frames.As alluded to above, the behaviour of the async stack tracing machinery is controlled by the
UNIFEX_NO_ASYNC_STACKS
preprocessor macro. If it's truthy, async stacks are not traced; if it's falsey, they are traced. The default inunifex/config.hpp
is to enable async stack tracing in non-Windows debug builds.any_sender_of<>
builds on Windows (both Clang and MSVC); the resolution is to land PR Make any_sender_of<> play nicer with MSVC #619, but that PR breaks an internal Meta build so I'll have to come back to it.UNIFEX_NO_ASYNC_STACKS=0
in your release build script if the extra debuggability is worth the extra binary size in production.This iteration is an MVP:
unifex::_get_return_address::default_return_address<T>()
, whereT
is the type of the senderFutures PRs will:
Co-authored-by: Ján Ondrušek [email protected]
Co-authored-by: Jessica Wong [email protected]
Co-authored-by: Deniz Evrenci [email protected]