-
Notifications
You must be signed in to change notification settings - Fork 12
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
connect flecs to tracing ecosystem #200
Open
Georgiy-Tugai
wants to merge
4
commits into
Indra-db:main
Choose a base branch
from
Georgiy-Tugai:tracing
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
f639508
move hashbrown dependency to the workspace
Georgiy-Tugai 380d0cf
connect flecs to tracing ecosystem
Georgiy-Tugai ef5429d
use a struct type instead of a tuple for span registry values
Georgiy-Tugai 215af72
obey tracing's static level filters
Georgiy-Tugai File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
[package] | ||
name = "flecs_ecs_tracing" | ||
version = "0.1.0" | ||
edition.workspace = true | ||
license.workspace = true | ||
repository.workspace = true | ||
rust-version.workspace = true | ||
|
||
[lints] | ||
workspace = true | ||
|
||
[dependencies] | ||
flecs_ecs = { workspace = true } | ||
hashbrown = { workspace = true } | ||
tracing-core = "0.1.32" | ||
tracing = { version = "0.1.40", default-features = false } | ||
|
||
[dev-dependencies] | ||
# for doctest/example | ||
tracing-subscriber = { version = "0.3.18", features = [ | ||
"env-filter", | ||
], default-features = false } | ||
# for perf_trace doctest/example | ||
tracing-tracy = { version = "0.11.3", default-features = false } | ||
|
||
[features] | ||
perf_trace = ["flecs_ecs/flecs_perf_trace"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
//! Integrates Flecs ECS logging and performance tracing into the `tracing` ecosystem. | ||
|
||
use flecs_ecs::prelude::*; | ||
|
||
mod log; | ||
mod metadata; | ||
#[cfg(feature = "perf_trace")] | ||
mod perf_trace; | ||
mod util; | ||
|
||
/// Send Flecs internal logging to `tracing` subscribers instead. | ||
/// | ||
/// Note that the application will need to set up those subscribers; by default, logs will go nowhere. | ||
/// | ||
/// This must be called before the first [`World`] is created anywhere in the process; | ||
/// see [`ecs_os_api::add_init_hook`] for details on those limitations. | ||
/// | ||
/// # Example | ||
/// ```standalone | ||
/// use flecs_ecs::prelude::*; | ||
/// use flecs_ecs_tracing::log_to_tracing; | ||
/// use tracing_subscriber::prelude::*; | ||
/// | ||
/// tracing_subscriber::registry() | ||
/// // Send logs to stdout | ||
/// .with( | ||
/// tracing_subscriber::fmt::layer() | ||
/// // By default, hide anything below WARN log level from stdout | ||
/// .with_filter( | ||
/// tracing_subscriber::EnvFilter::builder() | ||
/// .with_default_directive(tracing::level_filters::LevelFilter::WARN.into()) | ||
/// .from_env_lossy(), | ||
/// ), | ||
/// ) | ||
/// .init(); | ||
/// | ||
/// log_to_tracing(); | ||
/// let world = World::new(); | ||
/// ``` | ||
pub fn log_to_tracing() { | ||
// Ensure that the registry is initialized now | ||
metadata::init(); | ||
|
||
ecs_os_api::add_init_hook(Box::new(|api| { | ||
api.log_ = Some(log::log_to_tracing); | ||
})); | ||
} | ||
|
||
#[cfg(feature = "perf_trace")] | ||
/// Send Flecs performance traces to `tracing` subscribers. | ||
/// | ||
/// This must be called before the first [`World`] is created anywhere in the process; | ||
/// see [`ecs_os_api::add_init_hook`] for details on those limitations. | ||
/// | ||
/// # Example | ||
/// ```standalone | ||
/// use flecs_ecs::prelude::*; | ||
/// use flecs_ecs_tracing::{log_to_tracing, perf_trace_to_tracing}; | ||
/// use tracing_subscriber::prelude::*; | ||
/// | ||
/// tracing_subscriber::registry() | ||
/// // Send logs to stdout | ||
/// .with( | ||
/// tracing_subscriber::fmt::layer() | ||
/// // By default, hide anything below WARN log level from stdout | ||
/// .with_filter( | ||
/// tracing_subscriber::EnvFilter::builder() | ||
/// .with_default_directive(tracing::level_filters::LevelFilter::WARN.into()) | ||
/// .from_env_lossy(), | ||
/// ), | ||
/// ) | ||
/// // Send logs and performance tracing data to the Tracy profiler | ||
/// .with( | ||
/// tracing_tracy::TracyLayer::default() | ||
/// .with_filter(tracing::level_filters::LevelFilter::INFO), | ||
/// ) | ||
/// .init(); | ||
/// | ||
/// log_to_tracing(); // optional, but recommended | ||
/// perf_trace_to_tracing(tracing::Level::INFO); | ||
/// | ||
/// let world = World::new(); | ||
/// ``` | ||
pub fn perf_trace_to_tracing(span_level: tracing_core::Level) { | ||
// Ensure that the registry is initialized now | ||
perf_trace::init(); | ||
|
||
if span_level < tracing::level_filters::STATIC_MAX_LEVEL { | ||
// Early out due to static level constraint | ||
return; | ||
} | ||
|
||
// Set the log level of performance tracing spans | ||
perf_trace::SPAN_LEVEL.get_or_init(|| span_level); | ||
|
||
ecs_os_api::add_init_hook(Box::new(|api| { | ||
api.perf_trace_push_ = Some(perf_trace::perf_trace_push); | ||
api.perf_trace_pop_ = Some(perf_trace::perf_trace_pop); | ||
})); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
use std::borrow::Cow; | ||
|
||
use crate::{metadata, util::*}; | ||
use tracing_core::{Dispatch, Metadata}; | ||
|
||
fn ensure_event_meta( | ||
dispatch: &Dispatch, | ||
request: metadata::MetadataRequest<'_>, | ||
) -> &'static Metadata<'static> { | ||
// Double-checked locking | ||
{ | ||
let registry = metadata::REGISTRY.read().unwrap(); | ||
if let Some(existing) = registry.metadata.get(&request) { | ||
return existing; | ||
} | ||
} | ||
|
||
let mut registry = metadata::REGISTRY.write().unwrap(); | ||
|
||
if let Some(existing) = registry.metadata.get(&request) { | ||
return existing; | ||
} | ||
|
||
let (metadata, request) = request.register(dispatch); | ||
registry.metadata.insert(request, metadata); | ||
|
||
metadata | ||
} | ||
|
||
fn get_event_meta( | ||
dispatch: &Dispatch, | ||
filename: Option<Cow<'_, str>>, | ||
line: i32, | ||
level: tracing_core::metadata::Level, | ||
) -> &'static Metadata<'static> { | ||
let request = metadata::MetadataRequest { | ||
kind: metadata::MetadataKind::Event, | ||
name: Cow::Borrowed("log"), | ||
filename, | ||
line: line.try_into().ok(), | ||
level, | ||
}; | ||
|
||
if let Some(existing) = metadata::REGISTRY.read().unwrap().metadata.get(&request) { | ||
// existing metadata - fast path | ||
return existing; | ||
} | ||
|
||
// new metadata - slow path | ||
ensure_event_meta(dispatch, request) | ||
} | ||
|
||
pub(crate) unsafe extern "C-unwind" fn log_to_tracing( | ||
level: i32, | ||
c_file: *const i8, | ||
line: i32, | ||
c_msg: *const i8, | ||
) { | ||
let level = match level { | ||
-4 | -3 => tracing_core::metadata::Level::ERROR, | ||
-2 => tracing_core::metadata::Level::WARN, | ||
0 => tracing_core::metadata::Level::INFO, | ||
1..=3 => tracing_core::metadata::Level::DEBUG, | ||
_ => tracing_core::metadata::Level::TRACE, | ||
}; | ||
|
||
if level < tracing::level_filters::STATIC_MAX_LEVEL { | ||
// Early out due to static level constraint | ||
return; | ||
} | ||
|
||
tracing_core::dispatcher::get_default(|dispatch| { | ||
let file = flecs_str(c_file); | ||
let msg = flecs_str(c_msg).unwrap_or(Cow::Borrowed("")); | ||
|
||
let meta = get_event_meta(dispatch, file, line, level); | ||
if dispatch.enabled(meta) { | ||
let message_field = meta.fields().iter().next().expect("FieldSet corrupted"); | ||
tracing_core::Event::dispatch( | ||
meta, | ||
&meta.fields().value_set(&[( | ||
&message_field, | ||
Some(&(format_args!("{msg}")) as &dyn tracing_core::field::Value), | ||
)]), | ||
); | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
//! Construct [`tracing_core::Metadata`] objects in the way required by that ecosystem. | ||
//! | ||
//! Dynamic call site stuff based on <https://github.com/slowli/tracing-toolbox/> | ||
|
||
use crate::util::*; | ||
use hashbrown::HashMap; | ||
use std::{ | ||
borrow::Cow, | ||
sync::{LazyLock, OnceLock, RwLock}, | ||
}; | ||
use tracing_core::{field::FieldSet, metadata::Level, Dispatch, Kind, Metadata}; | ||
|
||
/// Registry of dynamic tracing metadata generated from Flecs | ||
pub(crate) static REGISTRY: LazyLock<RwLock<Registry>> = LazyLock::new(Default::default); | ||
|
||
/// Registry of dynamic tracing metadata generated from Flecs | ||
pub(crate) struct Registry { | ||
pub(crate) metadata: HashMap<MetadataRequest<'static>, &'static Metadata<'static>>, | ||
} | ||
|
||
impl Default for Registry { | ||
fn default() -> Self { | ||
Self { | ||
// Capacity was chosen arbitrarily | ||
metadata: HashMap::with_capacity(256), | ||
} | ||
} | ||
} | ||
|
||
pub(crate) fn init() { | ||
LazyLock::force(®ISTRY); | ||
} | ||
|
||
#[derive(Default)] | ||
pub(crate) struct DynamicCallsite { | ||
/// Due to the opaque [`tracing_core::callsite::Identifier`], | ||
/// which borrows [`tracing_core::Callsite`] for `'static`, have to use interior mutability. | ||
/// | ||
/// See [`MetadataRequest::register`] for details | ||
pub(crate) metadata: OnceLock<Metadata<'static>>, | ||
} | ||
|
||
impl tracing_core::Callsite for DynamicCallsite { | ||
fn set_interest(&self, _interest: tracing_core::Interest) { | ||
// No-op | ||
} | ||
|
||
fn metadata(&self) -> &Metadata<'_> { | ||
self.metadata.get().unwrap() | ||
} | ||
} | ||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | ||
pub(crate) enum MetadataKind { | ||
// hash/eq skip filename/line since they may differ between start & stop | ||
// only used when perf_trace is enabled | ||
#[cfg_attr(not(feature = "perf_trace"), allow(unused))] | ||
Span, | ||
Event, | ||
} | ||
|
||
#[derive(Clone, Debug)] | ||
pub(crate) struct MetadataRequest<'a> { | ||
pub(crate) kind: MetadataKind, | ||
pub(crate) level: Level, | ||
pub(crate) name: Cow<'a, str>, | ||
pub(crate) filename: Option<Cow<'a, str>>, | ||
pub(crate) line: Option<u32>, | ||
} | ||
|
||
impl<'a> PartialEq for MetadataRequest<'a> { | ||
fn eq(&self, other: &Self) -> bool { | ||
self.kind == other.kind | ||
&& self.name == other.name | ||
&& self.level == other.level | ||
&& match self.kind { | ||
MetadataKind::Span => true, | ||
MetadataKind::Event => self.filename == other.filename && self.line == other.line, | ||
} | ||
} | ||
} | ||
|
||
impl<'a> Eq for MetadataRequest<'a> {} | ||
|
||
impl<'a> std::hash::Hash for MetadataRequest<'a> { | ||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { | ||
self.kind.hash(state); | ||
self.name.hash(state); | ||
self.level.hash(state); | ||
match self.kind { | ||
MetadataKind::Span => {} | ||
MetadataKind::Event => { | ||
self.filename.hash(state); | ||
self.line.hash(state); | ||
} | ||
} | ||
} | ||
} | ||
|
||
impl<'a> MetadataRequest<'a> { | ||
/// Construct a new [`Metadata`]. | ||
/// | ||
/// Does not interact with [`crate::REGISTRY`] by design to avoid having to deal with locking; | ||
/// the caller should handle memoization if necessary. | ||
pub(crate) fn register( | ||
self, | ||
dispatch: &Dispatch, | ||
) -> (&'static Metadata<'static>, MetadataRequest<'static>) { | ||
// `Dispatch::register_callsite` demands `&'static Metadata`, this is the simplest way to get that | ||
let callsite = Box::leak(Box::new(DynamicCallsite::default())); | ||
|
||
// This macro is the only 'public' way of constructing [`Identifier`] | ||
let id = tracing_core::identify_callsite!(callsite); | ||
|
||
let name = leak_cowstr(self.name); | ||
let filename: Option<&'static str> = self.filename.map(leak_cowstr); | ||
|
||
let metadata = Metadata::new( | ||
name, | ||
"flecs", | ||
self.level, | ||
filename, | ||
self.line, | ||
Some("flecs_ecs_tracing"), | ||
match self.kind { | ||
MetadataKind::Span { .. } => FieldSet::new(&[], id), | ||
MetadataKind::Event { .. } => FieldSet::new(&["message"], id), | ||
}, | ||
match self.kind { | ||
MetadataKind::Span { .. } => Kind::SPAN, | ||
MetadataKind::Event { .. } => Kind::EVENT, | ||
}, | ||
); | ||
|
||
// Store the new Metadata | ||
callsite.metadata.set(metadata).unwrap(); | ||
|
||
// Since the `DynamicCallsite` is alive forever we can also use it to get | ||
// a `&'static` to the metadata, without extra allocations | ||
let metadata: &'static Metadata = callsite.metadata.get().unwrap(); | ||
|
||
// Tell `tracing` subscribers about the new callsite | ||
dispatch.register_callsite(metadata); | ||
|
||
( | ||
metadata, | ||
MetadataRequest { | ||
name: Cow::Borrowed(name), | ||
kind: self.kind, | ||
filename: filename.map(Cow::Borrowed), | ||
line: self.line, | ||
level: self.level, | ||
}, | ||
) | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Going by code below,
filename
andline
are only valid whenkind == MetadataKind::Event
, so it seems likeMetadataKind::Event
should be holding that data.In that case, then the
PartialEq
andHash
would be derived and not have a manual implementation.If there is a reason for not doing the expected thing (what I just described), then that should be documented with a comment somewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
filename
andline
are valid for bothEvent
andSpan
, but forSpan
they are not part of the hash key, since the push and pop events will come from different line numbers (& possibly different file names).It's a moot point at the moment, though, since metadata for spans is currently not cached in the metadata registry, only in the span registry which uses the span name as the key. This may change in the future; an alternative design would have one metadata per
ecs_os_perf_trace_push
callsite and put the span names into values (like log messages).