compio_executor/console/enabled.rs
1//! The actual [`tokio-console`] instrumentation.
2//!
3//! [`tokio-console`]: https://github.com/tokio-rs/console
4
5use std::{
6 mem,
7 panic::Location,
8 pin::Pin,
9 sync::{
10 Arc,
11 atomic::{AtomicU64, Ordering},
12 },
13 task::{Context, Poll, Wake, Waker},
14};
15
16use tracing::Span;
17
18use super::WakerOp;
19
20/// Target of the task spans. The console accepts any target for spans named
21/// `runtime.spawn`, but the target is displayed, so make it a useful one.
22const TARGET: &str = "compio::task";
23
24/// What a task span reports the task as being.
25///
26/// The console knows [`Self::Blocking`] and [`Self::BlockOn`] by name as the
27/// kinds it does not drive itself, and skips the lints that only make sense
28/// for a task it does: the self-wake ratio, the lost waker, the never-yielded
29/// and the large future ones. It treats every other kind, including one it
30/// does not know, as a task of its own.
31#[derive(Debug, Clone, Copy)]
32enum TaskKind {
33 Task,
34 Blocking,
35 BlockOn,
36}
37
38impl TaskKind {
39 /// The `kind` value of the span, as expected by the console.
40 const fn as_str(self) -> &'static str {
41 match self {
42 Self::Task => "task",
43 Self::Blocking => "blocking",
44 Self::BlockOn => "block_on",
45 }
46 }
47}
48
49/// Id displayed by the console in its `ID` column.
50///
51/// This is deliberately *not* the executor's [`TaskId`], which is a slot index
52/// and thus both reused and duplicated across the executors of a
53/// thread-per-core application.
54///
55/// [`TaskId`]: crate::queue::TaskId
56fn next_task_id() -> u64 {
57 static NEXT: AtomicU64 = AtomicU64::new(1);
58
59 NEXT.fetch_add(1, Ordering::Relaxed)
60}
61
62thread_local! {
63 /// Label of the current thread, to tell the tasks of the executors of a
64 /// thread-per-core application apart.
65 static THREAD: String = {
66 let thread = std::thread::current();
67 match thread.name() {
68 Some(name) => name.to_owned(),
69 None => format!("{:?}", thread.id()),
70 }
71 };
72}
73
74fn spawn_span(
75 kind: TaskKind,
76 size: usize,
77 loc: &'static Location<'static>,
78 name: Option<&'static str>,
79) -> Span {
80 fn build(
81 kind: TaskKind,
82 size: usize,
83 loc: &'static Location<'static>,
84 name: Option<&'static str>,
85 thread: &str,
86 ) -> Span {
87 tracing::trace_span!(
88 target: TARGET,
89 // The console attributes polls of a task to the innermost task span
90 // that is entered, so task spans must never be nested.
91 parent: None,
92 "runtime.spawn",
93 kind = kind.as_str(),
94 task.id = next_task_id(),
95 // The console gives this field a column of its own, and leaves it
96 // empty for the tasks that do not have it.
97 task.name = name,
98 size.bytes = size,
99 thread = thread,
100 loc.file = loc.file(),
101 loc.line = loc.line(),
102 loc.col = loc.column(),
103 )
104 }
105
106 // The label is gone once the thread tears its locals down, which a task
107 // spawned from another destructor still runs after. Report it unlabelled
108 // rather than panicking on the way out.
109 THREAD
110 .try_with(|thread| build(kind, size, loc, name, thread.as_str()))
111 .unwrap_or_else(|_| build(kind, size, loc, name, ""))
112}
113
114fn waker_op(id: u64, op: WakerOp) {
115 // `task.id` of a waker event is the *span* id of the task, which is how the
116 // console looks the task up.
117 tracing::trace!(target: "runtime::waker", op = op.as_str(), task.id = id);
118}
119
120/// Metadata of a spawned task, reported to the console.
121///
122/// `None` for a task that is not reported at all. The disabled variant of this
123/// is zero-sized, so nothing here may be load-bearing outside of the console.
124#[derive(Debug, Clone, Copy)]
125pub struct SpawnMeta(Option<Reported>);
126
127/// What is reported about a task, for the tasks that are reported at all.
128#[derive(Debug, Clone, Copy)]
129struct Reported {
130 loc: &'static Location<'static>,
131 name: Option<&'static str>,
132}
133
134impl SpawnMeta {
135 /// Capture the location of the caller.
136 #[inline]
137 #[track_caller]
138 pub fn capture() -> Self {
139 Self(Some(Reported {
140 loc: Location::caller(),
141 name: None,
142 }))
143 }
144
145 /// Name the task, which the console displays in a column of its own.
146 ///
147 /// This is worth doing for the tasks a user did not spawn themselves, since
148 /// the location of those points into compio rather than into their code.
149 #[inline]
150 pub fn named(self, name: &'static str) -> Self {
151 Self(self.0.map(|it| Reported {
152 name: Some(name),
153 ..it
154 }))
155 }
156
157 /// Do not report the task to the console at all.
158 ///
159 /// This is for tasks that only wrap work reported by something else, like
160 /// the future waiting for a blocking closure: reporting both would count
161 /// the same work twice, and hide the interesting one behind a wrapper that
162 /// is idle the whole time.
163 #[inline]
164 pub fn untracked() -> Self {
165 Self(None)
166 }
167
168 /// The span of the task, or a disabled span if it is not to be reported.
169 ///
170 /// Entering a disabled span and asking for its id are both no-ops, so
171 /// nothing downstream has to know about the difference.
172 fn span(self, kind: TaskKind, size: usize) -> Span {
173 match self.0 {
174 Some(it) => spawn_span(kind, size, it.loc, it.name),
175 None => Span::none(),
176 }
177 }
178}
179
180/// The guard [`TaskSpan::enter`] returns, named the same in both variants so
181/// that the parity assertions can reach it.
182pub(crate) type EnterGuard<'a> = tracing::span::Entered<'a>;
183
184/// The `runtime.spawn` span of a task.
185#[derive(Debug)]
186pub(crate) struct TaskSpan(Span);
187
188impl TaskSpan {
189 pub(crate) fn new<F>(meta: SpawnMeta) -> Self {
190 Self(meta.span(TaskKind::Task, mem::size_of::<F>()))
191 }
192
193 /// Enter the task span. The console measures the time the span is entered
194 /// as the busy time of the task, and counts one poll per entry.
195 #[inline]
196 pub(crate) fn enter(&self) -> EnterGuard<'_> {
197 self.0.enter()
198 }
199
200 /// Record a waker operation on this task.
201 #[inline]
202 pub(crate) fn waker_op(&self, op: WakerOp) {
203 if let Some(id) = self.0.id() {
204 waker_op(id.into_u64(), op);
205 }
206 }
207}
208
209/// A [`Waker`] that reports its operations to the console, on behalf of a task
210/// that is not owned by the executor.
211///
212/// The console derives the number of live wakers of a task from the clone and
213/// drop events, and cloning a [`Waker`] made of an [`Arc`] only bumps its
214/// refcount, which no hook of ours observes. The shim therefore reports the
215/// allocation rather than the handles to it: a `waker.clone` when it is made
216/// and a `waker.drop` when the last handle to it is gone. The console shows one
217/// live waker for as long as the task holds any, instead of how many.
218struct ShimWaker {
219 inner: Waker,
220 id: u64,
221}
222
223impl ShimWaker {
224 fn waker(inner: Waker, id: u64) -> Waker {
225 let this = Arc::new(Self { inner, id });
226 // The shim is a live waker of the task, and its drop reports a
227 // `waker.drop`, so report the matching `waker.clone` here.
228 this.report(WakerOp::Clone);
229 Waker::from(this)
230 }
231
232 #[inline]
233 fn report(&self, op: WakerOp) {
234 waker_op(self.id, op);
235 }
236}
237
238impl Wake for ShimWaker {
239 /// Report the wake as a [`WakerOp::WakeByRef`] even though this one
240 /// consumes a handle: the console counts a [`WakerOp::Wake`] as a drop too,
241 /// since [`Waker::wake`] does not run the [`Drop`] implementation — but the
242 /// [`Arc`] taken here does, once it is the last handle, and the drop would
243 /// be reported twice.
244 fn wake(self: Arc<Self>) {
245 self.wake_by_ref();
246 }
247
248 fn wake_by_ref(self: &Arc<Self>) {
249 self.report(WakerOp::WakeByRef);
250 self.inner.wake_by_ref();
251 }
252}
253
254impl Drop for ShimWaker {
255 fn drop(&mut self) {
256 self.report(WakerOp::Drop);
257 }
258}
259
260/// A future instrumented as a `block_on` task, returned by
261/// [`instrument_block_on`].
262struct BlockOn<F> {
263 span: Span,
264 id: Option<u64>,
265 /// The waker given to us by the runtime and the shim wrapping it, cached to
266 /// avoid rebuilding the shim on every poll.
267 waker: Option<(Waker, Waker)>,
268 fut: F,
269}
270
271impl<F: Future> Future for BlockOn<F> {
272 type Output = F::Output;
273
274 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
275 // SAFETY: we never move out of `fut`, and all other fields are `Unpin`.
276 let this = unsafe { self.get_unchecked_mut() };
277 let fut = unsafe { Pin::new_unchecked(&mut this.fut) };
278
279 let _entered = this.span.enter();
280
281 let Some(id) = this.id else {
282 return fut.poll(cx);
283 };
284
285 if !matches!(&this.waker, Some((given, _)) if given.will_wake(cx.waker())) {
286 let given = cx.waker().clone();
287 let shim = ShimWaker::waker(given.clone(), id);
288 this.waker = Some((given, shim));
289 }
290
291 let shim = &this.waker.as_ref().expect("waker was just set").1;
292 fut.poll(&mut Context::from_waker(shim))
293 }
294}
295
296/// Instrument a closure about to be handed to the blocking pool, so that it
297/// shows up as a blocking task in the console.
298///
299/// The span is created here rather than on the pool thread, so that the task
300/// shows up as soon as it is queued and the wait for a worker is reported as
301/// idle time. It is entered around the closure itself, so that the time spent
302/// in it is reported as busy time.
303///
304/// Plumbing for `compio-runtime`, not covered by this crate's semver.
305#[doc(hidden)]
306pub fn instrument_blocking<T, F: FnOnce() -> T>(meta: SpawnMeta, f: F) -> impl FnOnce() -> T {
307 let span = meta.span(TaskKind::Blocking, mem::size_of::<F>());
308
309 move || {
310 let _entered = span.enter();
311 f()
312 }
313}
314
315/// Instrument a future executed by a compatibility layer, so that it shows up
316/// as a task in the console.
317///
318/// `compio-compat` executes a future the way the runtime blocks on one, only
319/// driven by a foreign event loop instead of by a loop of its own, so report
320/// it as the same kind of task: the console has none that fits it better, and
321/// would take a kind of its own for one it drives itself.
322///
323/// Plumbing for `compio-compat`, not covered by this crate's semver.
324#[doc(hidden)]
325pub fn instrument_execute<F: Future>(meta: SpawnMeta, fut: F) -> impl Future<Output = F::Output> {
326 instrument_block_on(meta, fut)
327}
328
329/// Instrument a future blocked on by the runtime, so that it shows up as a
330/// task in the console.
331///
332/// Plumbing for `compio-runtime`, not covered by this crate's semver.
333#[doc(hidden)]
334pub fn instrument_block_on<F: Future>(meta: SpawnMeta, fut: F) -> impl Future<Output = F::Output> {
335 let span = meta.span(TaskKind::BlockOn, mem::size_of::<F>());
336 let id = span.id().map(|id| id.into_u64());
337
338 BlockOn {
339 span,
340 id,
341 waker: None,
342 fut,
343 }
344}