Skip to main content

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}