1use crate::model::{self, Loaded};
20use anyhow::{Context, Result};
21use backbeat::schema::{FieldType, Phase};
22use serde_json::{json, Value as Json};
23use std::{
24 collections::{HashMap, HashSet},
25 fs::File,
26 io::{BufWriter, Write},
27 path::Path,
28};
29
30pub fn to_trace(dumps: &[Loaded], output: &Path) -> Result<usize> {
40 let records = model::unique_records(dumps);
44
45 let mut max_ts = 0u64;
49 let mut has_enter: HashSet<(u64, u64)> = HashSet::new();
50 let mut has_exit: HashSet<(u64, u64)> = HashSet::new();
51 for (d, r) in &records {
52 let s = &d.schemas[r.schema_idx];
53 max_ts = max_ts.max(r.ts_nanos);
54 if let Some(sid) = s
55 .span_id()
56 .and_then(|f| read_u64(&r.fields, f.offset as usize))
57 {
58 match s.phase {
59 Phase::Enter => {
60 has_enter.insert((d.instance_id, sid));
61 }
62 Phase::Exit => {
63 has_exit.insert((d.instance_id, sid));
64 }
65 _ => {}
66 }
67 }
68 }
69
70 let file = File::create(output).with_context(|| format!("creating {}", output.display()))?;
74 let mut w = BufWriter::new(file);
75 write!(w, "{{\"displayTimeUnit\":\"ns\",\"traceEvents\":[").context("writing trace JSON")?;
76
77 let mut count = 0usize;
78 for (d, r) in &records {
79 let s = &d.schemas[r.schema_idx];
80 let span_id = s
81 .span_id()
82 .and_then(|f| read_u64(&r.fields, f.offset as usize));
83 let args: serde_json::Map<String, Json> = s
85 .fields
86 .iter()
87 .map(|f| (f.name.to_string(), decode_json(f, &r.fields, &d.intern)))
88 .collect();
89 let f = Flat {
90 instance_id: d.instance_id,
91 shard_id: r.shard_id,
92 ts_nanos: r.ts_nanos,
93 phase: s.phase,
94 span_id,
95 name: &s.qualified_name,
96 };
97 emit_record(&mut w, &f, args, max_ts, &has_enter, &has_exit, &mut count)?;
98 }
99
100 write!(w, "]}}").context("writing trace JSON")?;
101 w.flush().context("flushing trace JSON")?;
102 Ok(count)
103}
104
105struct Flat<'a> {
108 instance_id: u64,
109 shard_id: u32,
110 ts_nanos: u64,
111 phase: Phase,
112 span_id: Option<u64>,
113 name: &'a str,
114}
115
116fn write_event(w: &mut impl Write, event: &Json, count: &mut usize) -> Result<()> {
119 if *count > 0 {
120 write!(w, ",").context("writing trace JSON")?;
121 }
122 serde_json::to_writer(&mut *w, event).context("writing trace JSON")?;
123 *count += 1;
124 Ok(())
125}
126
127fn emit_record(
130 w: &mut impl Write,
131 f: &Flat,
132 args: serde_json::Map<String, Json>,
133 max_ts: u64,
134 has_enter: &HashSet<(u64, u64)>,
135 has_exit: &HashSet<(u64, u64)>,
136 count: &mut usize,
137) -> Result<()> {
138 let cat = f.name.split("::").next().unwrap_or("");
139 let pid = f.instance_id;
142 let tid = f.shard_id;
143
144 match (f.phase, f.span_id) {
145 (Phase::Enter, Some(sid)) => {
146 if !has_exit.contains(&(f.instance_id, sid)) {
148 let mut a = args.clone();
149 a.insert("backbeat_open".to_string(), json!(true));
150 let close = async_event_at("e", f, sid, cat, pid, tid, max_ts, a);
151 write_event(w, &async_event("b", f, sid, cat, pid, tid, args), count)?;
152 write_event(w, &close, count)?;
153 } else {
154 write_event(w, &async_event("b", f, sid, cat, pid, tid, args), count)?;
155 }
156 }
157 (Phase::Exit, Some(sid)) => {
158 if !has_enter.contains(&(f.instance_id, sid)) {
160 let mut a = args;
161 a.insert("backbeat_orphan_exit".to_string(), json!(true));
162 write_event(w, &instant_event(f, cat, pid, tid, a), count)?;
163 } else {
164 write_event(w, &async_event("e", f, sid, cat, pid, tid, args), count)?;
165 }
166 }
167 _ => write_event(w, &instant_event(f, cat, pid, tid, args), count)?,
170 }
171 Ok(())
172}
173
174fn async_event(
176 ph: &str,
177 f: &Flat,
178 sid: u64,
179 cat: &str,
180 pid: u64,
181 tid: u32,
182 args: serde_json::Map<String, Json>,
183) -> Json {
184 async_event_at(ph, f, sid, cat, pid, tid, f.ts_nanos, args)
185}
186
187#[allow(clippy::too_many_arguments)]
189fn async_event_at(
190 ph: &str,
191 f: &Flat,
192 sid: u64,
193 cat: &str,
194 pid: u64,
195 tid: u32,
196 ts_nanos: u64,
197 args: serde_json::Map<String, Json>,
198) -> Json {
199 json!({
200 "ph": ph,
201 "name": f.name,
202 "cat": cat,
203 "id": format!("{sid:#018x}"),
204 "pid": pid,
205 "tid": tid,
206 "ts": ts_micros(ts_nanos),
207 "args": args,
208 })
209}
210
211fn instant_event(
213 f: &Flat,
214 cat: &str,
215 pid: u64,
216 tid: u32,
217 args: serde_json::Map<String, Json>,
218) -> Json {
219 json!({
220 "ph": "i",
221 "name": f.name,
222 "cat": cat,
223 "pid": pid,
224 "tid": tid,
225 "ts": ts_micros(f.ts_nanos),
226 "s": "t", "args": args,
228 })
229}
230
231fn ts_micros(ts_nanos: u64) -> f64 {
234 ts_nanos as f64 / 1000.0
235}
236
237fn read_u64(bytes: &[u8], offset: usize) -> Option<u64> {
239 bytes
240 .get(offset..offset + 8)
241 .map(|s| u64::from_le_bytes(s.try_into().unwrap()))
242}
243
244fn decode_json(
246 field: &backbeat::wire::OwnedField,
247 bytes: &[u8],
248 intern: &HashMap<u32, String>,
249) -> Json {
250 let start = field.offset as usize;
251 let Some(slice) = bytes.get(start..start + field.width as usize) else {
252 return Json::Null;
253 };
254 match field.ty {
259 FieldType::U8 => le::<1>(slice).map_or(Json::Null, |b| json!(b[0])),
260 FieldType::U16 => le::<2>(slice).map_or(Json::Null, |b| json!(u16::from_le_bytes(b))),
261 FieldType::U32 => le::<4>(slice).map_or(Json::Null, |b| json!(u32::from_le_bytes(b))),
262 FieldType::U64 => le::<8>(slice).map_or(Json::Null, |b| json!(u64::from_le_bytes(b))),
263 FieldType::I8 => le::<1>(slice).map_or(Json::Null, |b| json!(b[0] as i8)),
264 FieldType::I16 => le::<2>(slice).map_or(Json::Null, |b| json!(i16::from_le_bytes(b))),
265 FieldType::I32 => le::<4>(slice).map_or(Json::Null, |b| json!(i32::from_le_bytes(b))),
266 FieldType::I64 => le::<8>(slice).map_or(Json::Null, |b| json!(i64::from_le_bytes(b))),
267 FieldType::Bool => slice.first().map_or(Json::Null, |b| json!(*b != 0)),
268 FieldType::Bytes => json!(hex(slice)),
269 FieldType::Enum { repr } => {
270 let Some(raw_bytes) = slice.get(..repr as usize) else {
271 return Json::Null;
272 };
273 let mut buf = [0u8; 8];
274 buf[..repr as usize].copy_from_slice(raw_bytes);
275 let raw = u64::from_le_bytes(buf);
276 match field.enum_labels.iter().find(|l| l.value == raw) {
277 Some(l) => json!(l.label),
278 None => json!(raw),
279 }
280 }
281 FieldType::Interned { .. } => match le::<4>(slice) {
282 Some(b) => {
283 let id = u32::from_le_bytes(b);
284 match intern.get(&id) {
285 Some(s) => json!(s),
286 None => json!(format!("#{id}")),
287 }
288 }
289 None => Json::Null,
290 },
291 _ => json!(hex(slice)),
292 }
293}
294
295fn le<const N: usize>(slice: &[u8]) -> Option<[u8; N]> {
299 slice.get(..N)?.try_into().ok()
300}
301
302fn hex(bytes: &[u8]) -> String {
303 let mut s = String::with_capacity(bytes.len() * 2);
304 for b in bytes {
305 s.push_str(&format!("{b:02x}"));
306 }
307 s
308}