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}