use rustc_hash::FxHashMap;
use serde::Serialize;
use std::io::Write;
use std::path::Path;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, Serialize)]
pub enum Phase {
#[serde(rename = "B")]
Begin,
#[serde(rename = "E")]
End,
#[serde(rename = "X")]
Complete,
#[serde(rename = "i")]
Instant,
#[serde(rename = "M")]
Metadata,
}
#[derive(Debug, Clone, Serialize)]
pub struct TraceEvent {
pub name: String,
pub cat: String,
pub ph: Phase,
pub ts: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub dur: Option<u64>,
pub pid: u32,
pub tid: u32,
#[serde(skip_serializing_if = "FxHashMap::is_empty")]
pub args: FxHashMap<String, serde_json::Value>,
}
pub mod categories {
pub const PROGRAM: &str = "program";
pub const PARSE: &str = "parse";
pub const BIND: &str = "bind";
pub const CHECK: &str = "check";
pub const EMIT: &str = "emit";
pub const IO: &str = "io";
pub const MODULE_RESOLUTION: &str = "moduleResolution";
}
#[derive(Debug)]
pub struct Tracer {
events: Vec<TraceEvent>,
start_time: Instant,
active_spans: FxHashMap<String, Instant>,
pid: u32,
tid: u32,
}
impl Tracer {
pub fn new() -> Self {
Self {
events: Vec::new(),
start_time: Instant::now(),
active_spans: FxHashMap::default(),
pid: std::process::id(),
tid: 1, }
}
fn timestamp(&self) -> u64 {
self.start_time.elapsed().as_micros() as u64
}
pub fn begin(&mut self, name: &str, category: &str) {
let ts = self.timestamp();
let key = format!("{category}:{name}");
self.active_spans.insert(key, Instant::now());
self.events.push(TraceEvent {
name: name.to_string(),
cat: category.to_string(),
ph: Phase::Begin,
ts,
dur: None,
pid: self.pid,
tid: self.tid,
args: FxHashMap::default(),
});
}
pub fn begin_with_args(
&mut self,
name: &str,
category: &str,
args: FxHashMap<String, serde_json::Value>,
) {
let ts = self.timestamp();
let key = format!("{category}:{name}");
self.active_spans.insert(key, Instant::now());
self.events.push(TraceEvent {
name: name.to_string(),
cat: category.to_string(),
ph: Phase::Begin,
ts,
dur: None,
pid: self.pid,
tid: self.tid,
args,
});
}
pub fn end(&mut self, name: &str, category: &str) {
let ts = self.timestamp();
let key = format!("{category}:{name}");
self.active_spans.remove(&key);
self.events.push(TraceEvent {
name: name.to_string(),
cat: category.to_string(),
ph: Phase::End,
ts,
dur: None,
pid: self.pid,
tid: self.tid,
args: FxHashMap::default(),
});
}
pub fn complete(&mut self, name: &str, category: &str, start: Instant, duration: Duration) {
let ts = (start.duration_since(self.start_time)).as_micros() as u64;
let dur = duration.as_micros() as u64;
self.events.push(TraceEvent {
name: name.to_string(),
cat: category.to_string(),
ph: Phase::Complete,
ts,
dur: Some(dur),
pid: self.pid,
tid: self.tid,
args: FxHashMap::default(),
});
}
pub fn complete_with_args(
&mut self,
name: &str,
category: &str,
start: Instant,
duration: Duration,
args: FxHashMap<String, serde_json::Value>,
) {
let ts = (start.duration_since(self.start_time)).as_micros() as u64;
let dur = duration.as_micros() as u64;
self.events.push(TraceEvent {
name: name.to_string(),
cat: category.to_string(),
ph: Phase::Complete,
ts,
dur: Some(dur),
pid: self.pid,
tid: self.tid,
args,
});
}
pub fn instant(&mut self, name: &str, category: &str) {
let ts = self.timestamp();
self.events.push(TraceEvent {
name: name.to_string(),
cat: category.to_string(),
ph: Phase::Instant,
ts,
dur: None,
pid: self.pid,
tid: self.tid,
args: FxHashMap::default(),
});
}
pub fn instant_with_args(
&mut self,
name: &str,
category: &str,
args: FxHashMap<String, serde_json::Value>,
) {
let ts = self.timestamp();
self.events.push(TraceEvent {
name: name.to_string(),
cat: category.to_string(),
ph: Phase::Instant,
ts,
dur: None,
pid: self.pid,
tid: self.tid,
args,
});
}
pub fn metadata(&mut self, name: &str, args: FxHashMap<String, serde_json::Value>) {
self.events.push(TraceEvent {
name: name.to_string(),
cat: "__metadata".to_string(),
ph: Phase::Metadata,
ts: 0,
dur: None,
pid: self.pid,
tid: self.tid,
args,
});
}
pub fn write_to_file(&self, path: &Path) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let file = std::fs::File::create(path)?;
let mut writer = std::io::BufWriter::new(file);
serde_json::to_writer_pretty(&mut writer, &self.events)?;
writer.flush()?;
Ok(())
}
pub fn events(&self) -> &[TraceEvent] {
&self.events
}
pub fn clear(&mut self) {
self.events.clear();
self.active_spans.clear();
}
}
impl Default for Tracer {
fn default() -> Self {
Self::new()
}
}
pub struct TraceSpan<'a> {
tracer: &'a mut Tracer,
name: String,
category: String,
}
impl<'a> TraceSpan<'a> {
pub fn new(tracer: &'a mut Tracer, name: &str, category: &str) -> Self {
tracer.begin(name, category);
TraceSpan {
tracer,
name: name.to_string(),
category: category.to_string(),
}
}
}
impl Drop for TraceSpan<'_> {
fn drop(&mut self) {
self.tracer.end(&self.name, &self.category);
}
}
#[macro_export]
macro_rules! trace_span {
($tracer:expr, $name:expr, $category:expr) => {
let _span = $crate::trace::TraceSpan::new($tracer, $name, $category);
};
}
#[cfg(test)]
#[path = "trace_tests.rs"]
mod tests;