use std::collections::{HashMap, VecDeque};
use std::fmt::Write as _;
use std::sync::Mutex;
use std::thread::ThreadId;
use crate::timestamp::Timestamp;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TracePhase {
Open,
Process,
Close,
}
impl TracePhase {
pub fn as_str(self) -> &'static str {
match self {
TracePhase::Open => "open",
TracePhase::Process => "process",
TracePhase::Close => "close",
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct TraceEvent {
pub node: u32,
pub phase: TracePhase,
pub tid: u32,
pub start_us: i64,
pub dur_us: i64,
pub input_ts: i64,
}
struct Inner {
events: VecDeque<TraceEvent>,
capacity: usize,
tids: HashMap<ThreadId, u32>,
next_tid: u32,
}
pub struct TraceRing {
inner: Mutex<Inner>,
}
impl TraceRing {
pub fn new(capacity: usize) -> Self {
Self {
inner: Mutex::new(Inner {
events: VecDeque::with_capacity(capacity.min(1024)),
capacity: capacity.max(1),
tids: HashMap::new(),
next_tid: 0,
}),
}
}
pub fn record(&self, node: u32, phase: TracePhase, start_us: i64, dur_us: i64, input_ts: i64) {
let id = std::thread::current().id();
let mut g = self.inner.lock().expect("trace ring poisoned");
let tid = match g.tids.get(&id) {
Some(&t) => t,
None => {
let t = g.next_tid;
g.next_tid += 1;
g.tids.insert(id, t);
t
}
};
if g.events.len() >= g.capacity {
g.events.pop_front();
}
g.events.push_back(TraceEvent {
node,
phase,
tid,
start_us,
dur_us,
input_ts,
});
}
pub fn snapshot(&self) -> Vec<TraceEvent> {
let g = self.inner.lock().expect("trace ring poisoned");
g.events.iter().copied().collect()
}
pub fn clear(&self) {
let mut g = self.inner.lock().expect("trace ring poisoned");
g.events.clear();
g.tids.clear();
g.next_tid = 0;
}
}
fn push_json_escaped(out: &mut String, s: &str) {
out.push('"');
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 => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
}
pub fn to_chrome_trace_json(events: &[TraceEvent], labels: &[(String, String)]) -> String {
let mut out = String::with_capacity(128 + events.len() * 128);
out.push_str("{\"traceEvents\":[");
let mut first = true;
let mut tids: Vec<u32> = events.iter().map(|e| e.tid).collect();
tids.sort_unstable();
tids.dedup();
for tid in tids {
if first {
first = false;
out.push_str(
"{\"name\":\"process_name\",\"ph\":\"M\",\"pid\":1,\
\"args\":{\"name\":\"lm-flow graph\"}},",
);
} else {
out.push(',');
}
let _ = write!(
out,
"{{\"name\":\"thread_name\",\"ph\":\"M\",\"pid\":1,\"tid\":{tid},\
\"args\":{{\"name\":\"worker {tid}\"}}}}"
);
}
for e in events {
if first {
first = false;
} else {
out.push(',');
}
let (node_name, kernel_name) = labels
.get(e.node as usize)
.map(|(n, k)| (n.as_str(), k.as_str()))
.unwrap_or(("<unknown>", ""));
out.push('{');
out.push_str("\"name\":");
push_json_escaped(&mut out, node_name);
out.push_str(",\"cat\":");
push_json_escaped(&mut out, e.phase.as_str());
out.push_str(",\"ph\":\"X\",\"pid\":1,\"tid\":");
let _ = write!(out, "{}", e.tid);
out.push_str(",\"ts\":");
let _ = write!(out, "{}", e.start_us);
out.push_str(",\"dur\":");
let _ = write!(out, "{}", e.dur_us.max(0));
out.push_str(",\"args\":{\"kernel\":");
push_json_escaped(&mut out, kernel_name);
out.push_str(",\"phase\":");
push_json_escaped(&mut out, e.phase.as_str());
out.push_str(",\"input_ts\":");
let ts = Timestamp(e.input_ts);
if ts.is_range_value() {
let _ = write!(out, "{}", e.input_ts);
} else {
push_json_escaped(&mut out, &ts.to_string());
}
out.push_str("}}");
}
out.push_str("],\"displayTimeUnit\":\"ms\"}");
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ring_is_bounded_and_drops_oldest() {
let ring = TraceRing::new(2);
ring.record(0, TracePhase::Process, 0, 5, 100);
ring.record(1, TracePhase::Process, 10, 5, 200);
ring.record(2, TracePhase::Process, 20, 5, 300);
let snap = ring.snapshot();
assert_eq!(snap.len(), 2, "capacity 2 keeps only 2");
assert_eq!(snap[0].node, 1, "oldest (node 0) was evicted");
assert_eq!(snap[1].node, 2);
}
#[test]
fn clear_resets_events_and_tids() {
let ring = TraceRing::new(4);
ring.record(0, TracePhase::Open, 0, 1, 0);
ring.clear();
assert!(ring.snapshot().is_empty());
ring.record(0, TracePhase::Process, 0, 1, 0);
assert_eq!(ring.snapshot()[0].tid, 0);
}
#[test]
fn chrome_json_escapes_and_wraps() {
let events = vec![TraceEvent {
node: 0,
phase: TracePhase::Process,
tid: 3,
start_us: 12,
dur_us: 34,
input_ts: 1000,
}];
let labels = vec![("no\"de".to_string(), "Kern\\el".to_string())];
let json = to_chrome_trace_json(&events, &labels);
assert!(json.starts_with("{\"traceEvents\":["));
assert!(json.contains("\"ts\":12"));
assert!(json.contains("\"dur\":34"));
assert!(json.contains("\"tid\":3"));
assert!(json.contains("no\\\"de"), "name quote escaped");
assert!(json.contains("Kern\\\\el"), "kernel backslash escaped");
assert!(json.contains("\"input_ts\":1000"));
}
#[test]
fn lanes_get_named_and_empty_stays_valid() {
let ev = |tid| TraceEvent {
node: 0,
phase: TracePhase::Process,
tid,
start_us: 0,
dur_us: 1,
input_ts: 7,
};
let labels = vec![("n".to_string(), "K".to_string())];
let json = to_chrome_trace_json(&[ev(0), ev(2), ev(0)], &labels);
assert!(json.contains("\"process_name\""), "进程名: {json}");
assert_eq!(
json.matches("\"thread_name\"").count(),
2,
"两条泳道各一条,重复 tid 不重复发: {json}"
);
assert!(
json.contains("\"name\":\"worker 2\""),
"泳道 2 命名: {json}"
);
let empty = to_chrome_trace_json(&[], &labels);
assert!(
empty.contains("\"traceEvents\":[]"),
"空 trace 不带元事件: {empty}"
);
}
#[test]
fn sentinel_input_ts_exports_as_a_name() {
let ev = |ts: Timestamp| TraceEvent {
node: 0,
phase: TracePhase::Open,
tid: 0,
start_us: 0,
dur_us: 1,
input_ts: ts.0,
};
let labels = vec![("n".to_string(), "K".to_string())];
let json = to_chrome_trace_json(&[ev(Timestamp::unstarted())], &labels);
assert!(
json.contains("\"input_ts\":\"Unstarted\""),
"哨兵应记成名字: {json}"
);
assert!(
!json.contains(&format!("{}", i64::MIN + 1)),
"不该出现天文数字: {json}"
);
let json = to_chrome_trace_json(&[ev(Timestamp::done())], &labels);
assert!(json.contains("\"input_ts\":\"Done\""), "Done: {json}");
let json = to_chrome_trace_json(&[ev(Timestamp(42))], &labels);
assert!(json.contains("\"input_ts\":42"), "普通值仍为数字: {json}");
}
#[test]
fn zero_capacity_ring_stays_bounded() {
let ring = TraceRing::new(0);
for i in 0..5 {
ring.record(i, TracePhase::Process, 0, 1, 0);
}
assert_eq!(ring.snapshot().len(), 1, "0 容量按 1 处理,仍然有界");
}
}