Skip to content

Commit 380d0cf

Browse files
committed
connect flecs to tracing ecosystem
1 parent f639508 commit 380d0cf

7 files changed

Lines changed: 504 additions & 1 deletion

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace]
2-
members = ["flecs_ecs", "flecs_ecs_derive", "flecs_ecs_sys", "test_crash_handler"]
2+
members = ["flecs_ecs", "flecs_ecs_derive", "flecs_ecs_sys", "flecs_ecs_tracing", "test_crash_handler"]
33
resolver = "2"
44

55
exclude = [

flecs_ecs_tracing/Cargo.toml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[package]
2+
name = "flecs_ecs_tracing"
3+
version = "0.1.0"
4+
edition.workspace = true
5+
license.workspace = true
6+
repository.workspace = true
7+
rust-version.workspace = true
8+
9+
[lints]
10+
workspace = true
11+
12+
[dependencies]
13+
flecs_ecs = { workspace = true }
14+
hashbrown = { workspace = true }
15+
tracing-core = "0.1.32"
16+
17+
[dev-dependencies]
18+
# for doctest/example
19+
tracing = { version = "0.1.40", default-features = false }
20+
tracing-subscriber = { version = "0.3.18", features = [
21+
"env-filter",
22+
], default-features = false }
23+
# for perf_trace doctest/example
24+
tracing-tracy = { version = "0.11.3", default-features = false }
25+
26+
[features]
27+
perf_trace = ["flecs_ecs/flecs_perf_trace"]

flecs_ecs_tracing/src/lib.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//! Integrates Flecs ECS logging and performance tracing into the `tracing` ecosystem.
2+
3+
use flecs_ecs::prelude::*;
4+
5+
mod log;
6+
mod metadata;
7+
#[cfg(feature = "perf_trace")]
8+
mod perf_trace;
9+
mod util;
10+
11+
/// Send Flecs internal logging to `tracing` subscribers instead.
12+
///
13+
/// Note that the application will need to set up those subscribers; by default, logs will go nowhere.
14+
///
15+
/// This must be called before the first [`World`] is created anywhere in the process;
16+
/// see [`ecs_os_api::add_init_hook`] for details on those limitations.
17+
///
18+
/// # Example
19+
/// ```standalone
20+
/// use flecs_ecs::prelude::*;
21+
/// use flecs_ecs_tracing::log_to_tracing;
22+
/// use tracing_subscriber::prelude::*;
23+
///
24+
/// tracing_subscriber::registry()
25+
/// // Send logs to stdout
26+
/// .with(
27+
/// tracing_subscriber::fmt::layer()
28+
/// // By default, hide anything below WARN log level from stdout
29+
/// .with_filter(
30+
/// tracing_subscriber::EnvFilter::builder()
31+
/// .with_default_directive(tracing::level_filters::LevelFilter::WARN.into())
32+
/// .from_env_lossy(),
33+
/// ),
34+
/// )
35+
/// .init();
36+
///
37+
/// log_to_tracing();
38+
/// let world = World::new();
39+
/// ```
40+
pub fn log_to_tracing() {
41+
// Ensure that the registry is initialized now
42+
metadata::init();
43+
44+
ecs_os_api::add_init_hook(Box::new(|api| {
45+
api.log_ = Some(log::log_to_tracing);
46+
}));
47+
}
48+
49+
#[cfg(feature = "perf_trace")]
50+
/// Send Flecs performance traces to `tracing` subscribers.
51+
///
52+
/// This must be called before the first [`World`] is created anywhere in the process;
53+
/// see [`ecs_os_api::add_init_hook`] for details on those limitations.
54+
///
55+
/// # Example
56+
/// ```standalone
57+
/// use flecs_ecs::prelude::*;
58+
/// use flecs_ecs_tracing::{log_to_tracing, perf_trace_to_tracing};
59+
/// use tracing_subscriber::prelude::*;
60+
///
61+
/// tracing_subscriber::registry()
62+
/// // Send logs to stdout
63+
/// .with(
64+
/// tracing_subscriber::fmt::layer()
65+
/// // By default, hide anything below WARN log level from stdout
66+
/// .with_filter(
67+
/// tracing_subscriber::EnvFilter::builder()
68+
/// .with_default_directive(tracing::level_filters::LevelFilter::WARN.into())
69+
/// .from_env_lossy(),
70+
/// ),
71+
/// )
72+
/// // Send logs and performance tracing data to the Tracy profiler
73+
/// .with(
74+
/// tracing_tracy::TracyLayer::default()
75+
/// .with_filter(tracing::level_filters::LevelFilter::INFO),
76+
/// )
77+
/// .init();
78+
///
79+
/// log_to_tracing(); // optional, but recommended
80+
/// perf_trace_to_tracing(tracing::Level::INFO);
81+
///
82+
/// let world = World::new();
83+
/// ```
84+
pub fn perf_trace_to_tracing(span_level: tracing_core::Level) {
85+
// Ensure that the registry is initialized now
86+
perf_trace::init();
87+
88+
// Set the log level of performance tracing spans
89+
perf_trace::SPAN_LEVEL.get_or_init(|| span_level);
90+
91+
ecs_os_api::add_init_hook(Box::new(|api| {
92+
api.perf_trace_push_ = Some(perf_trace::perf_trace_push);
93+
api.perf_trace_pop_ = Some(perf_trace::perf_trace_pop);
94+
}));
95+
}

flecs_ecs_tracing/src/log.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
use std::borrow::Cow;
2+
3+
use crate::{metadata, util::*};
4+
use tracing_core::{Dispatch, Metadata};
5+
6+
fn ensure_event_meta(
7+
dispatch: &Dispatch,
8+
request: metadata::MetadataRequest<'_>,
9+
) -> &'static Metadata<'static> {
10+
// Double-checked locking
11+
{
12+
let registry = metadata::REGISTRY.read().unwrap();
13+
if let Some(existing) = registry.metadata.get(&request) {
14+
return existing;
15+
}
16+
}
17+
18+
let mut registry = metadata::REGISTRY.write().unwrap();
19+
20+
if let Some(existing) = registry.metadata.get(&request) {
21+
return existing;
22+
}
23+
24+
let (metadata, request) = request.register(dispatch);
25+
registry.metadata.insert(request, metadata);
26+
27+
metadata
28+
}
29+
30+
fn get_event_meta(
31+
dispatch: &Dispatch,
32+
filename: Option<Cow<'_, str>>,
33+
line: i32,
34+
level: tracing_core::metadata::Level,
35+
) -> &'static Metadata<'static> {
36+
let request = metadata::MetadataRequest {
37+
kind: metadata::MetadataKind::Event,
38+
name: Cow::Borrowed("log"),
39+
filename,
40+
line: line.try_into().ok(),
41+
level,
42+
};
43+
44+
if let Some(existing) = metadata::REGISTRY.read().unwrap().metadata.get(&request) {
45+
// existing metadata - fast path
46+
return existing;
47+
}
48+
49+
// new metadata - slow path
50+
ensure_event_meta(dispatch, request)
51+
}
52+
53+
pub(crate) unsafe extern "C-unwind" fn log_to_tracing(
54+
level: i32,
55+
c_file: *const i8,
56+
line: i32,
57+
c_msg: *const i8,
58+
) {
59+
tracing_core::dispatcher::get_default(|dispatch| {
60+
let file = flecs_str(c_file);
61+
let msg = flecs_str(c_msg).unwrap_or(Cow::Borrowed(""));
62+
let level = match level {
63+
-4 | -3 => tracing_core::metadata::Level::ERROR,
64+
-2 => tracing_core::metadata::Level::WARN,
65+
0 => tracing_core::metadata::Level::INFO,
66+
1..=3 => tracing_core::metadata::Level::DEBUG,
67+
_ => tracing_core::metadata::Level::TRACE,
68+
};
69+
70+
let meta = get_event_meta(dispatch, file, line, level);
71+
if dispatch.enabled(meta) {
72+
let message_field = meta.fields().iter().next().expect("FieldSet corrupted");
73+
tracing_core::Event::dispatch(
74+
meta,
75+
&meta.fields().value_set(&[(
76+
&message_field,
77+
Some(&(format_args!("{msg}")) as &dyn tracing_core::field::Value),
78+
)]),
79+
);
80+
}
81+
});
82+
}

flecs_ecs_tracing/src/metadata.rs

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
//! Construct [`tracing_core::Metadata`] objects in the way required by that ecosystem.
2+
//!
3+
//! Dynamic call site stuff based on <https://github.com/slowli/tracing-toolbox/>
4+
5+
use crate::util::*;
6+
use hashbrown::HashMap;
7+
use std::{
8+
borrow::Cow,
9+
sync::{LazyLock, OnceLock, RwLock},
10+
};
11+
use tracing_core::{field::FieldSet, metadata::Level, Dispatch, Kind, Metadata};
12+
13+
/// Registry of dynamic tracing metadata generated from Flecs
14+
pub(crate) static REGISTRY: LazyLock<RwLock<Registry>> = LazyLock::new(Default::default);
15+
16+
/// Registry of dynamic tracing metadata generated from Flecs
17+
pub(crate) struct Registry {
18+
pub(crate) metadata: HashMap<MetadataRequest<'static>, &'static Metadata<'static>>,
19+
}
20+
21+
impl Default for Registry {
22+
fn default() -> Self {
23+
Self {
24+
// Capacity was chosen arbitrarily
25+
metadata: HashMap::with_capacity(256),
26+
}
27+
}
28+
}
29+
30+
pub(crate) fn init() {
31+
LazyLock::force(&REGISTRY);
32+
}
33+
34+
#[derive(Default)]
35+
pub(crate) struct DynamicCallsite {
36+
/// Due to the opaque [`tracing_core::callsite::Identifier`],
37+
/// which borrows [`tracing_core::Callsite`] for `'static`, have to use interior mutability.
38+
///
39+
/// See [`MetadataRequest::register`] for details
40+
pub(crate) metadata: OnceLock<Metadata<'static>>,
41+
}
42+
43+
impl tracing_core::Callsite for DynamicCallsite {
44+
fn set_interest(&self, _interest: tracing_core::Interest) {
45+
// No-op
46+
}
47+
48+
fn metadata(&self) -> &Metadata<'_> {
49+
self.metadata.get().unwrap()
50+
}
51+
}
52+
53+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
54+
pub(crate) enum MetadataKind {
55+
// hash/eq skip filename/line since they may differ between start & stop
56+
// only used when perf_trace is enabled
57+
#[cfg_attr(not(feature = "perf_trace"), allow(unused))]
58+
Span,
59+
Event,
60+
}
61+
62+
#[derive(Clone, Debug)]
63+
pub(crate) struct MetadataRequest<'a> {
64+
pub(crate) kind: MetadataKind,
65+
pub(crate) level: Level,
66+
pub(crate) name: Cow<'a, str>,
67+
pub(crate) filename: Option<Cow<'a, str>>,
68+
pub(crate) line: Option<u32>,
69+
}
70+
71+
impl<'a> PartialEq for MetadataRequest<'a> {
72+
fn eq(&self, other: &Self) -> bool {
73+
self.kind == other.kind
74+
&& self.name == other.name
75+
&& self.level == other.level
76+
&& match self.kind {
77+
MetadataKind::Span => true,
78+
MetadataKind::Event => self.filename == other.filename && self.line == other.line,
79+
}
80+
}
81+
}
82+
83+
impl<'a> Eq for MetadataRequest<'a> {}
84+
85+
impl<'a> std::hash::Hash for MetadataRequest<'a> {
86+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
87+
self.kind.hash(state);
88+
self.name.hash(state);
89+
self.level.hash(state);
90+
match self.kind {
91+
MetadataKind::Span => {}
92+
MetadataKind::Event => {
93+
self.filename.hash(state);
94+
self.line.hash(state);
95+
}
96+
}
97+
}
98+
}
99+
100+
impl<'a> MetadataRequest<'a> {
101+
/// Construct a new [`Metadata`].
102+
///
103+
/// Does not interact with [`crate::REGISTRY`] by design to avoid having to deal with locking;
104+
/// the caller should handle memoization if necessary.
105+
pub(crate) fn register(
106+
self,
107+
dispatch: &Dispatch,
108+
) -> (&'static Metadata<'static>, MetadataRequest<'static>) {
109+
// `Dispatch::register_callsite` demands `&'static Metadata`, this is the simplest way to get that
110+
let callsite = Box::leak(Box::new(DynamicCallsite::default()));
111+
112+
// This macro is the only 'public' way of constructing [`Identifier`]
113+
let id = tracing_core::identify_callsite!(callsite);
114+
115+
let name = leak_cowstr(self.name);
116+
let filename: Option<&'static str> = self.filename.map(leak_cowstr);
117+
118+
let metadata = Metadata::new(
119+
name,
120+
"flecs",
121+
self.level,
122+
filename,
123+
self.line,
124+
Some("flecs_ecs_tracing"),
125+
match self.kind {
126+
MetadataKind::Span { .. } => FieldSet::new(&[], id),
127+
MetadataKind::Event { .. } => FieldSet::new(&["message"], id),
128+
},
129+
match self.kind {
130+
MetadataKind::Span { .. } => Kind::SPAN,
131+
MetadataKind::Event { .. } => Kind::EVENT,
132+
},
133+
);
134+
135+
// Store the new Metadata
136+
callsite.metadata.set(metadata).unwrap();
137+
138+
// Since the `DynamicCallsite` is alive forever we can also use it to get
139+
// a `&'static` to the metadata, without extra allocations
140+
let metadata: &'static Metadata = callsite.metadata.get().unwrap();
141+
142+
// Tell `tracing` subscribers about the new callsite
143+
dispatch.register_callsite(metadata);
144+
145+
(
146+
metadata,
147+
MetadataRequest {
148+
name: Cow::Borrowed(name),
149+
kind: self.kind,
150+
filename: filename.map(Cow::Borrowed),
151+
line: self.line,
152+
level: self.level,
153+
},
154+
)
155+
}
156+
}

0 commit comments

Comments
 (0)