1use crate::model::Loaded;
20use anyhow::{Context, Result};
21use backbeat::schema::{FieldType, Phase};
22use serde_json::{json, Value as Json};
23use std::{collections::HashMap, fs::File, io::Write, path::Path};
24
25pub fn to_trace(dumps: &[Loaded], output: &Path) -> Result<usize> {
27 let events = build_events(dumps);
28 let doc = json!({
29 "displayTimeUnit": "ns",
30 "traceEvents": events,
31 });
32 let mut file =
33 File::create(output).with_context(|| format!("creating {}", output.display()))?;
34 serde_json::to_writer(&mut file, &doc).context("writing trace JSON")?;
35 file.flush().ok();
36 Ok(doc["traceEvents"].as_array().map_or(0, |a| a.len()))
37}
38
39struct Flat<'a> {
41 instance_id: u64,
42 shard_id: u32,
43 ts_nanos: u64,
44 phase: Phase,
45 span_id: Option<u64>,
46 name: &'a str,
47 args: Vec<(&'a str, Json)>,
49}
50
51fn build_events(dumps: &[Loaded]) -> Vec<Json> {
53 let mut flat: Vec<Flat> = Vec::new();
55 let mut max_ts = 0u64;
56 for d in dumps {
57 for r in &d.records {
58 let s = &d.schemas[r.schema_idx];
59 max_ts = max_ts.max(r.ts_nanos);
60 let span_id = s
61 .span_id()
62 .and_then(|f| read_u64(&r.fields, f.offset as usize));
63 let args = s
64 .fields
65 .iter()
66 .map(|f| (f.name.as_str(), decode_json(f, &r.fields, &d.intern)))
67 .collect();
68 flat.push(Flat {
69 instance_id: d.instance_id,
70 shard_id: r.shard_id,
71 ts_nanos: r.ts_nanos,
72 phase: s.phase,
73 span_id,
74 name: &s.qualified_name,
75 args,
76 });
77 }
78 }
79
80 let mut has_enter: HashMap<(u64, u64), bool> = HashMap::new();
82 let mut has_exit: HashMap<(u64, u64), bool> = HashMap::new();
83 for f in &flat {
84 if let Some(sid) = f.span_id {
85 match f.phase {
86 Phase::Enter => *has_enter.entry((f.instance_id, sid)).or_default() = true,
87 Phase::Exit => *has_exit.entry((f.instance_id, sid)).or_default() = true,
88 _ => {}
89 }
90 }
91 }
92
93 let mut events = Vec::with_capacity(flat.len());
94 for f in &flat {
95 let cat = f.name.split("::").next().unwrap_or("");
96 let args: serde_json::Map<String, Json> = f
97 .args
98 .iter()
99 .map(|(k, v)| (k.to_string(), v.clone()))
100 .collect();
101 let pid = f.instance_id;
104 let tid = f.shard_id;
105
106 match (f.phase, f.span_id) {
107 (Phase::Enter, Some(sid)) => {
108 events.push(async_event("b", f, sid, cat, pid, tid, args.clone()));
109 if !has_exit
111 .get(&(f.instance_id, sid))
112 .copied()
113 .unwrap_or(false)
114 {
115 let mut a = args.clone();
116 a.insert("backbeat_open".to_string(), json!(true));
117 events.push(async_event_at("e", f, sid, cat, pid, tid, max_ts, a));
118 }
119 }
120 (Phase::Exit, Some(sid)) => {
121 if !has_enter
123 .get(&(f.instance_id, sid))
124 .copied()
125 .unwrap_or(false)
126 {
127 let mut a = args.clone();
128 a.insert("backbeat_orphan_exit".to_string(), json!(true));
129 events.push(instant_event(f, cat, pid, tid, a));
130 } else {
131 events.push(async_event("e", f, sid, cat, pid, tid, args));
132 }
133 }
134 _ => events.push(instant_event(f, cat, pid, tid, args)),
137 }
138 }
139 events
140}
141
142fn async_event(
144 ph: &str,
145 f: &Flat,
146 sid: u64,
147 cat: &str,
148 pid: u64,
149 tid: u32,
150 args: serde_json::Map<String, Json>,
151) -> Json {
152 async_event_at(ph, f, sid, cat, pid, tid, f.ts_nanos, args)
153}
154
155#[allow(clippy::too_many_arguments)]
157fn async_event_at(
158 ph: &str,
159 f: &Flat,
160 sid: u64,
161 cat: &str,
162 pid: u64,
163 tid: u32,
164 ts_nanos: u64,
165 args: serde_json::Map<String, Json>,
166) -> Json {
167 json!({
168 "ph": ph,
169 "name": f.name,
170 "cat": cat,
171 "id": format!("{sid:#018x}"),
172 "pid": pid,
173 "tid": tid,
174 "ts": ts_micros(ts_nanos),
175 "args": args,
176 })
177}
178
179fn instant_event(
181 f: &Flat,
182 cat: &str,
183 pid: u64,
184 tid: u32,
185 args: serde_json::Map<String, Json>,
186) -> Json {
187 json!({
188 "ph": "i",
189 "name": f.name,
190 "cat": cat,
191 "pid": pid,
192 "tid": tid,
193 "ts": ts_micros(f.ts_nanos),
194 "s": "t", "args": args,
196 })
197}
198
199fn ts_micros(ts_nanos: u64) -> f64 {
202 ts_nanos as f64 / 1000.0
203}
204
205fn read_u64(bytes: &[u8], offset: usize) -> Option<u64> {
207 bytes
208 .get(offset..offset + 8)
209 .map(|s| u64::from_le_bytes(s.try_into().unwrap()))
210}
211
212fn decode_json(
214 field: &backbeat::wire::OwnedField,
215 bytes: &[u8],
216 intern: &HashMap<u32, String>,
217) -> Json {
218 let start = field.offset as usize;
219 let Some(slice) = bytes.get(start..start + field.width as usize) else {
220 return Json::Null;
221 };
222 match field.ty {
227 FieldType::U8 => le::<1>(slice).map_or(Json::Null, |b| json!(b[0])),
228 FieldType::U16 => le::<2>(slice).map_or(Json::Null, |b| json!(u16::from_le_bytes(b))),
229 FieldType::U32 => le::<4>(slice).map_or(Json::Null, |b| json!(u32::from_le_bytes(b))),
230 FieldType::U64 => le::<8>(slice).map_or(Json::Null, |b| json!(u64::from_le_bytes(b))),
231 FieldType::I8 => le::<1>(slice).map_or(Json::Null, |b| json!(b[0] as i8)),
232 FieldType::I16 => le::<2>(slice).map_or(Json::Null, |b| json!(i16::from_le_bytes(b))),
233 FieldType::I32 => le::<4>(slice).map_or(Json::Null, |b| json!(i32::from_le_bytes(b))),
234 FieldType::I64 => le::<8>(slice).map_or(Json::Null, |b| json!(i64::from_le_bytes(b))),
235 FieldType::Bool => slice.first().map_or(Json::Null, |b| json!(*b != 0)),
236 FieldType::Bytes => json!(hex(slice)),
237 FieldType::Enum { repr } => {
238 let Some(raw_bytes) = slice.get(..repr as usize) else {
239 return Json::Null;
240 };
241 let mut buf = [0u8; 8];
242 buf[..repr as usize].copy_from_slice(raw_bytes);
243 let raw = u64::from_le_bytes(buf);
244 match field.enum_labels.iter().find(|l| l.value == raw) {
245 Some(l) => json!(l.label),
246 None => json!(raw),
247 }
248 }
249 FieldType::Interned { .. } => match le::<4>(slice) {
250 Some(b) => {
251 let id = u32::from_le_bytes(b);
252 match intern.get(&id) {
253 Some(s) => json!(s),
254 None => json!(format!("#{id}")),
255 }
256 }
257 None => Json::Null,
258 },
259 _ => json!(hex(slice)),
260 }
261}
262
263fn le<const N: usize>(slice: &[u8]) -> Option<[u8; N]> {
267 slice.get(..N)?.try_into().ok()
268}
269
270fn hex(bytes: &[u8]) -> String {
271 let mut s = String::with_capacity(bytes.len() * 2);
272 for b in bytes {
273 s.push_str(&format!("{b:02x}"));
274 }
275 s
276}