tracing_tracy/
lib.rs

1//! Collect [Tracy] profiles in tracing-enabled applications.
2//!
3//! Assuming the application is well instrumented, this should in practice be a very low effort way
4//! to gain great amounts of insight into an application performance.
5//!
6//! Note, however that Tracy is ultimately a profiling, not an observability, tool. As thus, some
7//! of tracing concepts cannot be represented well by Tracy. For instance, out-of-order span
8//! entries and exits, are not supported, and neither are spans that are entered and exited on
9//! different threads. This crate will attempt to mitigate the problems and retain trace validity
10//! at the cost of potentially invalid data. When such a mitigation occurs, trace will contain a
11//! message with a note about the problem.
12//!
13//! Some other caveats to keep in mind:
14//!
15//! * Only span entries and exits are recorded;
16//! * Events show up as messages in Tracy, however Tracy can struggle with large numbers of
17//! messages;
18//! * Some additional functionality such as plotting and memory allocation profiling is only
19//! available as part of the [tracy-client](client) crate.
20//!
21//! # Examples
22//!
23//! The most basic way to setup the tracy subscriber globally is as follows:
24//!
25//! ```rust
26//! use tracing_subscriber::layer::SubscriberExt;
27//!
28//! tracing::subscriber::set_global_default(
29//!     tracing_subscriber::registry().with(tracing_tracy::TracyLayer::default())
30//! ).expect("setup tracy layer");
31//! ```
32//!
33//! # Important note
34//!
35//! Depending on the configuration Tracy may broadcast discovery packets to the local network and
36//! expose the data it collects in the background to that same network. Traces collected by Tracy
37//! may include source and assembly code as well.
38//!
39//! As thus, you may want make sure to only enable the `tracing-tracy` crate conditionally, via the
40//! `enable` feature flag provided by this crate.
41//!
42//! [Tracy]: https://github.com/wolfpld/tracy
43//!
44//! # Features
45//!
46//! The following crate features are provided to customize the functionality of the Tracy client:
47//!
48#![doc = include_str!("../FEATURES.mkd")]
49#![cfg_attr(tracing_tracy_docs, feature(doc_auto_cfg))]
50
51use client::{Client, Span};
52pub use config::{Config, DefaultConfig};
53use std::sync::atomic::{AtomicUsize, Ordering};
54use std::{fmt::Write, mem};
55use tracing_core::{
56    field::{Field, Visit},
57    span::{Attributes, Id, Record},
58    Event, Subscriber,
59};
60use tracing_subscriber::fmt::format::FormatFields;
61use tracing_subscriber::{
62    layer::{Context, Layer},
63    registry,
64};
65use utils::{StrCache, StrCacheGuard, VecCell};
66
67pub use client;
68mod config;
69
70type TracyFields<C> = tracing_subscriber::fmt::FormattedFields<<C as Config>::Formatter>;
71
72thread_local! {
73    /// A stack of spans currently active on the current thread.
74    static TRACY_SPAN_STACK: VecCell<(Span, u64)> = const { VecCell::new() };
75}
76
77/// A tracing layer that collects data in Tracy profiling format.
78///
79/// # Examples
80///
81/// ```rust
82/// use tracing_subscriber::layer::SubscriberExt;
83/// tracing::subscriber::set_global_default(
84///     tracing_subscriber::registry().with(tracing_tracy::TracyLayer::default())
85/// ).expect("setup tracy layer");
86/// ```
87#[derive(Clone)]
88pub struct TracyLayer<C = DefaultConfig> {
89    config: C,
90    client: Client,
91}
92
93impl<C> TracyLayer<C> {
94    /// Create a new `TracyLayer`.
95    ///
96    /// Defaults to collecting stack traces.
97    #[must_use]
98    pub fn new(config: C) -> Self {
99        Self {
100            config,
101            client: Client::start(),
102        }
103    }
104}
105
106impl<C: Config> TracyLayer<C> {
107    fn truncate_span_to_length<'a>(
108        &self,
109        data: &'a str,
110        file: &str,
111        function: &str,
112        error_msg: &'static str,
113    ) -> &'a str {
114        self.truncate_to_length(
115            // From AllocSourceLocation
116            usize::from(u16::MAX) - 2 - 4 - 4 - function.len() - 1 - file.len() - 1,
117            data,
118            error_msg,
119        )
120    }
121
122    fn truncate_to_length<'a>(
123        &self,
124        mut max_len: usize,
125        data: &'a str,
126        error_msg: &'static str,
127    ) -> &'a str {
128        if data.len() >= max_len {
129            while !data.is_char_boundary(max_len) {
130                max_len -= 1;
131            }
132            self.config.on_error(&self.client, error_msg);
133            &data[..max_len]
134        } else {
135            data
136        }
137    }
138}
139
140impl Default for TracyLayer {
141    fn default() -> Self {
142        Self::new(DefaultConfig::default())
143    }
144}
145
146static MAX_CACHE_SIZE: AtomicUsize = AtomicUsize::new(8192);
147
148/// Specify the maximum number of bytes used in thread local caches.
149///
150/// A value of zero disables the cache, while a value of [`usize::MAX`] denotes an unlimited
151/// cache size.
152///
153/// Note: the upper bound on the cache size is respected on a best effort basis only. We make
154/// no guarantees on the maximum memory used by tracing-tracy. Notably, changes to this value
155/// are eventually consistent, i.e. caches are not flushed upon an update.
156///
157/// Defaults to `8192` per thread.
158pub fn set_max_cache_size(max_bytes_used_per_thread: usize) {
159    MAX_CACHE_SIZE.store(max_bytes_used_per_thread, Ordering::Relaxed);
160}
161
162thread_local! {
163    static CACHE: StrCache = const { StrCache::new() };
164}
165
166impl<S, C> Layer<S> for TracyLayer<C>
167where
168    S: Subscriber + for<'a> registry::LookupSpan<'a>,
169    C: Config + 'static,
170{
171    fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
172        let Some(span) = ctx.span(id) else { return };
173
174        let mut extensions = span.extensions_mut();
175        if extensions.get_mut::<TracyFields<C>>().is_none() {
176            let mut fields =
177                TracyFields::<C>::new(CACHE.with(|cache| cache.acquire().into_inner()));
178            if self
179                .config
180                .formatter()
181                .format_fields(fields.as_writer(), attrs)
182                .is_ok()
183            {
184                extensions.insert(fields);
185            }
186        }
187    }
188
189    fn on_record(&self, id: &Id, values: &Record<'_>, ctx: Context<'_, S>) {
190        let Some(span) = ctx.span(id) else { return };
191
192        let mut extensions = span.extensions_mut();
193        if let Some(fields) = extensions.get_mut::<TracyFields<C>>() {
194            let _ = self.config.formatter().add_fields(fields, values);
195        } else {
196            let mut fields =
197                TracyFields::<C>::new(CACHE.with(|cache| cache.acquire().into_inner()));
198            if self
199                .config
200                .formatter()
201                .format_fields(fields.as_writer(), values)
202                .is_ok()
203            {
204                extensions.insert(fields);
205            }
206        }
207    }
208
209    fn on_event(&self, event: &Event, _: Context<'_, S>) {
210        CACHE.with(|cache| {
211            let mut buf = cache.acquire();
212            let mut visitor = TracyEventFieldVisitor {
213                dest: &mut buf,
214                first: true,
215                frame_mark: false,
216            };
217
218            event.record(&mut visitor);
219            if !visitor.first {
220                self.client.message(
221                    self.truncate_to_length(
222                        (u16::MAX - 1).into(),
223                        visitor.dest,
224                        "event message is too long and was truncated",
225                    ),
226                    self.config.stack_depth(event.metadata()),
227                );
228            }
229            if visitor.frame_mark {
230                self.client.frame_mark();
231            }
232        });
233    }
234
235    fn on_enter(&self, id: &Id, ctx: Context<S>) {
236        let Some(span) = ctx.span(id) else { return };
237
238        let extensions = span.extensions();
239        let fields = extensions.get::<TracyFields<C>>();
240        let stack_frame = {
241            let metadata = span.metadata();
242            let file = metadata.file().unwrap_or("<not available>");
243            let line = metadata.line().unwrap_or(0);
244            let span = |name: &str| {
245                (
246                    self.client.clone().span_alloc(
247                        Some(self.truncate_span_to_length(
248                            name,
249                            file,
250                            "",
251                            "span information is too long and was truncated",
252                        )),
253                        "",
254                        file,
255                        line,
256                        self.config.stack_depth(metadata),
257                    ),
258                    id.into_u64(),
259                )
260            };
261
262            match fields {
263                None => span(metadata.name()),
264                Some(fields) if fields.is_empty() => span(metadata.name()),
265                Some(fields) if self.config.format_fields_in_zone_name() => CACHE.with(|cache| {
266                    let mut buf = cache.acquire();
267                    let _ = write!(buf, "{}{{{}}}", metadata.name(), fields.fields);
268                    span(&buf)
269                }),
270                Some(fields) => {
271                    let span = span(metadata.name());
272                    span.0.emit_text(self.truncate_to_length(
273                        (u16::MAX - 1).into(),
274                        &fields.fields,
275                        "span field values are too long and were truncated",
276                    ));
277                    span
278                }
279            }
280        };
281
282        TRACY_SPAN_STACK.with(|s| {
283            s.push(stack_frame);
284        });
285    }
286
287    fn on_exit(&self, id: &Id, _: Context<S>) {
288        let stack_frame = TRACY_SPAN_STACK.with(VecCell::pop);
289
290        if let Some((span, span_id)) = stack_frame {
291            if id.into_u64() != span_id {
292                self.config.on_error(
293                    &self.client,
294                    "Tracing spans exited out of order! \
295                        Trace might not be accurate for this span stack.",
296                );
297            }
298            drop(span);
299        } else {
300            self.config.on_error(
301                &self.client,
302                "Exiting a tracing span, but got nothing on the tracy span stack!",
303            );
304        }
305    }
306
307    fn on_close(&self, id: Id, ctx: Context<'_, S>) {
308        let Some(span) = ctx.span(&id) else { return };
309
310        if let Some(fields) = span.extensions_mut().get_mut::<TracyFields<C>>() {
311            let buf = mem::take(&mut fields.fields);
312            CACHE.with(|cache| drop(StrCacheGuard::new(cache, buf)));
313        };
314    }
315}
316
317struct TracyEventFieldVisitor<'a> {
318    dest: &'a mut String,
319    frame_mark: bool,
320    first: bool,
321}
322
323impl Visit for TracyEventFieldVisitor<'_> {
324    fn record_bool(&mut self, field: &Field, value: bool) {
325        match (value, field.name()) {
326            (_, "tracy.frame_mark") => self.frame_mark = value,
327            (true, _) => self.record_str(field, "true"),
328            (false, _) => self.record_str(field, "false"),
329        }
330    }
331
332    fn record_str(&mut self, field: &Field, value: &str) {
333        let name = field.name();
334        let alloc_always_size = name.len() + " = ".len() + value.len();
335        if self.first {
336            self.dest.reserve(alloc_always_size);
337            self.first = false;
338        } else {
339            self.dest.reserve(", ".len() + alloc_always_size);
340            self.dest.push_str(", ");
341        }
342
343        self.dest.push_str(name);
344        self.dest.push_str(" = ");
345        self.dest.push_str(value);
346    }
347
348    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
349        // FIXME: this is a very crude formatter, but we don’t have
350        // an easy way to do anything better...
351        if self.first {
352            self.first = false;
353            let _ = write!(self.dest, "{} = {value:?}", field.name());
354        } else {
355            let _ = write!(self.dest, ", {} = {value:?}", field.name());
356        }
357    }
358}
359
360#[cfg(test)]
361mod tests;
362#[cfg(test)]
363fn main() {
364    if std::env::args_os().any(|p| p == std::ffi::OsStr::new("--bench")) {
365        tests::bench();
366    } else {
367        tests::test();
368    }
369}
370
371mod utils {
372    use crate::MAX_CACHE_SIZE;
373    use std::cell::{Cell, UnsafeCell};
374    use std::mem;
375    use std::mem::ManuallyDrop;
376    use std::ops::{Deref, DerefMut};
377    use std::sync::atomic::Ordering;
378
379    pub struct VecCell<T>(UnsafeCell<Vec<T>>);
380
381    impl<T> VecCell<T> {
382        pub const fn new() -> Self {
383            Self(UnsafeCell::new(Vec::new()))
384        }
385
386        pub fn push(&self, item: T) {
387            // SAFETY:
388            // The reference to the contents of the UnsafeCell remain strictly within this method.
389            // In addition, this method is not re-entrant.
390            unsafe { &mut *self.0.get() }.push(item);
391        }
392
393        pub fn pop(&self) -> Option<T> {
394            // SAFETY:
395            // The reference to the contents of the UnsafeCell remain strictly within this method.
396            // In addition, this method is not re-entrant.
397            unsafe { &mut *self.0.get() }.pop()
398        }
399    }
400
401    pub struct StrCache {
402        str_bufs: VecCell<String>,
403        total_size: Cell<usize>,
404    }
405
406    impl StrCache {
407        pub const fn new() -> Self {
408            Self {
409                str_bufs: VecCell::new(),
410                total_size: Cell::new(0),
411            }
412        }
413
414        pub fn acquire(&self) -> StrCacheGuard {
415            StrCacheGuard::new(
416                self,
417                self.str_bufs
418                    .pop()
419                    // TODO use inspect once 1.76 is stable
420                    .map(|buf| {
421                        self.total_size.set(self.total_size.get() - buf.capacity());
422                        buf
423                    })
424                    .unwrap_or_else(|| String::with_capacity(64)),
425            )
426        }
427
428        fn release(&self, mut buf: String) {
429            let new_cache_size = self.total_size.get().saturating_add(buf.capacity());
430            if new_cache_size == usize::MAX {
431                // This is never going to happen, but if we've used the entire address space,
432                // don't bother adding another cache entry as this keeps the logic simpler.
433                return;
434            };
435            let max_size = MAX_CACHE_SIZE.load(Ordering::Relaxed);
436            if buf.capacity() == 0 || max_size == 0 {
437                return;
438            }
439
440            buf.clear();
441            self.str_bufs.push(buf);
442            self.total_size.set(new_cache_size);
443
444            if new_cache_size > max_size {
445                // SAFETY:
446                // Sorting is not re-entrant and VecCell does not hold active references. Since we
447                // hold a reference for the duration of this line only, we do not alias.
448                unsafe { &mut *self.str_bufs.0.get() }.sort_unstable_by_key(String::capacity);
449                if let Some(trimmed) = self.str_bufs.pop() {
450                    self.total_size
451                        .set(self.total_size.get() - trimmed.capacity());
452                }
453            }
454        }
455    }
456
457    pub struct StrCacheGuard<'a> {
458        cache: &'a StrCache,
459        buf: String,
460    }
461
462    impl<'a> StrCacheGuard<'a> {
463        pub fn new(cache: &'a StrCache, buf: String) -> Self {
464            Self { cache, buf }
465        }
466
467        pub fn into_inner(self) -> String {
468            let mut this = ManuallyDrop::new(self);
469            mem::take(&mut this.buf)
470        }
471    }
472
473    impl Deref for StrCacheGuard<'_> {
474        type Target = String;
475
476        fn deref(&self) -> &Self::Target {
477            &self.buf
478        }
479    }
480
481    impl DerefMut for StrCacheGuard<'_> {
482        fn deref_mut(&mut self) -> &mut Self::Target {
483            &mut self.buf
484        }
485    }
486
487    impl Drop for StrCacheGuard<'_> {
488        fn drop(&mut self) {
489            self.cache.release(mem::take(&mut self.buf));
490        }
491    }
492}