Skip to main content

compio_executor/
console.rs

1//! [`tokio-console`] instrumentation.
2//!
3//! [`tokio-console`] collects its data through [`tracing`] spans and events
4//! that follow a fixed naming convention. It is *not* tied to tokio's internals
5//! in any way, so any executor emitting the same spans and events can be
6//! observed with it.
7//!
8//! Enable the `console` feature to make this executor emit them:
9//!
10//! * every task gets a `runtime.spawn` span, entered while the task is polled,
11//!   so that the console can compute poll counts, busy/idle/scheduled times and
12//!   the poll time histogram;
13//! * every waker operation emits a `runtime::waker` event, so that the console
14//!   can compute waker counts and detect self-wakes and lost wakers;
15//! * a closure handed to the blocking pool gets such a span too, entered around
16//!   the closure instead of around a poll, so that the time spent in it is
17//!   reported as busy time rather than as idle time.
18//!
19//! When the feature is disabled, all of this compiles down to nothing: the
20//! types in this module become zero-sized and every method an empty inlined
21//! function.
22//!
23//! # Usage
24//!
25//! `console-subscriber` refuses to run unless it can prove that the runtime is
26//! instrumented, which for tokio means the `tokio_unstable` cfg. For other
27//! runtimes it provides the `console_without_tokio_unstable` escape hatch, so a
28//! binary observing compio needs:
29//!
30//! ```toml
31//! # .cargo/config.toml
32//! [build]
33//! rustflags = ["--cfg", "console_without_tokio_unstable"]
34//! ```
35//!
36//! Depending on `console-subscriber` and installing it is then all it takes:
37//!
38//! ```ignore
39//! console_subscriber::init();
40//! compio::runtime::Runtime::new().unwrap().block_on(async {
41//!     // ...
42//! });
43//! ```
44//!
45//! # Limitations
46//!
47//! * The console's data model has one runtime per process, while compio is
48//!   thread-per-core and has one executor per thread. The tasks of all of them
49//!   are listed together; the `thread` field tells them apart.
50//! * The subscriber has to be the global default, which
51//!   `console_subscriber::init` makes it. A span carries the subscriber it was
52//!   created with, but an event goes to whichever one is current on the thread
53//!   emitting it, so a thread-local subscriber misses the waker operations
54//!   other threads perform. Wakers cross threads routinely — that is what
55//!   waking a task from another executor is — and the clone and drop counts of
56//!   one that does no longer balance, leaving the console to report a lost
57//!   waker that is not lost.
58//! * A `block_on` nested inside a task — a runtime built within another one —
59//!   reports the two as separate tasks, but both of their spans are entered on
60//!   the same stack. The console attributes the polls to the inner one for as
61//!   long as that is the case.
62//! * A blocking task has no waker operations, since it is a closure rather than
63//!   a future. The console knows this from its `kind` and does not report a
64//!   lost waker for it.
65//! * A task spawned by an `async fn` is attributed to that function rather than
66//!   to its caller, since [`#[track_caller]`][async-track-caller] is a no-op on
67//!   `async fn`s and [`SpawnMeta`] therefore cannot be forwarded through them.
68//!   A function that wants the caller instead can be a plain `fn` returning a
69//!   future, capturing the [`SpawnMeta`] before the `async` block it returns —
70//!   at the cost of an opaque return type, and of running whatever precedes the
71//!   block when it is called rather than when it is first polled. The ones
72//!   compio spawns itself are named either way.
73//!
74//!   Nightly's `async_fn_track_caller` is not a substitute: it reports the
75//!   caller of `poll`, which is the `.await` when a future is awaited directly,
76//!   but a line inside `join!`, `select!` or whichever combinator drives it
77//!   otherwise.
78//! * The resources tab stays empty: timers and in-flight operations are not
79//!   instrumented yet.
80//! * A task's span is closed even when the thread is unwinding, or the console
81//!   would show the task as running forever. The subscriber therefore runs
82//!   during a panic, where a panic of its own aborts instead of unwinding.
83//!
84//! [`tokio-console`]: https://github.com/tokio-rs/console
85//! [`tracing`]: https://docs.rs/tracing
86//! [async-track-caller]: https://github.com/rust-lang/rust/issues/110011
87
88cfg_select! {
89    feature = "console" => {
90        mod enabled;
91        use enabled as imp;
92    }
93    _ => {
94        mod disabled;
95        use disabled as imp;
96    }
97}
98
99pub(crate) use imp::TaskSpan;
100pub use imp::{SpawnMeta, instrument_block_on, instrument_blocking, instrument_execute};
101
102/// An operation on a task's waker, reported as a `runtime::waker` event.
103///
104/// Note that [`Waker::wake`](std::task::Waker::wake) does not call the `drop`
105/// implementation, so the console counts [`Self::Wake`] as both a wake and a
106/// drop. Emitting an additional [`Self::Drop`] for it would make the live waker
107/// count (clones - drops) go negative.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub(crate) enum WakerOp {
110    Clone,
111    Drop,
112    Wake,
113    WakeByRef,
114}
115
116impl WakerOp {
117    /// The `op` value of the event, as expected by the console.
118    ///
119    /// Only the enabled variant reports anything, so only it reads this.
120    #[cfg(feature = "console")]
121    pub(crate) const fn as_str(self) -> &'static str {
122        match self {
123            Self::Clone => "waker.clone",
124            Self::Drop => "waker.drop",
125            Self::Wake => "waker.wake",
126            Self::WakeByRef => "waker.wake_by_ref",
127        }
128    }
129}
130/// Assertions that the two variants above present the same surface.
131///
132/// Only one of them is ever compiled, and the one compiled by default is the
133/// one nearly every build uses: a difference between the two shows up as a
134/// build failure for whoever turns the feature on, long after the code that
135/// assumed the other shape was written.
136///
137/// Coercing each item to a function pointer pins its whole signature, and
138/// naming [`EnterGuard`] with a lifetime pins the shape of the guard: the
139/// enabled one borrows the span, so a disabled one that owns itself, and would
140/// let code outlive the span it is timing, does not have a lifetime to name.
141#[cfg(test)]
142mod parity {
143    use std::{fmt::Debug, future::Future};
144
145    use super::{imp::EnterGuard, *};
146
147    const _: fn() -> SpawnMeta = SpawnMeta::capture;
148    const _: fn(SpawnMeta, &'static str) -> SpawnMeta = SpawnMeta::named;
149    const _: fn() -> SpawnMeta = SpawnMeta::untracked;
150
151    const _: fn(SpawnMeta) -> TaskSpan = TaskSpan::new::<()>;
152    const _: for<'a> fn(&'a TaskSpan) -> EnterGuard<'a> = TaskSpan::enter;
153    const _: fn(&TaskSpan, WakerOp) = TaskSpan::waker_op;
154
155    /// [`SpawnMeta`] is copied out of a spawn call rather than moved, and
156    /// reaches the dispatcher's threads through its channel.
157    const fn meta<T: Copy + Send + Sync + Unpin + Debug + 'static>() {}
158    const _: () = meta::<SpawnMeta>();
159
160    /// [`TaskSpan`] sits in the task header, which threads share.
161    const fn span<T: Send + Sync + Debug>() {}
162    const _: () = span::<TaskSpan>();
163
164    /// The wrappers return `impl Trait`, so pin them by use instead.
165    #[test]
166    fn the_wrappers_pass_their_argument_through() {
167        assert_eq!(instrument_blocking(SpawnMeta::untracked(), || 1u8)(), 1);
168
169        let fut = instrument_block_on(SpawnMeta::untracked(), std::future::ready(1u8));
170        let _: &dyn Future<Output = u8> = &fut;
171
172        let fut = instrument_execute(SpawnMeta::untracked(), std::future::ready(1u8));
173        let _: &dyn Future<Output = u8> = &fut;
174    }
175}