1#[cfg(feature = "profiling")]
16use std::fs::File;
17#[cfg(feature = "profiling")]
18use std::io::BufWriter;
19#[cfg(feature = "profiling")]
20use std::path::PathBuf;
21#[cfg(feature = "profiling")]
22use tracing_subscriber::EnvFilter;
23#[cfg(feature = "profiling")]
24use tracing_subscriber::layer::SubscriberExt;
25#[cfg(feature = "profiling")]
26use tracing_subscriber::util::SubscriberInitExt;
27
28#[cfg(feature = "profiling")]
31const DEFAULT_PROFILE_FILTER: &str = "bevy_ecs=trace,lumen=trace,lumenc=trace";
32
33#[cfg(feature = "profiling")]
34fn profile_filter() -> EnvFilter {
35 EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(DEFAULT_PROFILE_FILTER))
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum ProfileMode {
41 Chrome,
43 Tracy,
45 Stderr,
47}
48
49impl TryFrom<&str> for ProfileMode {
50 type Error = String;
51 fn try_from(s: &str) -> Result<Self, Self::Error> {
52 match s {
53 "chrome" => Ok(Self::Chrome),
54 "tracy" => Ok(Self::Tracy),
55 "stderr" => Ok(Self::Stderr),
56 other => Err(format!(
57 "unknown profile mode '{other}' (supported: chrome, tracy, stderr)"
58 )),
59 }
60 }
61}
62
63#[cfg(feature = "profiling")]
65pub struct ProfileGuard {
66 chrome: Option<tracing_chrome::FlushGuard>,
67 chrome_path: Option<PathBuf>,
68}
69
70#[cfg(not(feature = "profiling"))]
74pub struct ProfileGuard {}
75
76#[cfg(feature = "profiling")]
77impl Drop for ProfileGuard {
78 fn drop(&mut self) {
79 self.chrome.take();
82 if let Some(path) = self.chrome_path.take()
83 && let Err(e) = rewrite_chrome_names(&path)
84 {
85 eprintln!(
86 "lumenc: failed to rewrite chrome trace names ({}): {e}",
87 path.display()
88 );
89 }
90 }
91}
92
93#[cfg(feature = "profiling")]
95fn rewrite_chrome_names(path: &std::path::Path) -> std::io::Result<()> {
96 use std::io::{BufRead, BufReader, Write};
97 let tmp = path.with_extension("json.tmp");
98 let reader = BufReader::new(File::open(path)?);
99 let mut writer = BufWriter::new(File::create(&tmp)?);
100 for line in reader.lines() {
101 let mut line = line?;
102 if let Some(rewritten) = lift_args_name(&line) {
103 line = rewritten;
104 }
105 writer.write_all(line.as_bytes())?;
106 writer.write_all(b"\n")?;
107 }
108 writer.flush()?;
109 drop(writer);
110 std::fs::rename(&tmp, path)?;
111 Ok(())
112}
113
114#[cfg(feature = "profiling")]
117fn lift_args_name(line: &str) -> Option<String> {
118 let trimmed = line.trim_end_matches(',').trim();
119 if trimmed.is_empty() || matches!(trimmed, "[" | "]") {
120 return None;
121 }
122 let trailing_comma = line.trim_end().ends_with(',');
123 let mut v: serde_json::Value = serde_json::from_str(trimmed).ok()?;
124 let obj = v.as_object_mut()?;
125 let current_name = obj.get("name").and_then(|n| n.as_str())?;
127 if !matches!(current_name, "system" | "schedule" | "system_commands") {
128 return None;
129 }
130 let args_name = obj
131 .get("args")
132 .and_then(|a| a.get("name"))
133 .and_then(|n| n.as_str())?
134 .trim_matches('"')
135 .to_string();
136 if args_name.is_empty() {
137 return None;
138 }
139 let lifted = if current_name == "system_commands" {
145 format!("{args_name} (commands)")
146 } else {
147 args_name
148 };
149 obj.insert("name".into(), serde_json::Value::String(lifted));
150 let mut s = serde_json::to_string(&v).ok()?;
151 if trailing_comma {
152 s.push(',');
153 }
154 Some(s)
155}
156
157#[cfg(all(test, feature = "profiling"))]
158mod lift_tests {
159 use super::lift_args_name;
160
161 #[test]
162 fn system_span_takes_bare_fn_path() {
163 let line = r#"{"name":"system","args":{"name":"my_crate::my_system"}},"#;
164 let out = lift_args_name(line).unwrap();
165 assert!(out.contains(r#""name":"my_crate::my_system""#));
166 assert!(out.ends_with(','));
167 }
168
169 #[test]
173 fn system_commands_span_keeps_commands_suffix() {
174 let line = r#"{"name":"system_commands","args":{"name":"my_crate::my_system"}}"#;
175 let out = lift_args_name(line).unwrap();
176 assert!(out.contains(r#""name":"my_crate::my_system (commands)""#));
177 }
178
179 #[test]
180 fn non_placeholder_names_pass_through() {
181 let line = r#"{"name":"already_named","args":{"name":"x"}}"#;
182 assert!(lift_args_name(line).is_none());
183 }
184}
185
186#[cfg(not(feature = "profiling"))]
192pub fn install(_mode: ProfileMode) -> Result<ProfileGuard, String> {
193 Err(
194 "this lumenc was built without profiler support; rebuild with \
195 `cargo build -p lumenc --features profiling` (chrome / stderr) or \
196 `--features profiling-tracy` (adds tracy)"
197 .into(),
198 )
199}
200
201#[cfg(feature = "profiling")]
207pub fn install(mode: ProfileMode) -> Result<ProfileGuard, String> {
208 match mode {
209 ProfileMode::Chrome => {
210 let path = PathBuf::from("lumen-trace.json");
211 let file = File::create(&path).map_err(|e| format!("open {}: {e}", path.display()))?;
212 let writer = BufWriter::new(file);
213 let (chrome_layer, guard) = tracing_chrome::ChromeLayerBuilder::new()
214 .writer(writer)
215 .include_args(true)
216 .build();
217 tracing_subscriber::registry()
218 .with(profile_filter())
219 .with(chrome_layer)
220 .try_init()
221 .map_err(|e| format!("install chrome subscriber: {e}"))?;
222 eprintln!(
223 "lumenc: chrome profiler writing to {} - open in chrome://tracing or https://ui.perfetto.dev",
224 path.display()
225 );
226 Ok(ProfileGuard {
227 chrome: Some(guard),
228 chrome_path: Some(path),
229 })
230 }
231 #[cfg(feature = "profiling-tracy")]
232 ProfileMode::Tracy => {
233 tracing_subscriber::registry()
238 .with(profile_filter())
239 .with(tracing_tracy::TracyLayer::default())
240 .try_init()
241 .map_err(|e| format!("install tracy subscriber: {e}"))?;
242 eprintln!("lumenc: tracy profiler active - launch `tracy-profiler` to connect");
243 Ok(ProfileGuard {
244 chrome: None,
245 chrome_path: None,
246 })
247 }
248 #[cfg(not(feature = "profiling-tracy"))]
249 ProfileMode::Tracy => Err("this lumenc was built without tracy support; rebuild with \
250 `cargo build -p lumenc --features profiling-tracy`"
251 .into()),
252 ProfileMode::Stderr => {
253 tracing_subscriber::registry()
254 .with(profile_filter())
255 .with(
256 tracing_subscriber::fmt::layer()
257 .with_target(false)
258 .with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE)
264 .with_writer(std::io::stderr),
265 )
266 .try_init()
267 .map_err(|e| format!("install stderr subscriber: {e}"))?;
268 eprintln!("lumenc: stderr profiler active - one line per system span");
269 Ok(ProfileGuard {
270 chrome: None,
271 chrome_path: None,
272 })
273 }
274 }
275}