diff --git a/examples/dump.rs b/examples/dump.rs index c7358489933..4d8ff19c065 100644 --- a/examples/dump.rs +++ b/examples/dump.rs @@ -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, @@ -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> { + use std::sync::Arc; + use tokio::sync::Barrier; #[inline(never)] - async fn a() { - black_box(b()).await + async fn a(barrier: Arc) { + b(barrier).await } #[inline(never)] - async fn b() { - black_box(c()).await + async fn b(barrier: Arc) { + c(barrier).await } #[inline(never)] - async fn c() { - loop { - tokio::task::yield_now().await; - } + async fn c(barrier: Arc) { + 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 = 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( diff --git a/tokio/src/runtime/dump.rs b/tokio/src/runtime/dump.rs index 846839e4de5..994b7f9c015 100644 --- a/tokio/src/runtime/dump.rs +++ b/tokio/src/runtime/dump.rs @@ -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, } /// 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, diff --git a/tokio/src/runtime/handle.rs b/tokio/src/runtime/handle.rs index 0951d8a3736..02ba279ff2d 100644 --- a/tokio/src/runtime/handle.rs +++ b/tokio/src/runtime/handle.rs @@ -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(),