Skip to content

Commit cb18b0a

Browse files
authored
tokio: improve task dump documentation (#5778)
Adds depth to the taskdump example, and documentation to Handle::dump.
1 parent 7ccd3e0 commit cb18b0a

File tree

3 files changed

+135
-23
lines changed

3 files changed

+135
-23
lines changed

examples/dump.rs

+54-22
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
//! This example demonstrates tokio's experimental taskdumping functionality.
1+
//! This example demonstrates tokio's experimental task dumping functionality.
2+
//! This application deadlocks. Input CTRL+C to display traces of each task, or
3+
//! input CTRL+C twice within 1 second to quit.
24
35
#[cfg(all(
46
tokio_unstable,
@@ -7,44 +9,74 @@
79
any(target_arch = "aarch64", target_arch = "x86", target_arch = "x86_64")
810
))]
911
#[tokio::main]
10-
async fn main() {
11-
use std::hint::black_box;
12+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
13+
use std::sync::Arc;
14+
use tokio::sync::Barrier;
1215

1316
#[inline(never)]
14-
async fn a() {
15-
black_box(b()).await
17+
async fn a(barrier: Arc<Barrier>) {
18+
b(barrier).await
1619
}
1720

1821
#[inline(never)]
19-
async fn b() {
20-
black_box(c()).await
22+
async fn b(barrier: Arc<Barrier>) {
23+
c(barrier).await
2124
}
2225

2326
#[inline(never)]
24-
async fn c() {
25-
loop {
26-
tokio::task::yield_now().await;
27-
}
27+
async fn c(barrier: Arc<Barrier>) {
28+
barrier.wait().await;
2829
}
2930

30-
async fn dump() {
31+
// Prints a task dump upon receipt of CTRL+C, or returns if CTRL+C is
32+
// inputted twice within a second.
33+
async fn dump_or_quit() {
34+
use tokio::time::{timeout, Duration, Instant};
3135
let handle = tokio::runtime::Handle::current();
32-
let dump = handle.dump().await;
36+
let mut last_signal: Option<Instant> = None;
37+
// wait for CTRL+C
38+
while let Ok(_) = tokio::signal::ctrl_c().await {
39+
// exit if a CTRL+C is inputted twice within 1 second
40+
if let Some(time_since_last_signal) = last_signal.map(|i| i.elapsed()) {
41+
if time_since_last_signal < Duration::from_secs(1) {
42+
return;
43+
}
44+
}
45+
last_signal = Some(Instant::now());
3346

34-
for (i, task) in dump.tasks().iter().enumerate() {
35-
let trace = task.trace();
36-
println!("task {i} trace:");
37-
println!("{trace}\n");
47+
// capture a dump, and print each trace
48+
println!("{:-<80}", "");
49+
if let Ok(dump) = timeout(Duration::from_secs(2), handle.dump()).await {
50+
for (i, task) in dump.tasks().iter().enumerate() {
51+
let trace = task.trace();
52+
println!("TASK {i}:");
53+
println!("{trace}\n");
54+
}
55+
} else {
56+
println!("Task dumping timed out. Use a native debugger (like gdb) to debug the deadlock.");
57+
}
58+
println!("{:-<80}", "");
59+
println!("Input CTRL+C twice within 1 second to exit.");
3860
}
3961
}
4062

63+
println!("This program has a deadlock.");
64+
println!("Input CTRL+C to print a task dump.");
65+
println!("Input CTRL+C twice within 1 second to exit.");
66+
67+
// oops! this barrier waits for one more task than will ever come.
68+
let barrier = Arc::new(Barrier::new(3));
69+
70+
let task_1 = tokio::spawn(a(barrier.clone()));
71+
let task_2 = tokio::spawn(a(barrier));
72+
4173
tokio::select!(
42-
biased;
43-
_ = tokio::spawn(a()) => {},
44-
_ = tokio::spawn(b()) => {},
45-
_ = tokio::spawn(c()) => {},
46-
_ = dump() => {},
74+
_ = dump_or_quit() => {},
75+
_ = task_1 => {},
76+
_ = task_2 => {},
4777
);
78+
79+
Ok(())
4880
}
4981

5082
#[cfg(not(all(

tokio/src/runtime/dump.rs

+10
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,36 @@
11
//! Snapshots of runtime state.
2+
//!
3+
//! See [Handle::dump][crate::runtime::Handle::dump].
24
35
use std::fmt;
46

57
/// A snapshot of a runtime's state.
8+
///
9+
/// See [Handle::dump][crate::runtime::Handle::dump].
610
#[derive(Debug)]
711
pub struct Dump {
812
tasks: Tasks,
913
}
1014

1115
/// Snapshots of tasks.
16+
///
17+
/// See [Handle::dump][crate::runtime::Handle::dump].
1218
#[derive(Debug)]
1319
pub struct Tasks {
1420
tasks: Vec<Task>,
1521
}
1622

1723
/// A snapshot of a task.
24+
///
25+
/// See [Handle::dump][crate::runtime::Handle::dump].
1826
#[derive(Debug)]
1927
pub struct Task {
2028
trace: Trace,
2129
}
2230

2331
/// An execution trace of a task's last poll.
32+
///
33+
/// See [Handle::dump][crate::runtime::Handle::dump].
2434
#[derive(Debug)]
2535
pub struct Trace {
2636
inner: super::task::trace::Trace,

tokio/src/runtime/handle.rs

+71-1
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,77 @@ cfg_metrics! {
373373

374374
cfg_taskdump! {
375375
impl Handle {
376-
/// Capture a snapshot of this runtime's state.
376+
/// Captures a snapshot of the runtime's state.
377+
///
378+
/// This functionality is experimental, and comes with a number of
379+
/// requirements and limitations.
380+
///
381+
/// # Examples
382+
///
383+
/// This can be used to get call traces of each task in the runtime.
384+
/// Calls to `Handle::dump` should usually be enclosed in a
385+
/// [timeout][crate::time::timeout], so that dumping does not escalate a
386+
/// single blocked runtime thread into an entirely blocked runtime.
387+
///
388+
/// ```
389+
/// # use tokio::runtime::Runtime;
390+
/// # fn dox() {
391+
/// # let rt = Runtime::new().unwrap();
392+
/// # rt.spawn(async {
393+
/// use tokio::runtime::Handle;
394+
/// use tokio::time::{timeout, Duration};
395+
///
396+
/// // Inside an async block or function.
397+
/// let handle = Handle::current();
398+
/// if let Ok(dump) = timeout(Duration::from_secs(2), handle.dump()).await {
399+
/// for (i, task) in dump.tasks().iter().enumerate() {
400+
/// let trace = task.trace();
401+
/// println!("TASK {i}:");
402+
/// println!("{trace}\n");
403+
/// }
404+
/// }
405+
/// # });
406+
/// # }
407+
/// ```
408+
///
409+
/// # Requirements
410+
///
411+
/// ## Debug Info Must Be Available
412+
/// To produce task traces, the application must **not** be compiled
413+
/// with split debuginfo. On Linux, including debuginfo within the
414+
/// application binary is the (correct) default. You can further ensure
415+
/// this behavior with the following directive in your `Cargo.toml`:
416+
///
417+
/// ```toml
418+
/// [profile.*]
419+
/// split-debuginfo = "off"
420+
/// ```
421+
///
422+
/// ## Platform Requirements
423+
///
424+
/// Task dumps are supported on Linux atop x86 and x86_64.
425+
///
426+
/// ## Current Thread Runtime Requirements
427+
///
428+
/// On the `current_thread` runtime, task dumps may only be requested
429+
/// from *within* the context of the runtime being dumped. Do not, for
430+
/// example, await `Handle::dump()` on a different runtime.
431+
///
432+
/// # Limitations
433+
///
434+
/// ## Local Executors
435+
///
436+
/// Tasks managed by local executors (e.g., `FuturesUnordered` and
437+
/// [`LocalSet`][crate::task::LocalSet]) may not appear in task dumps.
438+
///
439+
/// ## Non-Termination When Workers Are Blocked
440+
///
441+
/// The future produced by `Handle::dump` may never produce `Ready` if
442+
/// another runtime worker is blocked for more than 250ms. This may
443+
/// occur if a dump is requested during shutdown, or if another runtime
444+
/// worker is infinite looping or synchronously deadlocked. For these
445+
/// reasons, task dumping should usually be paired with an explicit
446+
/// [timeout][crate::time::timeout].
377447
pub async fn dump(&self) -> crate::runtime::Dump {
378448
match &self.inner {
379449
scheduler::Handle::CurrentThread(handle) => handle.dump(),

0 commit comments

Comments
 (0)