Skip to content

Commit

Permalink
tokio: improve task dump documentation (#5778)
Browse files Browse the repository at this point in the history
Adds depth to the taskdump example, and documentation to Handle::dump.
  • Loading branch information
jswrenn authored Jun 10, 2023
1 parent 7ccd3e0 commit cb18b0a
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 23 deletions.
76 changes: 54 additions & 22 deletions examples/dump.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
//! This example demonstrates tokio's experimental taskdumping functionality.
//! This example demonstrates tokio's experimental task dumping functionality.
//! This application deadlocks. Input CTRL+C to display traces of each task, or
//! input CTRL+C twice within 1 second to quit.
#[cfg(all(
tokio_unstable,
Expand All @@ -7,44 +9,74 @@
any(target_arch = "aarch64", target_arch = "x86", target_arch = "x86_64")
))]
#[tokio::main]
async fn main() {
use std::hint::black_box;
async fn main() -> Result<(), Box<dyn std::error::Error>> {
use std::sync::Arc;
use tokio::sync::Barrier;

#[inline(never)]
async fn a() {
black_box(b()).await
async fn a(barrier: Arc<Barrier>) {
b(barrier).await
}

#[inline(never)]
async fn b() {
black_box(c()).await
async fn b(barrier: Arc<Barrier>) {
c(barrier).await
}

#[inline(never)]
async fn c() {
loop {
tokio::task::yield_now().await;
}
async fn c(barrier: Arc<Barrier>) {
barrier.wait().await;
}

async fn dump() {
// Prints a task dump upon receipt of CTRL+C, or returns if CTRL+C is
// inputted twice within a second.
async fn dump_or_quit() {
use tokio::time::{timeout, Duration, Instant};
let handle = tokio::runtime::Handle::current();
let dump = handle.dump().await;
let mut last_signal: Option<Instant> = None;
// wait for CTRL+C
while let Ok(_) = tokio::signal::ctrl_c().await {
// exit if a CTRL+C is inputted twice within 1 second
if let Some(time_since_last_signal) = last_signal.map(|i| i.elapsed()) {
if time_since_last_signal < Duration::from_secs(1) {
return;
}
}
last_signal = Some(Instant::now());

for (i, task) in dump.tasks().iter().enumerate() {
let trace = task.trace();
println!("task {i} trace:");
println!("{trace}\n");
// capture a dump, and print each trace
println!("{:-<80}", "");
if let Ok(dump) = timeout(Duration::from_secs(2), handle.dump()).await {
for (i, task) in dump.tasks().iter().enumerate() {
let trace = task.trace();
println!("TASK {i}:");
println!("{trace}\n");
}
} else {
println!("Task dumping timed out. Use a native debugger (like gdb) to debug the deadlock.");
}
println!("{:-<80}", "");
println!("Input CTRL+C twice within 1 second to exit.");
}
}

println!("This program has a deadlock.");
println!("Input CTRL+C to print a task dump.");
println!("Input CTRL+C twice within 1 second to exit.");

// oops! this barrier waits for one more task than will ever come.
let barrier = Arc::new(Barrier::new(3));

let task_1 = tokio::spawn(a(barrier.clone()));
let task_2 = tokio::spawn(a(barrier));

tokio::select!(
biased;
_ = tokio::spawn(a()) => {},
_ = tokio::spawn(b()) => {},
_ = tokio::spawn(c()) => {},
_ = dump() => {},
_ = dump_or_quit() => {},
_ = task_1 => {},
_ = task_2 => {},
);

Ok(())
}

#[cfg(not(all(
Expand Down
10 changes: 10 additions & 0 deletions tokio/src/runtime/dump.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
//! Snapshots of runtime state.
//!
//! See [Handle::dump][crate::runtime::Handle::dump].
use std::fmt;

/// A snapshot of a runtime's state.
///
/// See [Handle::dump][crate::runtime::Handle::dump].
#[derive(Debug)]
pub struct Dump {
tasks: Tasks,
}

/// Snapshots of tasks.
///
/// See [Handle::dump][crate::runtime::Handle::dump].
#[derive(Debug)]
pub struct Tasks {
tasks: Vec<Task>,
}

/// A snapshot of a task.
///
/// See [Handle::dump][crate::runtime::Handle::dump].
#[derive(Debug)]
pub struct Task {
trace: Trace,
}

/// An execution trace of a task's last poll.
///
/// See [Handle::dump][crate::runtime::Handle::dump].
#[derive(Debug)]
pub struct Trace {
inner: super::task::trace::Trace,
Expand Down
72 changes: 71 additions & 1 deletion tokio/src/runtime/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,77 @@ cfg_metrics! {

cfg_taskdump! {
impl Handle {
/// Capture a snapshot of this runtime's state.
/// Captures a snapshot of the runtime's state.
///
/// This functionality is experimental, and comes with a number of
/// requirements and limitations.
///
/// # Examples
///
/// This can be used to get call traces of each task in the runtime.
/// Calls to `Handle::dump` should usually be enclosed in a
/// [timeout][crate::time::timeout], so that dumping does not escalate a
/// single blocked runtime thread into an entirely blocked runtime.
///
/// ```
/// # use tokio::runtime::Runtime;
/// # fn dox() {
/// # let rt = Runtime::new().unwrap();
/// # rt.spawn(async {
/// use tokio::runtime::Handle;
/// use tokio::time::{timeout, Duration};
///
/// // Inside an async block or function.
/// let handle = Handle::current();
/// if let Ok(dump) = timeout(Duration::from_secs(2), handle.dump()).await {
/// for (i, task) in dump.tasks().iter().enumerate() {
/// let trace = task.trace();
/// println!("TASK {i}:");
/// println!("{trace}\n");
/// }
/// }
/// # });
/// # }
/// ```
///
/// # Requirements
///
/// ## Debug Info Must Be Available
/// To produce task traces, the application must **not** be compiled
/// with split debuginfo. On Linux, including debuginfo within the
/// application binary is the (correct) default. You can further ensure
/// this behavior with the following directive in your `Cargo.toml`:
///
/// ```toml
/// [profile.*]
/// split-debuginfo = "off"
/// ```
///
/// ## Platform Requirements
///
/// Task dumps are supported on Linux atop x86 and x86_64.
///
/// ## Current Thread Runtime Requirements
///
/// On the `current_thread` runtime, task dumps may only be requested
/// from *within* the context of the runtime being dumped. Do not, for
/// example, await `Handle::dump()` on a different runtime.
///
/// # Limitations
///
/// ## Local Executors
///
/// Tasks managed by local executors (e.g., `FuturesUnordered` and
/// [`LocalSet`][crate::task::LocalSet]) may not appear in task dumps.
///
/// ## Non-Termination When Workers Are Blocked
///
/// The future produced by `Handle::dump` may never produce `Ready` if
/// another runtime worker is blocked for more than 250ms. This may
/// occur if a dump is requested during shutdown, or if another runtime
/// worker is infinite looping or synchronously deadlocked. For these
/// reasons, task dumping should usually be paired with an explicit
/// [timeout][crate::time::timeout].
pub async fn dump(&self) -> crate::runtime::Dump {
match &self.inner {
scheduler::Handle::CurrentThread(handle) => handle.dump(),
Expand Down

0 comments on commit cb18b0a

Please sign in to comment.