SeaQL / FireDBG.for.Rust

🔥 Time Travel Visual Debugger for Rust
https://firedbg.sea-ql.org
MIT License
1.2k stars 21 forks source link

Async Rust support #37

Open tyt2y3 opened 5 months ago

tyt2y3 commented 5 months ago

How can we support debugging async Rust? As it is basically impossible to use a step debugger.

First off, the background. Async Rust has a runtime. The runtime manages a number of async tasks, called Future. Each Future is a state machine (the complex part is it is hierarchical), and it can only progress by having someone polling it. From an instruction point of view, each async function is a block of code ("closure") that can be reentered an unknown number of times. There is a context associated with this future, and our goal is to track the lifecycle of each Future, from create to drop, and every time it makes progress. Every await point is a yield point, meaning the closure returns, and the state machine may or may not have a state transition.

Static analysis already tells us whether a function is async. (it is actually quite tricky to derive this from looking at the assembly).

I think we can model the event stream in FireDBG just like regular functions, but with an additional async context pointer. At any given point in time, the pointer should uniquely identifies a Future. But we also need to hook into the Future lifecycle and record the "async context create" and "async context destroy" events. The Pin semantic ensures that once a Future is being polled, it stays in place in memory. Then we should have enough information to reconstruct an async timeline. Hierarchical async functions shares the same async context, so they can be uniquely identified by (async context, function address).

Constructing a call tree requires parent-child relation. We need to identify the "true" parent of an async function on runtime.

async fn func_a() {
    future::all([func_b, func_c]).await;
}

We have to be aware of b, c both being children of a, instead of a -> b -> c if we naively look at the sequence of events. At least in the above case, we can reconstruct the async call stack by looking at the real stack trace:

#0 func_b/c
#1 poll*
#2 tokio
#3 tokio
#4 func_a
#5 poll*
...
#9 main

It should be doable to capture the parameters of an async function on first call, but I couldn't think of a way to capture the return value (yet).

Reference: https://fitzgeraldnick.com/2019/08/27/async-stacks-in-rust.html

tyt2y3 commented 2 months ago

Screenshot 2024-04-20 at 4 58 12 PM

Milo123459 commented 2 months ago

Woo, awesome work! Does this mean we are getting closer to seeing this in a release version of FireDBG?