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//! The install has to happen before the `block_on`, not within it, or the task
46//! running the body is missing from the console — see the limitation below.
47//! `#[compio::main]` therefore installs the subscriber itself when asked to:
48//!
49//! ```ignore
50//! #[compio::main(console)]
51//! async fn main() {
52//!     // ...
53//! }
54//! ```
55//!
56//! # Limitations
57//!
58//! * The console's data model has one runtime per process, while compio is
59//!   thread-per-core and has one executor per thread. The tasks of all of them
60//!   are listed together; the `thread` field tells them apart.
61//! * A subscriber installed inside `block_on` never sees the task it runs in.
62//!   The `runtime.spawn` span of a task is created when the task is -- for the
63//!   `block_on` kind, on the way in -- and a span created while no subscriber
64//!   is installed stays disabled for its whole life. The console then lists
65//!   everything spawned afterwards and nothing else, which reads as healthy
66//!   rather than as incomplete. Installing it inside the body of
67//!   `#[compio::main]` is exactly this case, since that body *is* the future
68//!   handed to `block_on`; `#[compio::main(console)]` installs it around the
69//!   `block_on` instead, and a binary building its runtime itself installs it
70//!   before `block_on` is called.
71//! * `#[compio::test(console)]` is rejected rather than supported: a test
72//!   binary runs more than one test, and the subscriber is a process-wide
73//!   default that only the first of them could set.
74//! * The subscriber has to be the global default, which
75//!   `console_subscriber::init` makes it. A span carries the subscriber it was
76//!   created with, but an event goes to whichever one is current on the thread
77//!   emitting it, so a thread-local subscriber misses the waker operations
78//!   other threads perform. Wakers cross threads routinely — that is what
79//!   waking a task from another executor is — and the clone and drop counts of
80//!   one that does no longer balance, leaving the console to report a lost
81//!   waker that is not lost.
82//! * A `block_on` nested inside a task — a runtime built within another one —
83//!   reports the two as separate tasks, but both of their spans are entered on
84//!   the same stack. The console attributes the polls to the inner one for as
85//!   long as that is the case.
86//! * A blocking task has no waker operations, since it is a closure rather than
87//!   a future. The console knows this from its `kind` and does not report a
88//!   lost waker for it.
89//! * A task spawned by an `async fn` is attributed to that function rather than
90//!   to its caller, since [`#[track_caller]`][async-track-caller] is a no-op on
91//!   `async fn`s and [`SpawnMeta`] therefore cannot be forwarded through them.
92//!   A function that wants the caller instead can be a plain `fn` returning a
93//!   future, capturing the [`SpawnMeta`] before the `async` block it returns —
94//!   at the cost of an opaque return type, and of running whatever precedes the
95//!   block when it is called rather than when it is first polled. The ones
96//!   compio spawns itself are named either way.
97//!
98//!   Nightly's `async_fn_track_caller` is not a substitute: it reports the
99//!   caller of `poll`, which is the `.await` when a future is awaited directly,
100//!   but a line inside `join!`, `select!` or whichever combinator drives it
101//!   otherwise.
102//! * The resources tab stays empty: timers and in-flight operations are not
103//!   instrumented yet.
104//! * A task's span is closed even when the thread is unwinding, or the console
105//!   would show the task as running forever. The subscriber therefore runs
106//!   during a panic, where a panic of its own aborts instead of unwinding.
107//!
108//! [`tokio-console`]: https://github.com/tokio-rs/console
109//! [`tracing`]: https://docs.rs/tracing
110//! [async-track-caller]: https://github.com/rust-lang/rust/issues/110011
111
112cfg_select! {
113    feature = "console" => {
114        mod enabled;
115        use enabled as imp;
116    }
117    _ => {
118        mod disabled;
119        use disabled as imp;
120    }
121}
122
123pub(crate) use imp::TaskSpan;
124pub use imp::{SpawnMeta, instrument_block_on, instrument_blocking, instrument_execute};
125
126/// An operation on a task's waker, reported as a `runtime::waker` event.
127///
128/// Note that [`Waker::wake`](std::task::Waker::wake) does not call the `drop`
129/// implementation, so the console counts [`Self::Wake`] as both a wake and a
130/// drop. Emitting an additional [`Self::Drop`] for it would make the live waker
131/// count (clones - drops) go negative.
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub(crate) enum WakerOp {
134    Clone,
135    Drop,
136    Wake,
137    WakeByRef,
138}
139
140impl WakerOp {
141    /// The `op` value of the event, as expected by the console.
142    ///
143    /// Only the enabled variant reports anything, so only it reads this.
144    #[cfg(feature = "console")]
145    pub(crate) const fn as_str(self) -> &'static str {
146        match self {
147            Self::Clone => "waker.clone",
148            Self::Drop => "waker.drop",
149            Self::Wake => "waker.wake",
150            Self::WakeByRef => "waker.wake_by_ref",
151        }
152    }
153}
154/// Assertions that the two variants above present the same surface.
155///
156/// Only one of them is ever compiled, and the one compiled by default is the
157/// one nearly every build uses: a difference between the two shows up as a
158/// build failure for whoever turns the feature on, long after the code that
159/// assumed the other shape was written.
160///
161/// Coercing each item to a function pointer pins its whole signature, and
162/// naming [`EnterGuard`] with a lifetime pins the shape of the guard: the
163/// enabled one borrows the span, so a disabled one that owns itself, and would
164/// let code outlive the span it is timing, does not have a lifetime to name.
165#[cfg(test)]
166mod parity {
167    use std::{fmt::Debug, future::Future};
168
169    use super::{imp::EnterGuard, *};
170
171    const _: fn() -> SpawnMeta = SpawnMeta::capture;
172    const _: fn(SpawnMeta, &'static str) -> SpawnMeta = SpawnMeta::named;
173    const _: fn() -> SpawnMeta = SpawnMeta::untracked;
174
175    const _: fn(SpawnMeta) -> TaskSpan = TaskSpan::new::<()>;
176    const _: for<'a> fn(&'a TaskSpan) -> EnterGuard<'a> = TaskSpan::enter;
177    const _: fn(&TaskSpan, WakerOp) = TaskSpan::waker_op;
178
179    /// [`SpawnMeta`] is copied out of a spawn call rather than moved, and
180    /// reaches the dispatcher's threads through its channel.
181    const fn meta<T: Copy + Send + Sync + Unpin + Debug + 'static>() {}
182    const _: () = meta::<SpawnMeta>();
183
184    /// [`TaskSpan`] sits in the task header, which threads share.
185    const fn span<T: Send + Sync + Debug>() {}
186    const _: () = span::<TaskSpan>();
187
188    /// The wrappers return `impl Trait`, so pin them by use instead.
189    #[test]
190    fn the_wrappers_pass_their_argument_through() {
191        assert_eq!(instrument_blocking(SpawnMeta::untracked(), || 1u8)(), 1);
192
193        let fut = instrument_block_on(SpawnMeta::untracked(), std::future::ready(1u8));
194        let _: &dyn Future<Output = u8> = &fut;
195
196        let fut = instrument_execute(SpawnMeta::untracked(), std::future::ready(1u8));
197        let _: &dyn Future<Output = u8> = &fut;
198    }
199}