Skip to content

Commit b9caa5f

Browse files
committed
tokio: introduce Handle::dump and impl for current-thread runtime
Task dumps are snapshots of runtime state. Taskdumps are collected by instrumenting Tokio's leaves to conditionally collect backtraces, which are then coalesced per-task into execution tree traces. This initial implementation only supports collecting taskdumps from within the context of a current-thread runtime, and only `yield_now()` is instrumented.
1 parent 3c403d6 commit b9caa5f

20 files changed

+751
-7
lines changed

examples/Cargo.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ edition = "2018"
77
# If you copy one of the examples into a new project, you should be using
88
# [dependencies] instead, and delete the **path**.
99
[dev-dependencies]
10-
tokio = { version = "1.0.0", path = "../tokio", features = ["full", "tracing"] }
10+
tokio = { version = "1.0.0", path = "../tokio", features = ["full", "tracing", "taskdump"] }
1111
tokio-util = { version = "0.7.0", path = "../tokio-util", features = ["full"] }
1212
tokio-stream = { version = "0.1", path = "../tokio-stream" }
1313

@@ -90,3 +90,7 @@ path = "named-pipe-ready.rs"
9090
[[example]]
9191
name = "named-pipe-multi-client"
9292
path = "named-pipe-multi-client.rs"
93+
94+
[[example]]
95+
name = "dump"
96+
path = "dump.rs"

examples/dump.rs

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//! This example demonstrates tokio's experimental taskdumping functionality.
2+
3+
use std::hint::black_box;
4+
5+
#[inline(never)]
6+
async fn a() {
7+
black_box(b()).await
8+
}
9+
10+
#[inline(never)]
11+
async fn b() {
12+
black_box(c()).await
13+
}
14+
15+
#[inline(never)]
16+
async fn c() {
17+
black_box(tokio::task::yield_now()).await
18+
}
19+
20+
#[tokio::main(flavor = "current_thread")]
21+
async fn main() {
22+
tokio::spawn(a());
23+
tokio::spawn(b());
24+
tokio::spawn(c());
25+
26+
let handle = tokio::runtime::Handle::current();
27+
let dump = handle.dump();
28+
29+
for (i, task) in dump.tasks().iter().enumerate() {
30+
let trace = task.trace();
31+
println!("task {i} trace:");
32+
println!("{trace}");
33+
}
34+
}

tokio/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ signal = [
8585
"windows-sys/Win32_System_Console",
8686
]
8787
sync = []
88+
taskdump = ["backtrace"]
8889
test-util = ["rt", "sync", "time"]
8990
time = []
9091

@@ -114,6 +115,7 @@ socket2 = { version = "0.4.9", optional = true, features = [ "all" ] }
114115
# Requires `--cfg tokio_unstable` to enable.
115116
[target.'cfg(tokio_unstable)'.dependencies]
116117
tracing = { version = "0.1.25", default-features = false, features = ["std"], optional = true } # Not in full
118+
backtrace = { version = "0.3.0", optional = true }
117119

118120
[target.'cfg(unix)'.dependencies]
119121
libc = { version = "0.2.42", optional = true }

tokio/src/macros/cfg.rs

+10
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,16 @@ macro_rules! cfg_not_rt_multi_thread {
373373
}
374374
}
375375

376+
macro_rules! cfg_taskdump {
377+
($($item:item)*) => {
378+
$(
379+
#[cfg(all(tokio_unstable, feature = "taskdump"))]
380+
#[cfg_attr(docsrs, doc(cfg(all(tokio_unstable, feature = "taskdump"))))]
381+
$item
382+
)*
383+
};
384+
}
385+
376386
macro_rules! cfg_test_util {
377387
($($item:item)*) => {
378388
$(

tokio/src/runtime/context.rs

+18
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ cfg_rt! {
1414
use std::time::Duration;
1515
}
1616

17+
cfg_taskdump! {
18+
use crate::runtime::task::trace;
19+
}
20+
1721
struct Context {
1822
/// Uniquely identifies the current thread
1923
#[cfg(feature = "rt")]
@@ -45,6 +49,9 @@ struct Context {
4549
/// Tracks the amount of "work" a task may still do before yielding back to
4650
/// the sheduler
4751
budget: Cell<coop::Budget>,
52+
53+
#[cfg(all(tokio_unstable, feature = "taskdump"))]
54+
trace: trace::Context,
4855
}
4956

5057
tokio_thread_local! {
@@ -75,6 +82,9 @@ tokio_thread_local! {
7582
rng: FastRand::new(RngSeed::new()),
7683

7784
budget: Cell::new(coop::Budget::unconstrained()),
85+
86+
#[cfg(all(tokio_unstable, feature = "taskdump"))]
87+
trace: trace::Context::new(),
7888
}
7989
}
8090
}
@@ -380,6 +390,14 @@ cfg_rt! {
380390
}
381391
}
382392

393+
cfg_taskdump! {
394+
/// SAFETY: Callers of this function must ensure that trace frames always
395+
/// form a valid linked list.
396+
pub(crate) unsafe fn with_trace<R>(f: impl FnOnce(&trace::Context) -> R) -> R {
397+
CONTEXT.with(|c| f(&c.trace))
398+
}
399+
}
400+
383401
// Forces the current "entered" state to be cleared while the closure
384402
// is executed.
385403
//

tokio/src/runtime/dump.rs

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//! Snapshots of runtime state.
2+
3+
use std::fmt;
4+
5+
/// A snapshot of a runtime's state.
6+
#[derive(Debug)]
7+
8+
pub struct Dump {
9+
tasks: Tasks,
10+
}
11+
12+
/// Snapshots of tasks.
13+
#[derive(Debug)]
14+
15+
pub struct Tasks {
16+
tasks: Vec<Task>,
17+
}
18+
19+
/// A snapshot of a task.
20+
#[derive(Debug)]
21+
22+
pub struct Task {
23+
trace: Trace,
24+
}
25+
26+
/// An execution trace of a task's last poll.
27+
#[derive(Debug)]
28+
pub struct Trace {
29+
inner: super::task::trace::Trace,
30+
}
31+
32+
impl Dump {
33+
pub(crate) fn new(tasks: Vec<Task>) -> Self {
34+
Self {
35+
tasks: Tasks { tasks },
36+
}
37+
}
38+
39+
/// Tasks in this snapshot.
40+
pub fn tasks(&self) -> &Tasks {
41+
&self.tasks
42+
}
43+
}
44+
45+
impl Tasks {
46+
/// Iterate over tasks.
47+
pub fn iter(&self) -> impl Iterator<Item = &Task> {
48+
self.tasks.iter()
49+
}
50+
}
51+
52+
impl Task {
53+
pub(crate) fn new(trace: super::task::trace::Trace) -> Self {
54+
Self {
55+
trace: Trace { inner: trace },
56+
}
57+
}
58+
59+
/// A trace of this task's state.
60+
pub fn trace(&self) -> &Trace {
61+
&self.trace
62+
}
63+
}
64+
65+
impl fmt::Display for Trace {
66+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67+
self.inner.fmt(f)
68+
}
69+
}

tokio/src/runtime/handle.rs

+16
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,8 @@ impl Handle {
274274
F::Output: Send + 'static,
275275
{
276276
let id = crate::runtime::task::Id::next();
277+
#[cfg(all(tokio_unstable, feature = "taskdump"))]
278+
let future = super::task::trace::Trace::root(future);
277279
#[cfg(all(tokio_unstable, feature = "tracing"))]
278280
let future = crate::util::trace::task(future, "task", _name, id.as_u64());
279281
self.inner.spawn(future, id)
@@ -321,6 +323,20 @@ cfg_metrics! {
321323
}
322324
}
323325

326+
cfg_taskdump! {
327+
impl Handle {
328+
/// Capture a snapshot of this runtime's state.
329+
pub fn dump(&self) -> crate::runtime::Dump {
330+
match &self.inner {
331+
scheduler::Handle::CurrentThread(handle) => handle.dump(),
332+
#[cfg(all(feature = "rt-multi-thread", not(tokio_wasi)))]
333+
scheduler::Handle::MultiThread(_) =>
334+
unimplemented!("taskdumps are unsupported on the multi-thread runtime"),
335+
}
336+
}
337+
}
338+
}
339+
324340
/// Error returned by `try_current` when no Runtime has been started
325341
#[derive(Debug)]
326342
pub struct TryCurrentError {

tokio/src/runtime/mod.rs

+5
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,11 @@ cfg_rt! {
233233
mod defer;
234234
pub(crate) use defer::Defer;
235235

236+
cfg_taskdump! {
237+
pub mod dump;
238+
pub use dump::Dump;
239+
}
240+
236241
mod handle;
237242
pub use handle::{EnterGuard, Handle, TryCurrentError};
238243

tokio/src/runtime/scheduler/current_thread.rs

+40
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,46 @@ impl Handle {
377377
handle
378378
}
379379

380+
/// Capture a snapshot of this runtime's state.
381+
#[cfg(all(tokio_unstable, feature = "taskdump"))]
382+
pub(crate) fn dump(&self) -> crate::runtime::Dump {
383+
use crate::runtime::{dump, task::trace::Trace};
384+
385+
let mut snapshots = vec![];
386+
387+
// todo: how to make this work outside of a runtime context?
388+
CURRENT.with(|maybe_context| {
389+
// drain the local queue
390+
let Some(context) = maybe_context else { return };
391+
let mut maybe_core = context.core.borrow_mut();
392+
let Some(core) = maybe_core.as_mut() else { return };
393+
let local = &mut core.tasks;
394+
let _ = local.drain(..);
395+
396+
// drain the injection queue
397+
if let Some(injection) = self.shared.queue.lock().as_mut() {
398+
let _ = injection.drain(..);
399+
}
400+
401+
// notify each task
402+
let mut tasks = vec![];
403+
self.shared.owned.for_each(|task| {
404+
// set the notified bit
405+
let _ = task.as_raw().state().transition_to_notified_for_tracing();
406+
// store the raw tasks into a vec
407+
tasks.push(task.as_raw());
408+
});
409+
410+
// trace each task
411+
for task in tasks {
412+
let ((), trace) = Trace::capture(|| task.poll());
413+
snapshots.push(dump::Task::new(trace));
414+
}
415+
});
416+
417+
dump::Dump::new(snapshots)
418+
}
419+
380420
fn pop(&self) -> Option<task::Notified<Arc<Handle>>> {
381421
match self.shared.queue.lock().as_mut() {
382422
Some(queue) => queue.pop_front(),

tokio/src/runtime/task/list.rs

+12
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,18 @@ impl<S: 'static> OwnedTasks<S> {
172172
}
173173
}
174174

175+
cfg_taskdump! {
176+
impl<S: 'static> OwnedTasks<S> {
177+
/// Locks the tasks, and calls `f` on an iterator over them.
178+
pub(crate) fn for_each<F>(&self, f: F)
179+
where
180+
F: FnMut(&Task<S>)
181+
{
182+
self.inner.lock().list.for_each(f)
183+
}
184+
}
185+
}
186+
175187
impl<S: 'static> LocalOwnedTasks<S> {
176188
pub(crate) fn new() -> Self {
177189
Self {

tokio/src/runtime/task/mod.rs

+9
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ use self::state::State;
207207

208208
mod waker;
209209

210+
cfg_taskdump! {
211+
pub(crate) mod trace;
212+
}
213+
210214
use crate::future::Future;
211215
use crate::util::linked_list;
212216

@@ -340,6 +344,11 @@ impl<S: 'static> Task<S> {
340344
}
341345
}
342346

347+
#[cfg(all(tokio_unstable, feature = "taskdump"))]
348+
pub(crate) fn as_raw(&self) -> RawTask {
349+
self.raw
350+
}
351+
343352
fn header(&self) -> &Header {
344353
self.raw.header()
345354
}

tokio/src/runtime/task/raw.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::ptr::NonNull;
66
use std::task::{Poll, Waker};
77

88
/// Raw task handle
9-
pub(super) struct RawTask {
9+
pub(crate) struct RawTask {
1010
ptr: NonNull<Header>,
1111
}
1212

@@ -190,12 +190,12 @@ impl RawTask {
190190
}
191191

192192
/// Returns a reference to the task's state.
193-
pub(super) fn state(&self) -> &State {
193+
pub(crate) fn state(&self) -> &State {
194194
&self.header().state
195195
}
196196

197197
/// Safety: mutual exclusion is required to call this function.
198-
pub(super) fn poll(self) {
198+
pub(crate) fn poll(self) {
199199
let vtable = self.header().vtable;
200200
unsafe { (vtable.poll)(self.ptr) }
201201
}

tokio/src/runtime/task/state.rs

+12-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::fmt;
44
use std::sync::atomic::Ordering::{AcqRel, Acquire, Release};
55
use std::usize;
66

7-
pub(super) struct State {
7+
pub(crate) struct State {
88
val: AtomicUsize,
99
}
1010

@@ -88,7 +88,7 @@ pub(super) enum TransitionToNotifiedByVal {
8888
}
8989

9090
#[must_use]
91-
pub(super) enum TransitionToNotifiedByRef {
91+
pub(crate) enum TransitionToNotifiedByRef {
9292
DoNothing,
9393
Submit,
9494
}
@@ -270,6 +270,16 @@ impl State {
270270
})
271271
}
272272

273+
/// Transitions the state to `NOTIFIED`, unconditionally increasing the ref count.
274+
#[cfg(all(tokio_unstable, feature = "taskdump"))]
275+
pub(crate) fn transition_to_notified_for_tracing(&self) {
276+
self.fetch_update_action(|mut snapshot| {
277+
snapshot.set_notified();
278+
snapshot.ref_inc();
279+
((), Some(snapshot))
280+
});
281+
}
282+
273283
/// Sets the cancelled bit and transitions the state to `NOTIFIED` if idle.
274284
///
275285
/// Returns `true` if the task needs to be submitted to the pool for

0 commit comments

Comments
 (0)