Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement task dumps for current-thread runtime. #5608

Merged
merged 39 commits into from
Apr 27, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
1222dfb
tokio: introduce `Handle::dump` and impl for current-thread runtime
jswrenn Mar 29, 2023
d9aa093
tokio: limit taskdump test to tokio_unstable & 'taskdump' feature
jswrenn Apr 7, 2023
66cd9c9
tokio: limit taskdump example to 'taskdump' feature
jswrenn Apr 7, 2023
206b3d4
tokio: don't use let-else syntax
jswrenn Apr 11, 2023
b998f74
tokio: encapsulate taskdumping refcount shenanigans
jswrenn Apr 11, 2023
8a727a1
tokio: revise taskdump imports to conform to tokio conventions
jswrenn Apr 11, 2023
80dbaf7
tokio: fix formatting issue missed by rustfmt
jswrenn Apr 11, 2023
c132b33
tokio: limit taskdumps to linux
jswrenn Apr 11, 2023
9c86604
tokio: remove pointless let binding
jswrenn Apr 11, 2023
306a547
tokio: add taskdump roots to all appropriate places
jswrenn Apr 11, 2023
725bf3f
tokio: fix broken taskdump test
jswrenn Apr 11, 2023
c20af63
tokio: fallback taskdump example when unsupported
jswrenn Apr 11, 2023
52c8ca9
tokio: bump `backtrace-rs` version to 0.3.58
jswrenn Apr 11, 2023
fdc01e1
tokio: tweak dump example to avoid warnings
jswrenn Apr 11, 2023
144f378
tokio: make `taskdump` feature depend on `rt`
jswrenn Apr 11, 2023
c824389
tokio: limit taskdumps to specific, blessed platforms
jswrenn Apr 12, 2023
0c655bb
tokio: compile_error when `taskdump` feature is unsupported
jswrenn Apr 13, 2023
578c3c4
tokio: taskdumps cargo feature -> `--cfg tokio_taskump`
jswrenn Apr 13, 2023
ac7fa6b
tokio: restrict visibility of various items
jswrenn Apr 13, 2023
a246b11
tokio: rustfmt
jswrenn Apr 13, 2023
a62f9ee
tokio: don't cross-test with `--cfg tokio_taskdump`
jswrenn Apr 13, 2023
f695499
tokio: don't `--cfg tokio_taskdump` on unsupported platforms
jswrenn Apr 14, 2023
bb6990f
tokio: restrict visibility of `RawTask`
jswrenn Apr 14, 2023
51d15f0
tokio: don't alias `HashMap` and `HashSet`
jswrenn Apr 14, 2023
178bf74
Merge branch 'master' into taskdump-backtracing
jswrenn Apr 14, 2023
0a8d240
Merge branch 'master' into taskdump-backtracing
jswrenn Apr 16, 2023
b6d093a
tokio: tweak taskdump CI
jswrenn Apr 17, 2023
1ff355b
tokio: fix taskdump arch cfg
jswrenn Apr 17, 2023
9e0dd91
tokio: `--cfg tokio_taskdump` requires `rt` feature
jswrenn Apr 17, 2023
9a5e7fa
Merge branch 'master' into taskdump-backtracing
jswrenn Apr 17, 2023
f536db5
tokio: optimize LinkedList::for_each
jswrenn Apr 18, 2023
d3cda94
Merge branch 'master' into taskdump-backtracing
jswrenn Apr 18, 2023
a29974d
tokio: compile_error liberally for unsupported taskdump platforms
jswrenn Apr 19, 2023
51f970f
tokio: use `clear()` instead of `drain(..)` to drain queues
jswrenn Apr 19, 2023
8a8dceb
ci: don't --cfg tokio_taskdump in 'build loom tests'
jswrenn Apr 19, 2023
b0b1160
ci: test unstable with and without taskdumps
jswrenn Apr 19, 2023
8318d93
tokio: don't error on `all(not(feature = "rt"), tokio_taskdump))`
jswrenn Apr 19, 2023
0dc4b68
ci: test feature powerset with `unstable && !taskdump` cfgs
jswrenn Apr 26, 2023
04bd5e1
Merge branch 'master' into taskdump-backtracing
jswrenn Apr 26, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ edition = "2018"
# If you copy one of the examples into a new project, you should be using
# [dependencies] instead, and delete the **path**.
[dev-dependencies]
tokio = { version = "1.0.0", path = "../tokio", features = ["full", "tracing"] }
tokio = { version = "1.0.0", path = "../tokio", features = ["full", "tracing", "taskdump"] }
tokio-util = { version = "0.7.0", path = "../tokio-util", features = ["full"] }
tokio-stream = { version = "0.1", path = "../tokio-stream" }

Expand Down Expand Up @@ -90,3 +90,7 @@ path = "named-pipe-ready.rs"
[[example]]
name = "named-pipe-multi-client"
path = "named-pipe-multi-client.rs"

[[example]]
name = "dump"
path = "dump.rs"
48 changes: 48 additions & 0 deletions examples/dump.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//! This example demonstrates tokio's experimental taskdumping functionality.

#[cfg(all(
tokio_unstable,
target_os = "linux",
any(target_arch = "aarch64", target_arch = "i686", target_arch = "x86_64")
))]
#[tokio::main(flavor = "current_thread")]
async fn main() {
use std::hint::black_box;

#[inline(never)]
async fn a() {
black_box(b()).await
}

#[inline(never)]
async fn b() {
black_box(c()).await
}

#[inline(never)]
async fn c() {
black_box(tokio::task::yield_now()).await
}

tokio::spawn(a());
tokio::spawn(b());
tokio::spawn(c());

let handle = tokio::runtime::Handle::current();
let dump = handle.dump();

for (i, task) in dump.tasks().iter().enumerate() {
let trace = task.trace();
println!("task {i} trace:");
println!("{trace}");
}
}

#[cfg(not(all(
tokio_unstable,
target_os = "linux",
any(target_arch = "aarch64", target_arch = "i686", target_arch = "x86_64")
)))]
fn main() {
println!("task dumps are not available")
}
2 changes: 2 additions & 0 deletions tokio/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ signal = [
"windows-sys/Win32_System_Console",
]
sync = []
taskdump = ["rt", "backtrace"]
carllerche marked this conversation as resolved.
Show resolved Hide resolved
test-util = ["rt", "sync", "time"]
time = []

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

[target.'cfg(unix)'.dependencies]
libc = { version = "0.2.42", optional = true }
Expand Down
28 changes: 28 additions & 0 deletions tokio/src/macros/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,34 @@ macro_rules! cfg_not_rt_multi_thread {
}
}

macro_rules! cfg_taskdump {
($($item:item)*) => {
$(
#[cfg(all(
tokio_unstable,
feature = "taskdump",
target_os = "linux",
any(
target_arch = "aarch64",
target_arch = "i686",
target_arch = "x86_64"
)
))]
#[cfg_attr(docsrs, doc(cfg(all(
tokio_unstable,
feature = "taskdump",
target_os = "linux",
any(
target_arch = "aarch64",
target_arch = "i686",
target_arch = "x86_64"
)
))))]
$item
)*
};
}

macro_rules! cfg_test_util {
($($item:item)*) => {
$(
Expand Down
32 changes: 32 additions & 0 deletions tokio/src/runtime/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ cfg_rt! {
use std::time::Duration;
}

cfg_taskdump! {
use crate::runtime::task::trace;
}

struct Context {
/// Uniquely identifies the current thread
#[cfg(feature = "rt")]
Expand Down Expand Up @@ -45,6 +49,14 @@ struct Context {
/// Tracks the amount of "work" a task may still do before yielding back to
/// the sheduler
budget: Cell<coop::Budget>,

#[cfg(all(
tokio_unstable,
feature = "taskdump",
target_os = "linux",
any(target_arch = "aarch64", target_arch = "i686", target_arch = "x86_64")
))]
trace: trace::Context,
}

tokio_thread_local! {
Expand Down Expand Up @@ -75,6 +87,18 @@ tokio_thread_local! {
rng: FastRand::new(RngSeed::new()),

budget: Cell::new(coop::Budget::unconstrained()),

#[cfg(all(
tokio_unstable,
feature = "taskdump",
target_os = "linux",
any(
target_arch = "aarch64",
target_arch = "i686",
target_arch = "x86_64"
)
))]
trace: trace::Context::new(),
}
}
}
Expand Down Expand Up @@ -380,6 +404,14 @@ cfg_rt! {
}
}

cfg_taskdump! {
/// SAFETY: Callers of this function must ensure that trace frames always
/// form a valid linked list.
pub(crate) unsafe fn with_trace<R>(f: impl FnOnce(&trace::Context) -> R) -> R {
CONTEXT.with(|c| f(&c.trace))
}
}

// Forces the current "entered" state to be cleared while the closure
// is executed.
//
Expand Down
66 changes: 66 additions & 0 deletions tokio/src/runtime/dump.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//! Snapshots of runtime state.

use std::fmt;

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

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

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

/// An execution trace of a task's last poll.
#[derive(Debug)]
pub struct Trace {
inner: super::task::trace::Trace,
}

impl Dump {
pub(crate) fn new(tasks: Vec<Task>) -> Self {
Self {
tasks: Tasks { tasks },
}
}

/// Tasks in this snapshot.
pub fn tasks(&self) -> &Tasks {
&self.tasks
}
}

impl Tasks {
/// Iterate over tasks.
pub fn iter(&self) -> impl Iterator<Item = &Task> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One possible improvement here is to use impl Trait to simplify the definition of methods that return iterators. For example:

Suggested change
pub fn iter(&self) -> impl Iterator<Item = &Task> {
pub fn iter(&self) -> impl Iterator<Item = &Task> + '_ {

'_ means that the lifetime of the returned iterator is bound to the lifetime of the Tasks object

self.tasks.iter()
}
}

impl Task {
pub(crate) fn new(trace: super::task::trace::Trace) -> Self {
Self {
trace: Trace { inner: trace },
}
}

/// A trace of this task's state.
pub fn trace(&self) -> &Trace {
&self.trace
}
}

impl fmt::Display for Trace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.inner.fmt(f)
}
}
29 changes: 29 additions & 0 deletions tokio/src/runtime/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,14 @@ impl Handle {
/// [`tokio::time`]: crate::time
#[track_caller]
pub fn block_on<F: Future>(&self, future: F) -> F::Output {
#[cfg(all(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't require this change for the PR to land, but generally, these inline cfg flags are hard to maintain. I tend to prefer using cfg to define APIs and then using the API regardless of the cfg in code like this. For example, Trace::root would always be defined, but when the task dump capability is not enabled, it would be a no-op and return the argument without modifying it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, many of these cfgs could be reduced to a single #[cfg(tokio_taskdump)] since lib.rs contains a compile_error! if you have #[cfg(tokio_taskdump)] without the other options also set.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned that reducing these to just #[cfg(tokio_taskdump)] wouldn't prevent errors in these locations under odd configurations, and thus would drown out the lone explicit compile_error in lib.rs. I think Carl's suggestion is probably the way to go.

tokio_unstable,
feature = "taskdump",
target_os = "linux",
any(target_arch = "aarch64", target_arch = "i686", target_arch = "x86_64")
))]
let future = super::task::trace::Trace::root(future);

#[cfg(all(tokio_unstable, feature = "tracing"))]
let future =
crate::util::trace::task(future, "block_on", None, super::task::Id::next().as_u64());
Expand All @@ -274,6 +282,13 @@ impl Handle {
F::Output: Send + 'static,
{
let id = crate::runtime::task::Id::next();
#[cfg(all(
tokio_unstable,
feature = "taskdump",
target_os = "linux",
any(target_arch = "aarch64", target_arch = "i686", target_arch = "x86_64")
))]
let future = super::task::trace::Trace::root(future);
#[cfg(all(tokio_unstable, feature = "tracing"))]
let future = crate::util::trace::task(future, "task", _name, id.as_u64());
self.inner.spawn(future, id)
Expand Down Expand Up @@ -321,6 +336,20 @@ cfg_metrics! {
}
}

cfg_taskdump! {
impl Handle {
/// Capture a snapshot of this runtime's state.
pub fn dump(&self) -> crate::runtime::Dump {
match &self.inner {
scheduler::Handle::CurrentThread(handle) => handle.dump(),
#[cfg(all(feature = "rt-multi-thread", not(tokio_wasi)))]
scheduler::Handle::MultiThread(_) =>
unimplemented!("taskdumps are unsupported on the multi-thread runtime"),
}
}
}
}

/// Error returned by `try_current` when no Runtime has been started
#[derive(Debug)]
pub struct TryCurrentError {
Expand Down
5 changes: 5 additions & 0 deletions tokio/src/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,11 @@ cfg_rt! {
mod defer;
pub(crate) use defer::Defer;

cfg_taskdump! {
pub mod dump;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not critical for this PR, but we probably want to reconsider the API details before stabilization.

Today, tokio::runtime does not have any public submodules. Instead, it is flat. That would apply to task dumps as well. We would need to spend a bit of time bikeshedding type names, though.

pub use dump::Dump;
}

mod handle;
pub use handle::{EnterGuard, Handle, TryCurrentError};

Expand Down
8 changes: 8 additions & 0 deletions tokio/src/runtime/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,14 @@ impl Runtime {
/// [handle]: fn@Handle::block_on
#[track_caller]
pub fn block_on<F: Future>(&self, future: F) -> F::Output {
#[cfg(all(
tokio_unstable,
feature = "taskdump",
target_os = "linux",
any(target_arch = "aarch64", target_arch = "i686", target_arch = "x86_64")
))]
let future = super::task::trace::Trace::root(future);

#[cfg(all(tokio_unstable, feature = "tracing"))]
let future = crate::util::trace::task(
future,
Expand Down
45 changes: 45 additions & 0 deletions tokio/src/runtime/scheduler/current_thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,51 @@ impl Handle {
handle
}

/// Capture a snapshot of this runtime's state.
#[cfg(all(
tokio_unstable,
feature = "taskdump",
target_os = "linux",
any(target_arch = "aarch64", target_arch = "i686", target_arch = "x86_64")
))]
pub(crate) fn dump(&self) -> crate::runtime::Dump {
use crate::runtime::dump;
use task::trace::trace_current_thread;

let mut traces = vec![];

// todo: how to make this work outside of a runtime context?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You will most likely have to send a signal somehow.

CURRENT.with(|maybe_context| {
// drain the local queue
let context = if let Some(context) = maybe_context {
context
} else {
return;
};
let mut maybe_core = context.core.borrow_mut();
let core = if let Some(core) = maybe_core.as_mut() {
core
} else {
return;
};
let local = &mut core.tasks;

let mut injection = self.shared.queue.lock();
let injection = if let Some(injection) = injection.as_mut() {
injection
} else {
return;
};

traces = trace_current_thread(&self.shared.owned, local, injection)
.into_iter()
.map(dump::Task::new)
.collect();
});

dump::Dump::new(traces)
}

fn pop(&self) -> Option<task::Notified<Arc<Handle>>> {
match self.shared.queue.lock().as_mut() {
Some(queue) => queue.pop_front(),
Expand Down
12 changes: 12 additions & 0 deletions tokio/src/runtime/task/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,18 @@ impl<S: 'static> OwnedTasks<S> {
}
}

cfg_taskdump! {
impl<S: 'static> OwnedTasks<S> {
/// Locks the tasks, and calls `f` on an iterator over them.
pub(crate) fn for_each<F>(&self, f: F)
where
F: FnMut(&Task<S>)
{
self.inner.lock().list.for_each(f)
}
}
}

impl<S: 'static> LocalOwnedTasks<S> {
pub(crate) fn new() -> Self {
Self {
Expand Down