Skip to content

tracing-tracy: replace baked-in config fields with a Config trait #91

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions tracing-tracy/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use client::Client;
use tracing_subscriber::fmt::format::DefaultFields;
use tracing_subscriber::fmt::FormatFields;

/// Configuration of the [`TracyLayer`](super::TracyLayer) behaviour.
///
/// For most users [`DefaultConfig`] is going to be a good default choice, however advanced users
/// can implement this trait manually to override the formatter used or to otherwise modify the
/// behaviour of the `TracyLayer`.
///
/// # Examples
///
/// ```
/// use tracing_subscriber::fmt::format::DefaultFields;
///
/// struct TracyLayerConfig {
/// fmt: DefaultFields,
/// }
/// impl tracing_tracy::Config for TracyLayerConfig {
/// type Formatter = DefaultFields;
/// fn formatter(&self) -> &Self::Formatter {
/// &self.fmt
/// }
/// // The boilerplate ends here
///
/// /// Collect 32 frames in stack traces.
/// fn stack_depth(&self, _: &tracing::Metadata) -> u16 {
/// 32
/// }
///
/// /// Do not format fields into zone names.
/// fn format_fields_in_zone_name(&self) -> bool {
/// false
/// }
///
/// // etc.
/// }
/// ```
///
/// With this configuration `TracyLayer` will collect some call stacks and the formatting of the
/// zone names is different from the `DefaultConfig`.
pub trait Config {
type Formatter: for<'writer> FormatFields<'writer> + 'static;

/// Use a custom field formatting implementation.
fn formatter(&self) -> &Self::Formatter;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wish there was a way to specify a default here. Sadly not much progress: https://rust-lang.github.io/rfcs/2532-associated-type-defaults.html

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wish it was possible too, however even with ATDs it wouldn’t be possible as the method returns a reference to &self and traits do not have access to fields. Achieving this would need something like rust-lang/rfcs#1546.


/// Specify the maximum number of stack frames that will be collected.
///
/// Note that enabling callstack collection can and will introduce a non-trivial overhead at
/// every instrumentation point. Specifying 0 frames will disable stack trace collection.
///
/// Default implementation returns `0`.
fn stack_depth(&self, metadata: &tracing_core::Metadata<'_>) -> u16 {
let _ = metadata;
0
}

/// Specify whether or not to include tracing span fields in the tracy zone name, or to emit
/// them as zone text.
///
/// The former enables zone analysis along unique span field invocations, while the latter
/// aggregates every invocation of a given span into a single zone, irrespective of field
/// values.
///
/// Default implementation returns `true`.
fn format_fields_in_zone_name(&self) -> bool {
true
}

/// Apply handling for errors detected by the [`TracyLayer`](super::TracyLayer).
///
/// Fundamentally the way the tracing crate and the Tracy profiler work are somewhat
/// incompatible in certain ways. For instance, a [`tracing::Span`] can be created on one
/// thread and moved to another, where it is cleaned up. Tracy on the other hand expects that
/// its eqvivalent concept of zone remains entirely within a thread.
///
/// Another example a limitation in `Tracy` where the message length or zone name cannot exceed
/// a certain (low) limit of bytes.
///
/// Although `tracing_tracy` does it best to paper over these sorts of differences, it can’t
/// always make them invisible. In certain cases detecting these sorts of issues is
/// straightforward, and it is when `tracing_tracy` will invoke this method to enable users to
/// report the issues in whatever way they wish to.
///
/// By default a message coloured in red is emitted to the tracy client.
fn on_error(&self, client: &Client, error: &'static str) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooooooh, this is quite a nice flexibility improvement.

client.color_message(error, 0xFF000000, 0);
}
}

/// A default configuration of the [`TracyLayer`](super::TracyLayer).
///
/// This type does not allow for any adjustment of the configuration. In order to customize
/// the behaviour of the layer implement the [`Config`] trait for your own type.
#[derive(Default)]
pub struct DefaultConfig(DefaultFields);

impl Config for DefaultConfig {
type Formatter = DefaultFields;
fn formatter(&self) -> &Self::Formatter {
&self.0
}
}
133 changes: 54 additions & 79 deletions tracing-tracy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@
//! use tracing_subscriber::layer::SubscriberExt;
//!
//! tracing::subscriber::set_global_default(
//! tracing_subscriber::registry()
//! .with(tracing_tracy::TracyLayer::new()),
//! ).expect("set up the subscriber");
//! tracing_subscriber::registry().with(tracing_tracy::TracyLayer::default())
//! ).expect("setup tracy layer");
//! ```
//!
//! # Important note
Expand All @@ -49,93 +48,62 @@
#![doc = include_str!("../FEATURES.mkd")]
#![cfg_attr(tracing_tracy_docs, feature(doc_auto_cfg))]

use client::{Client, Span};
pub use config::{Config, DefaultConfig};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::{fmt::Write, mem};
use tracing_core::{
field::{Field, Visit},
span::{Attributes, Id, Record},
Event, Subscriber,
};
use tracing_subscriber::fmt::format::{DefaultFields, FormatFields};
use tracing_subscriber::fmt::format::FormatFields;
use tracing_subscriber::{
fmt::FormattedFields,
layer::{Context, Layer},
registry,
};

use client::{Client, Span};
use utils::{StrCache, StrCacheGuard, VecCell};

pub use client;
mod config;

type TracyFields<C> = tracing_subscriber::fmt::FormattedFields<<C as Config>::Formatter>;

thread_local! {
/// A stack of spans currently active on the current thread.
static TRACY_SPAN_STACK: VecCell<(Span, u64)> = const { VecCell::new() };
}

/// A tracing layer that collects data in Tracy profiling format.
///
/// # Examples
///
/// ```rust
/// use tracing_subscriber::layer::SubscriberExt;
/// tracing::subscriber::set_global_default(
/// tracing_subscriber::registry().with(tracing_tracy::TracyLayer::default())
/// ).expect("setup tracy layer");
/// ```
#[derive(Clone)]
pub struct TracyLayer<F = DefaultFields> {
fmt: F,
stack_depth: u16,
fields_in_zone_name: bool,
pub struct TracyLayer<C = DefaultConfig> {
config: C,
client: Client,
}

impl TracyLayer<DefaultFields> {
impl<C> TracyLayer<C> {
/// Create a new `TracyLayer`.
///
/// Defaults to collecting stack traces.
#[must_use]
pub fn new() -> Self {
pub fn new(config: C) -> Self {
Self {
fmt: DefaultFields::default(),
stack_depth: 0,
fields_in_zone_name: true,
config,
client: Client::start(),
}
}
}

impl<F> TracyLayer<F> {
/// Specify the maximum number of stack frames that will be collected.
///
/// Note that enabling callstack collection can and will introduce a non-trivial overhead at
/// every instrumentation point. Specifying 0 frames (which is the default) will disable stack
/// trace collection.
///
/// Defaults to `0`.
#[must_use]
pub const fn with_stackdepth(mut self, stack_depth: u16) -> Self {
self.stack_depth = stack_depth;
self
}

/// Specify whether or not to include tracing span fields in the tracy zone name, or to emit
/// them as zone text.
///
/// The former enables zone analysis along unique span field invocations, while the latter
/// aggregates every invocation of a given span into a single zone, irrespective of field
/// values.
///
/// Defaults to `true`.
#[must_use]
pub const fn with_fields_in_zone_name(mut self, fields_in_zone_name: bool) -> Self {
self.fields_in_zone_name = fields_in_zone_name;
self
}

/// Use a custom field formatting implementation.
#[must_use]
pub fn with_formatter<Fmt>(self, fmt: Fmt) -> TracyLayer<Fmt> {
TracyLayer {
fmt,
stack_depth: self.stack_depth,
fields_in_zone_name: self.fields_in_zone_name,
client: self.client,
}
}

impl<C: Config> TracyLayer<C> {
fn truncate_span_to_length<'a>(
&self,
data: &'a str,
Expand All @@ -161,8 +129,7 @@ impl<F> TracyLayer<F> {
while !data.is_char_boundary(max_len) {
max_len -= 1;
}
self.client
.color_message(error_msg, 0xFF000000, self.stack_depth);
self.config.on_error(&self.client, error_msg);
&data[..max_len]
} else {
data
Expand All @@ -172,7 +139,7 @@ impl<F> TracyLayer<F> {

impl Default for TracyLayer {
fn default() -> Self {
Self::new()
Self::new(DefaultConfig::default())
}
}

Expand All @@ -196,19 +163,24 @@ thread_local! {
static CACHE: StrCache = const { StrCache::new() };
}

impl<S, F> Layer<S> for TracyLayer<F>
impl<S, C> Layer<S> for TracyLayer<C>
where
S: Subscriber + for<'a> registry::LookupSpan<'a>,
F: for<'writer> FormatFields<'writer> + 'static,
C: Config + 'static,
{
fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
let Some(span) = ctx.span(id) else { return };

let mut extensions = span.extensions_mut();
if extensions.get_mut::<FormattedFields<F>>().is_none() {
if extensions.get_mut::<TracyFields<C>>().is_none() {
let mut fields =
FormattedFields::<F>::new(CACHE.with(|cache| cache.acquire().into_inner()));
if self.fmt.format_fields(fields.as_writer(), attrs).is_ok() {
TracyFields::<C>::new(CACHE.with(|cache| cache.acquire().into_inner()));
if self
.config
.formatter()
.format_fields(fields.as_writer(), attrs)
.is_ok()
{
extensions.insert(fields);
}
}
Expand All @@ -218,12 +190,17 @@ where
let Some(span) = ctx.span(id) else { return };

let mut extensions = span.extensions_mut();
if let Some(fields) = extensions.get_mut::<FormattedFields<F>>() {
let _ = self.fmt.add_fields(fields, values);
if let Some(fields) = extensions.get_mut::<TracyFields<C>>() {
let _ = self.config.formatter().add_fields(fields, values);
} else {
let mut fields =
FormattedFields::<F>::new(CACHE.with(|cache| cache.acquire().into_inner()));
if self.fmt.format_fields(fields.as_writer(), values).is_ok() {
TracyFields::<C>::new(CACHE.with(|cache| cache.acquire().into_inner()));
if self
.config
.formatter()
.format_fields(fields.as_writer(), values)
.is_ok()
{
extensions.insert(fields);
}
}
Expand All @@ -246,7 +223,7 @@ where
visitor.dest,
"event message is too long and was truncated",
),
self.stack_depth,
self.config.stack_depth(event.metadata()),
);
}
if visitor.frame_mark {
Expand All @@ -259,7 +236,7 @@ where
let Some(span) = ctx.span(id) else { return };

let extensions = span.extensions();
let fields = extensions.get::<FormattedFields<F>>();
let fields = extensions.get::<TracyFields<C>>();
let stack_frame = {
let metadata = span.metadata();
let file = metadata.file().unwrap_or("<not available>");
Expand All @@ -276,7 +253,7 @@ where
"",
file,
line,
self.stack_depth,
self.config.stack_depth(metadata),
),
id.into_u64(),
)
Expand All @@ -285,7 +262,7 @@ where
match fields {
None => span(metadata.name()),
Some(fields) if fields.is_empty() => span(metadata.name()),
Some(fields) if self.fields_in_zone_name => CACHE.with(|cache| {
Some(fields) if self.config.format_fields_in_zone_name() => CACHE.with(|cache| {
let mut buf = cache.acquire();
let _ = write!(buf, "{}{{{}}}", metadata.name(), fields.fields);
span(&buf)
Expand All @@ -312,27 +289,25 @@ where

if let Some((span, span_id)) = stack_frame {
if id.into_u64() != span_id {
self.client.color_message(
self.config.on_error(
&self.client,
"Tracing spans exited out of order! \
Trace may not be accurate for this span stack.",
0xFF000000,
self.stack_depth,
Trace might not be accurate for this span stack.",
);
}
drop(span);
} else {
self.client.color_message(
self.config.on_error(
&self.client,
"Exiting a tracing span, but got nothing on the tracy span stack!",
0xFF000000,
self.stack_depth,
);
}
}

fn on_close(&self, id: Id, ctx: Context<'_, S>) {
let Some(span) = ctx.span(&id) else { return };

if let Some(fields) = span.extensions_mut().get_mut::<FormattedFields<F>>() {
if let Some(fields) = span.extensions_mut().get_mut::<TracyFields<C>>() {
let buf = mem::take(&mut fields.fields);
CACHE.with(|cache| drop(StrCacheGuard::new(cache, buf)));
};
Expand Down
Loading