#[cfg(feature = "tracy")]
use std::cell::Cell;
#[cfg(feature = "tracy")]
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(feature = "tracy")]
use std::time::Instant;
#[cfg(feature = "tracy")]
use once_cell::sync::Lazy;
#[cfg(feature = "tracy")]
use parking_lot::Mutex;
#[cfg(feature = "tracy")]
#[derive(Debug, Clone)]
struct TraceEvent {
name: String,
category: &'static str,
ph: char,
ts_micros: f64,
dur_micros: Option<f64>,
tid: u64,
pid: u32,
arg: Option<String>,
}
#[cfg(feature = "tracy")]
static PROCESS_START: Lazy<Instant> = Lazy::new(Instant::now);
#[cfg(feature = "tracy")]
static TRACE_EVENTS: Lazy<Mutex<Vec<TraceEvent>>> = Lazy::new(|| Mutex::new(Vec::new()));
#[cfg(feature = "tracy")]
static NEXT_TID: AtomicU64 = AtomicU64::new(0);
#[cfg(feature = "tracy")]
thread_local! {
static THREAD_TID: Cell<Option<u64>> = const { Cell::new(None) };
}
#[cfg(feature = "tracy")]
fn current_tid() -> u64 {
THREAD_TID.with(|cell| {
if let Some(tid) = cell.get() {
tid
} else {
let tid = NEXT_TID.fetch_add(1, Ordering::Relaxed);
cell.set(Some(tid));
tid
}
})
}
#[cfg(feature = "tracy")]
fn record_event(event: TraceEvent) {
TRACE_EVENTS.lock().push(event);
}
#[cfg(feature = "tracy")]
fn json_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
out.push_str(&format!("\\u{:04x}", c as u32));
}
c => out.push(c),
}
}
out
}
#[cfg(feature = "tracy")]
fn serialize_event(event: &TraceEvent) -> String {
let mut obj = String::new();
obj.push('{');
obj.push_str(&format!("\"name\":\"{}\",", json_escape(&event.name)));
obj.push_str(&format!("\"cat\":\"{}\",", json_escape(event.category)));
obj.push_str(&format!("\"ph\":\"{}\",", event.ph));
obj.push_str(&format!("\"ts\":{},", event.ts_micros));
if let Some(dur) = event.dur_micros {
obj.push_str(&format!("\"dur\":{dur},"));
}
obj.push_str(&format!("\"pid\":{},", event.pid));
obj.push_str(&format!("\"tid\":{}", event.tid));
if event.ph == 'i' {
obj.push_str(",\"s\":\"g\"");
}
if let Some(arg) = &event.arg {
obj.push_str(&format!(
",\"args\":{{\"message\":\"{}\"}}",
json_escape(arg)
));
}
obj.push('}');
obj
}
#[cfg(feature = "tracy")]
fn serialize_document(events: &[TraceEvent]) -> String {
let mut doc = String::from("{\"traceEvents\":[");
for (i, event) in events.iter().enumerate() {
if i > 0 {
doc.push(',');
}
doc.push_str(&serialize_event(event));
}
doc.push_str("],\"displayTimeUnit\":\"ns\"}");
doc
}
const EMPTY_TRACE_DOCUMENT: &str = "{\"traceEvents\":[],\"displayTimeUnit\":\"ns\"}";
pub struct TracySpan {
#[cfg(feature = "tracy")]
name: String,
#[cfg(feature = "tracy")]
start: Instant,
#[cfg(not(feature = "tracy"))]
_phantom: (),
}
#[cfg(feature = "tracy")]
impl Drop for TracySpan {
fn drop(&mut self) {
let dur = self.start.elapsed();
let ts_micros = self.start.duration_since(*PROCESS_START).as_secs_f64() * 1_000_000.0;
record_event(TraceEvent {
name: std::mem::take(&mut self.name),
category: "zone",
ph: 'X',
ts_micros,
dur_micros: Some(dur.as_secs_f64() * 1_000_000.0),
tid: current_tid(),
pid: std::process::id(),
arg: None,
});
}
}
pub struct TracyClient {
active: bool,
}
impl TracyClient {
pub fn new() -> Self {
#[cfg(feature = "tracy")]
{
Lazy::force(&PROCESS_START);
Lazy::force(&TRACE_EVENTS);
TracyClient { active: true }
}
#[cfg(not(feature = "tracy"))]
{
TracyClient { active: false }
}
}
#[inline]
pub fn is_active(&self) -> bool {
self.active
}
#[inline]
pub fn span(&self, name: &str) -> TracySpan {
#[cfg(feature = "tracy")]
{
TracySpan {
name: name.to_owned(),
start: Instant::now(),
}
}
#[cfg(not(feature = "tracy"))]
{
let _ = name;
TracySpan { _phantom: () }
}
}
#[inline]
pub fn frame_mark(&self, name: &str) {
#[cfg(feature = "tracy")]
{
let ts_micros =
Instant::now().duration_since(*PROCESS_START).as_secs_f64() * 1_000_000.0;
record_event(TraceEvent {
name: format!("frame: {name}"),
category: "frame",
ph: 'i',
ts_micros,
dur_micros: None,
tid: current_tid(),
pid: std::process::id(),
arg: None,
});
}
#[cfg(not(feature = "tracy"))]
let _ = name;
}
#[inline]
pub fn message(&self, msg: &str) {
#[cfg(feature = "tracy")]
{
let ts_micros =
Instant::now().duration_since(*PROCESS_START).as_secs_f64() * 1_000_000.0;
record_event(TraceEvent {
name: "message".to_owned(),
category: "log",
ph: 'i',
ts_micros,
dur_micros: None,
tid: current_tid(),
pid: std::process::id(),
arg: Some(msg.to_owned()),
});
}
#[cfg(not(feature = "tracy"))]
let _ = msg;
}
pub fn export_chrome_trace(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
use std::io::Write;
#[cfg(feature = "tracy")]
let document = {
let events = TRACE_EVENTS.lock().clone();
serialize_document(&events)
};
#[cfg(not(feature = "tracy"))]
let document = EMPTY_TRACE_DOCUMENT.to_owned();
let mut file = std::fs::File::create(path)?;
file.write_all(document.as_bytes())?;
Ok(())
}
}
impl Default for TracyClient {
fn default() -> Self {
Self::new()
}
}
#[macro_export]
macro_rules! tracy_span {
($client:expr, $name:expr) => {
let _tracy_span_guard = $client.span($name);
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tracy_client_default_features() {
let client = TracyClient::new();
#[cfg(not(feature = "tracy"))]
assert!(
!client.is_active(),
"TracyClient should be inactive without the tracy feature"
);
#[cfg(feature = "tracy")]
assert!(
client.is_active(),
"TracyClient should be active with the tracy feature"
);
}
#[test]
fn test_tracy_span_drop() {
let client = TracyClient::new();
{
let _span = client.span("test_span_drop");
}
}
#[test]
fn test_tracy_frame_mark() {
let client = TracyClient::new();
client.frame_mark("test_frame");
}
#[test]
fn test_tracy_message() {
let client = TracyClient::new();
client.message("test message from tracy integration test");
}
#[test]
fn test_tracy_default_impl() {
let client = TracyClient::default();
#[cfg(not(feature = "tracy"))]
assert!(!client.is_active());
}
#[test]
fn test_tracy_span_macro() {
let client = TracyClient::new();
tracy_span!(client, "macro_test_span");
}
#[test]
fn test_export_chrome_trace_produces_valid_json_shape() {
let client = TracyClient::new();
client.message("export shape test");
let path = std::env::temp_dir().join(format!(
"scirs2_core_tracy_export_shape_{}.json",
std::process::id()
));
client
.export_chrome_trace(&path)
.expect("export_chrome_trace should succeed");
let contents = std::fs::read_to_string(&path).expect("exported file should be readable");
let _ = std::fs::remove_file(&path);
assert!(
contents.starts_with("{\"traceEvents\":["),
"exported document should start with the traceEvents array: {contents}"
);
assert!(
contents.ends_with("],\"displayTimeUnit\":\"ns\"}"),
"exported document should end with the displayTimeUnit field: {contents}"
);
}
#[cfg(feature = "tracy")]
#[test]
fn test_export_chrome_trace_records_span_event() {
let client = TracyClient::new();
let marker = format!(
"unique_span_marker_{}_{}",
std::process::id(),
current_tid()
);
{
let _span = client.span(&marker);
}
let path = std::env::temp_dir().join(format!(
"scirs2_core_tracy_export_span_{}_{}.json",
std::process::id(),
current_tid()
));
client
.export_chrome_trace(&path)
.expect("export_chrome_trace should succeed");
let contents = std::fs::read_to_string(&path).expect("exported file should be readable");
let _ = std::fs::remove_file(&path);
assert!(
contents.contains(&marker),
"exported document should contain the span's name: {contents}"
);
assert!(
contents.contains("\"ph\":\"X\""),
"exported document should contain a complete-event phase marker: {contents}"
);
}
}