Announcing async-backtrace

October 27, 2022

Today, we are happy to announce the initial release of async-backtrace, a crate that enables you to efficiently track and view the state of asynchronous tasks in your application.

In synchronous, multi-threaded applications, you can investigate deadlocks by inspecting stack traces of all running threads. Unfortunately, this approach breaks down for most asynchronous Rust applications, since suspended tasks — tasks that are not actively being polled — are invisible to traditional stack traces. The async-backtrace crate fills this gap, allowing you see the state of these hidden tasks. It is architected to be highly efficient without configuration, and is suitable for deployment in production environments.

This crate complements (but is not yet integrated with) the tracing library and tokio-console. Use async-backtrace for a birds-eye view of task state in your application, and tracing for isolating the inputs that lead to that state. If your application uses the tokio runtime, you can use tokio-console for a deeper look into your application's interactions with tokio's synchronization primitives.

Getting Started

To use async-backtrace, first add the crate to your Cargo.toml file:

[dependencies]
async-backtrace = "0.2"

Then, to include your async fns in async task traces, simply annotate them with #[async_backtrace::framed] and call taskdump_tree to receive pretty-printed trees of your application's tasks. For instance:

#[tokio::main(flavor = "current_thread")]
async fn main() {
    tokio::select! {
        // run the following branches in order of their appearance
        biased;

        // spawn task #1
        _ = tokio::spawn(foo()) => { unreachable!() }

        // spawn task #2
        _ = tokio::spawn(foo()) => { unreachable!() }

        // print the running tasks
        _ = tokio::spawn(async {}) => {
            println!("{}", async_backtrace::taskdump_tree(true));
        }
    };
}

#[async_backtrace::framed]
async fn foo() {
    bar().await;
}

#[async_backtrace::framed]
async fn bar() {
    baz().await;
}

#[async_backtrace::framed]
async fn baz() {
    std::future::pending::<()>().await
}

Running the above example prints the trees:

╼ multiple::foo::{{closure}} at backtrace/examples/multiple.rs:22:1
  └╼ multiple::bar::{{closure}} at backtrace/examples/multiple.rs:27:1
     └╼ multiple::baz::{{closure}} at backtrace/examples/multiple.rs:32:1
╼ multiple::foo::{{closure}} at backtrace/examples/multiple.rs:22:1
  └╼ multiple::bar::{{closure}} at backtrace/examples/multiple.rs:27:1
     └╼ multiple::baz::{{closure}} at backtrace/examples/multiple.rs:32:1

See here for more examples!

Feedback Welcome

This launch is only an initial release. Work on async-backtrace has just begun. To guide our development, we need your feedback. So, give it a shot, and let us know how it goes. Please file issues and ping us on Discord.

— Jack Wrenn (@jswrenn)