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}