tokio_dtrace/
lib.rs

1// Copyright 2025 Oxide Computer Company
2
3#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
4#![doc = include_str!("../README.md")]
5//!
6//! ## Usage
7//!
8//! ### Enabling `tokio_unstable` Features
9//!
10//! `tokio-dtrace` requires Tokio's [unstable features] in order to use runtime
11//! hooks. These are features of Tokio that do not yet have stable APIs, and may
12//! change in 1.x releases. Unlike other optional features, Tokio requires that
13//! only the top-level binary workspace may opt in to these features (i.e., they
14//! may not be enabled by library dependencies).This means that the unstable
15//! features are enabled using a `RUSTFLAGS` config, rather than a Cargo
16//! feature.
17//!
18//! The simplest way to enable Tokio's unstable features is to add the
19//! following to your workspace's `.cargo/config.toml` file:
20//!
21//! ```toml
22//! [build]
23//! rustflags = ["--cfg", "tokio_unstable"]
24//! ```
25//!
26//! <div class="warning">
27//! The <code>[build]</code> section does <strong>not</strong> go in a
28//! <code>Cargo.toml</code> file. Instead it must be placed in the Cargo config
29//! file <code>.cargo/config.toml</code>.
30//! </div>
31//!
32//! For more details, see [Tokio's documentation on unstable
33//! features][unstable features].
34//!
35//! ### Adding Runtime Hooks
36//!
37//! Once Tokio's unstable features are enabled, `tokio-dtrace`'s runtime hooks
38//! must be added to the application's Tokio runtime. The easiest way to do this
39//! is to pass a mutable reference to the application's
40//! [`tokio::runtime::Builder`] to the
41//! [`tokio_dtrace::register_hooks`](crate::register_hooks) function.
42//!
43//! For example:
44//! ```
45//! // Construct a new Tokio runtime builder.
46//! let mut builder = tokio::runtime::Builder::new_multi_thread();
47//!
48//! // Attempt to register `tokio-dtrace` hooks with the runtime.
49//! let rt = tokio_dtrace::register_hooks(&mut builder)
50//!     // `register_hooks` returns an error if DTrace probes could
51//!     // not be enabled.
52//!     .unwrap()
53//!     // Enable other Tokio runtime features, and configure other builder
54//!     // settings as needed...
55//!     .enable_all()
56//!     .build()
57//!     .unwrap();
58//!
59//! // ... Use the configured runtime in your application ...
60//! # drop(rt);
61//! ```
62//!
63//! Note that, because `tokio-dtrace` requires the use of the
64//! [`tokio::runtime::Builder`] to add hooks to the runtime, it is not possible
65//! to use `tokio-dtrace` with the [`tokio::main`] attribute macro.
66//! Fortunately, code using `#[tokio::main]` can be transformed to code using
67//! the runtime builder fairly simply.
68//!
69//! For example, this:
70//!
71//! ```rust
72//! # async fn do_stuff() {}
73//! #[tokio::main(flavor = "multi_thread", worker_threads = 10)]
74//! async fn main() {
75//!     do_stuff().await;
76//! }
77//! ```
78//!
79//! becomes this:
80//!
81//!```rust
82//! # async fn do_stuff() {}
83//! fn main() {
84//!     let mut builder = tokio::runtime::Builder::new_multi_thread();
85//!     tokio_dtrace::register_hooks(&mut builder).unwrap();
86//!     let rt = builder.enable_all().worker_threads(10).build().unwrap();
87//!     rt.block_on(async {
88//!         tokio::spawn(async {
89//!             do_stuff().await;
90//!         })
91//!         .await
92//!         .unwrap()
93//!     });
94//! }
95//! ```
96//!
97//! See the documentation for
98//! [`tokio_dtrace::register_hooks`](crate::register_hooks) for more
99//! information.
100//!
101//! Note the `register_hooks` function sets a function to be called in all of
102//! Tokio's unstable runtime hook callbacks. If additional code must also be
103//! called in one or more of these hooks, refer to the documentation for the
104//! [`hooks`] module for more complex uses.
105//!
106//! [unstable features]: https://docs.rs/tokio/latest/tokio/#unstable-features
107//! [`tokio::main`]: https://docs.rs/tokio/latest/tokio/attr.main.html
108//!
109use std::num::NonZeroU64;
110
111/// Registers `tokio-dtrace`s probe hooks with the provided
112/// [`tokio::runtime::Builder`].
113///
114/// This function configures the runtime to emit DTrace probes. In addition,
115/// it also registers the USDT probes provided by this crate with DTrace, and
116/// calls the [`check_casts`] function to ensure that type layout in Tokio has
117/// not changed in a way that would render unsafe casts used by `tokio-dtrace`
118/// unsound.
119///
120/// Note that this sets a function to be called in all of Tokio's unstable
121/// runtime hook callbacks. If additional code must also be called in one or
122/// more of these hooks, refer to the documentation for the [`hooks`] module for
123/// more complex uses.
124///
125/// ## Errors
126///
127/// This function returns [an error](RegistrationError) in the following
128/// conditions:
129///
130/// - [`RegistrationError::UnstableFeaturesRequired`] if Tokio's
131///   [unstable features](crate#enabling-tokio_unstable-features) are not
132///   enabled at compile time.
133/// - [`RegistrationError::DTrace`] if the [`usdt`] crate returns an error
134///   when registering probes with DTrace.
135/// - [`RegistrationError::InvalidCasts`] if a call to [`check_casts`] fails,
136///   which would indicate that type layout in Tokio has changed in a way that
137///   would render unsafe casts used by `tokio-dtrace` unsound.
138///
139/// ## Examples
140///
141/// Basic usage:
142///
143/// ```
144/// fn main() -> Result<(), Box<dyn std::error::Error>> {
145///     // Construct a new Tokio runtime builder.
146///     let mut builder = tokio::runtime::Builder::new_multi_thread();
147///     // Attempt to register `tokio-dtrace` hooks with the runtime.
148///     tokio_dtrace::register_hooks(&mut builder)?;
149///
150///     let rt = builder
151///         // Enable other Tokio runtime features, and configure other builder
152///         // settings as needed...
153///         .enable_all()
154///         // Finish building the runtime.
155///         .build()?;
156///
157///     // Use the constructed runtime to run the application.
158///     rt.block_on(async {
159///         // Your application code here
160///     });
161///
162///     Ok(())
163/// }
164/// ```
165///
166/// In some cases, such as when the application may run in environments where
167/// DTrace may not be available, it may be desirable to allow `tokio-dtrace`
168/// registration to fail, rather than exiting. For example:
169///
170/// ```
171/// fn main() -> Result<(), Box<dyn std::error::Error>> {
172///     let mut builder = tokio::runtime::Builder::new_multi_thread();
173///
174///     if let Err(e) = tokio_dtrace::register_hooks(&mut builder) {
175///         // If registering tokio-dtrace hooks fails, print a warning
176///         // message, but continue running the application.
177///         eprintln!("WARNING: could not register Tokio DTrace probes: {e}");
178///     }
179///
180///     let rt = builder
181///         .enable_all()
182///         .build()?;
183///
184///     rt.block_on(async {
185///         // Your application code here
186///     });
187///
188///     Ok(())
189/// }
190/// ```
191pub fn register_hooks(
192    builder: &mut tokio::runtime::Builder,
193) -> Result<&mut tokio::runtime::Builder, RegistrationError> {
194    #[cfg(tokio_unstable)]
195    {
196        check_casts()?;
197        usdt::register_probes()?;
198        let builder = builder
199            .on_thread_start(hooks::on_thread_start)
200            .on_thread_park(hooks::on_thread_park)
201            .on_thread_unpark(hooks::on_thread_unpark)
202            .on_thread_stop(hooks::on_thread_stop)
203            .on_task_spawn(hooks::on_task_spawn)
204            .on_before_task_poll(hooks::on_before_task_poll)
205            .on_after_task_poll(hooks::on_after_task_poll)
206            .on_task_terminate(hooks::on_task_terminate);
207        Ok(builder)
208    }
209    #[cfg(not(tokio_unstable))]
210    {
211        Err(RegistrationError::UnstableFeaturesRequired)
212    }
213}
214
215/// Errors returned by [`register_hooks`].
216#[derive(Debug, thiserror::Error)]
217pub enum RegistrationError {
218    /// `tokio-dtrace` hooks cannot be registered, as [Tokio's unstable
219    /// features][unstable] are not enabled.
220    ///
221    /// [unstable]: crate#enabling-tokio_unstable-features
222    #[error("tokio-dtrace requires `RUSTFLAGS=\"--cfg tokio_unstable\"`")]
223    UnstableFeaturesRequired,
224
225    /// `tokio-dtrace` hooks were not registered as the layout of Tokio's
226    /// [`tokio::task::Id`] type has changed. See [`check_casts`] for details.
227    #[error(transparent)]
228    InvalidCasts(#[from] InvalidCasts),
229
230    /// Probes could not be registered with DTrace.
231    #[error(transparent)]
232    DTrace(#[from] usdt::Error),
233}
234
235/// Errors returned by [`check_casts`].
236#[derive(Debug, thiserror::Error)]
237#[error(
238    "\
239tokio-dtrace: POTENTIALLY UNSOUND CAST DETECTED!\n \
240  size_of::<tokio::task::Id>() = {id_size}\n       \
241       size_of::<NonZeroU64>() = {nonzero_u64_size}\n \
242 align_of::<tokio::task::Id>() = {id_align}\n      \
243      align_of::<NonZeroU64>() = {nonzero_u64_align}\n\
244"
245)]
246pub struct InvalidCasts {
247    id_size: usize,
248    nonzero_u64_size: usize,
249    id_align: usize,
250    nonzero_u64_align: usize,
251}
252
253/// Checks that unsafe casts performed by `tokio-dtrace` are valid.
254///
255/// `tokio-dtrace` relies on the ability to cast a [`tokio::task::Id`] to a
256/// [`u64`] in order to pass a task ID to DTrace probes as an integer. This
257/// function checks that the sizes and alignments of the types are compatible. If
258/// they are not, it returns an error, indicating that the casts are potentially
259/// unsound. This may occur if Tokio has changed the representation of the
260/// [`tokio::task::Id`] type, which is unlikely, but always possible.
261///
262/// If this function returns an error, `tokio-dtrace`'s runtime hooks should not
263/// be used. Registering hooks using the [`register_hooks`] function will call
264/// this function prior to registering the runtime hooks, and will fail to do so
265/// if casts are unsound.
266pub fn check_casts() -> Result<(), InvalidCasts> {
267    use std::mem::{align_of, size_of};
268
269    let id_size = size_of::<tokio::task::Id>();
270    let nonzero_u64_size = size_of::<NonZeroU64>();
271    let id_align = align_of::<tokio::task::Id>();
272    let nonzero_u64_align = align_of::<NonZeroU64>();
273
274    if id_size != nonzero_u64_size || id_align != nonzero_u64_align {
275        Err(InvalidCasts {
276            id_size,
277            nonzero_u64_size,
278            id_align,
279            nonzero_u64_align,
280        })
281    } else {
282        Ok(())
283    }
284}
285
286/// Tokio runtime hooks for DTrace probes.
287///
288/// This module contains functions that are called by the Tokio runtime when
289/// events occur in order to emit DTrace probes for those events. They are
290/// intended to be passed to a [`tokio::runtime::Builder`]'s methods that set
291/// the hook functions called by the runtime being constructed:
292///
293/// - [`tokio::runtime::Builder::on_task_spawn`]
294/// - [`tokio::runtime::Builder::on_before_task_poll`]
295/// - [`tokio::runtime::Builder::on_after_task_poll`]
296/// - [`tokio::runtime::Builder::on_task_terminate`]
297/// - [`tokio::runtime::Builder::on_thread_start`]
298/// - [`tokio::runtime::Builder::on_thread_stop`]
299/// - [`tokio::runtime::Builder::on_thread_park`]
300/// - [`tokio::runtime::Builder::on_thread_unpark`]
301///
302/// Typically, users of `tokio-dtrace` need not interact with these functions
303/// directly: the [`register_hooks`] function can be used to register all
304/// runtime hooks in a single function call. However, the Tokio runtime only
305/// allows a *single* function to be set for each of these hooks. Therefore, if
306/// the application needs to run other code in addition to `tokio-dtrace`'s
307/// probes in one or more runtime hooks, it is necessary to write a wrapper
308/// function that calls the `tokio-dtrace` hooks as well as any other code that
309/// must run in the hook callback.
310///
311/// For instance, if I have some function `other_on_task_spawn_thing()` that I
312/// would like to run in the
313/// [`on_task_spawn`](tokio::runtime::Builder::on_task_spawn) hook, I can write
314/// a wrapper function like this:
315///
316/// ```rust
317/// use tokio::runtime::TaskMeta;
318/// # fn other_on_task_spawn_thing(meta: &TaskMeta<'_>) {};
319///
320/// fn on_task_spawn(meta: &TaskMeta<'_>) {
321///     // Call the tokio-dtrace hook.
322///     tokio_dtrace::hooks::on_task_spawn(meta);
323///     // Call the other function that should run in the `on_task_spawn` hook.
324///     other_on_task_spawn_thing(meta);
325/// }
326///
327/// fn main() -> Result<(), Box<dyn std::error::Error>> {
328///     // Check that tokio-dtrace's casts are valid, and register DTrace probes.
329///     tokio_dtrace::check_casts()?;
330///     usdt::register_probes()?;
331///
332///     // Construct a new Tokio runtime builder.
333///     let rt = tokio::runtime::Builder::new_multi_thread()
334///         // Register the `on_task_spawn` hook defined above.
335///         .on_task_spawn(on_task_spawn)
336///         // Register `tokio-dtrace`'s other hooks.
337///         .on_before_task_poll(tokio_dtrace::hooks::on_before_task_poll)
338///         .on_after_task_poll(tokio_dtrace::hooks::on_after_task_poll)
339///         .on_task_terminate(tokio_dtrace::hooks::on_task_terminate)
340///         .on_thread_start(tokio_dtrace::hooks::on_thread_start)
341///         .on_thread_stop(tokio_dtrace::hooks::on_thread_stop)
342///         .on_thread_park(tokio_dtrace::hooks::on_thread_park)
343///         .on_thread_unpark(tokio_dtrace::hooks::on_thread_unpark)
344///         .on_thread_stop(tokio_dtrace::hooks::on_thread_stop)
345///         // Enable other Tokio runtime features, and configure other builder
346///         // settings as needed...
347///         .enable_all()
348///         // Finish building the runtime.
349///         .build()?;
350///
351///     rt.block_on(async {
352///         // Your application code here
353///     });
354///
355///     Ok(())
356/// }
357/// ```
358///
359/// If only a small number of hooks need to call additional functions in this
360/// manner, it is also possible to use the [`tokio_dtrace::register_hooks`]
361/// function, and then override a subset of the hooks with user-defined
362/// functions. Note that the calls to [`tokio_dtrace::register_hooks`] should be
363/// made *before* overriding any hooks with user-defined code, as calling the
364/// runtime builder method *overrides* any previously registered function for
365/// that hook.
366///
367/// In that case, the previous example reduces to:
368///
369/// ```rust
370/// use tokio::runtime::TaskMeta;
371/// # fn other_on_task_spawn_thing(meta: &TaskMeta<'_>) {};
372///
373/// fn on_task_spawn(meta: &TaskMeta<'_>) {
374///     // Call the tokio-dtrace hook.
375///     tokio_dtrace::hooks::on_task_spawn(meta);
376///     // Call the other function that should run in the `on_task_spawn` hook.
377///     other_on_task_spawn_thing(meta);
378/// }
379///
380/// fn main() -> Result<(), Box<dyn std::error::Error>> {
381///     let mut builder  = tokio::runtime::Builder::new_multi_thread();
382///     // Register the default `tokio-dtrace` hooks.
383///     //
384///     // Since `register_hooks` also checks for cast validitiy and registers
385///     // DTrace probes, it is not necessary to call those functions
386///     // separately this time.
387///     let rt = tokio_dtrace::register_hooks(&mut builder)?
388///         // Override *just* the `on_task_spawn` hook.
389///         .on_task_spawn(on_task_spawn)
390///         // Enable other Tokio runtime features, and configure other builder
391///         // settings as needed...
392///         .enable_all()
393///         // Finish building the runtime.
394///         .build()?;
395///
396///     rt.block_on(async {
397///         // Your application code here
398///     });
399///
400///     Ok(())
401/// }
402/// ```
403///
404/// [`tokio_dtrace::register_hooks`]: crate::register_hooks
405#[cfg(tokio_unstable)]
406pub mod hooks {
407    use super::*;
408    use tokio::runtime::TaskMeta;
409
410    /// Hook function to be used in [`tokio::runtime::Builder::on_task_spawn`].
411    pub fn on_task_spawn(meta: &TaskMeta<'_>) {
412        probes::task__spawn!(|| unpack_meta(meta));
413    }
414
415    /// Hook function to be used in [`tokio::runtime::Builder::on_before_task_poll`].
416    pub fn on_before_task_poll(meta: &TaskMeta<'_>) {
417        probes::task__poll__start!(|| unpack_meta(meta));
418    }
419
420    /// Hook function to be used in [`tokio::runtime::Builder::on_after_task_poll`].
421    pub fn on_after_task_poll(meta: &TaskMeta<'_>) {
422        probes::task__poll__end!(|| unpack_meta(meta));
423    }
424
425    /// Hook function to be used in [`tokio::runtime::Builder::on_task_terminate`].
426    pub fn on_task_terminate(meta: &TaskMeta<'_>) {
427        probes::task__terminate!(|| unpack_meta(meta));
428    }
429
430    /// Hook function to be used in [`tokio::runtime::Builder::on_thread_start`].
431    pub fn on_thread_start() {
432        probes::worker__thread__start!(|| ());
433    }
434
435    /// Hook function to be used in [`tokio::runtime::Builder::on_thread_stop`].
436    pub fn on_thread_stop() {
437        probes::worker__thread__stop!(|| ());
438    }
439
440    /// Hook function to be used in [`tokio::runtime::Builder::on_thread_park`].
441    pub fn on_thread_park() {
442        probes::worker__thread__park!(|| ());
443    }
444
445    /// Hook function to be used in [`tokio::runtime::Builder::on_thread_unpark`].
446    pub fn on_thread_unpark() {
447        probes::worker__thread__unpark!(|| ());
448    }
449
450    #[inline]
451    fn unpack_meta(meta: &TaskMeta<'_>) -> (u64, String, u32, u32) {
452        let id = id_to_u64(meta.id());
453        let location = meta.spawned_at();
454        let file = location.file().to_string();
455        let line = location.line();
456        let col = location.column();
457        (id, file, line, col)
458    }
459
460    #[inline]
461    fn id_to_u64(id: tokio::task::Id) -> u64 {
462        unsafe {
463            // SAFETY: Based on training and experience, I know that a
464            // `tokio::task::Id` is represented as a single `NonZeroU64`.
465            union TrustMeOnThis {
466                id: tokio::task::Id,
467                int: NonZeroU64,
468            }
469            TrustMeOnThis { id }.int.get()
470        }
471    }
472}
473
474#[usdt::provider(provider = "tokio")]
475#[allow(non_snake_case)]
476mod probes {
477    fn task__spawn(task_id: u64, file: String, line: u32, col: u32) {}
478    fn task__poll__start(task_id: u64, file: String, line: u32, col: u32) {}
479    fn task__poll__end(task_id: u64, file: String, line: u32, col: u32) {}
480    fn task__terminate(task_id: u64, file: String, line: u32, col: u32) {}
481
482    fn worker__thread__start() {}
483    fn worker__thread__stop() {}
484    fn worker__thread__park() {}
485    fn worker__thread__unpark() {}
486}
487
488#[cfg(test)]
489mod tests {
490    #[test]
491    fn casts_are_valid() {
492        crate::check_casts().unwrap();
493    }
494}