use crate::model::Loaded;
use anyhow::{Context, Result};
use backbeat::schema::{FieldType, Phase};
use serde_json::{json, Value as Json};
use std::{collections::HashMap, fs::File, io::Write, path::Path};
pub fn to_trace(dumps: &[Loaded], output: &Path) -> Result<usize> {
let events = build_events(dumps);
let doc = json!({
"displayTimeUnit": "ns",
"traceEvents": events,
});
let mut file =
File::create(output).with_context(|| format!("creating {}", output.display()))?;
serde_json::to_writer(&mut file, &doc).context("writing trace JSON")?;
file.flush().ok();
Ok(doc["traceEvents"].as_array().map_or(0, |a| a.len()))
}
struct Flat<'a> {
instance_id: u64,
shard_id: u32,
ts_nanos: u64,
phase: Phase,
span_id: Option<u64>,
name: &'a str,
args: Vec<(&'a str, Json)>,
}
fn build_events(dumps: &[Loaded]) -> Vec<Json> {
let mut flat: Vec<Flat> = Vec::new();
let mut max_ts = 0u64;
for d in dumps {
for r in &d.records {
let s = &d.schemas[r.schema_idx];
max_ts = max_ts.max(r.ts_nanos);
let span_id = s
.span_id()
.and_then(|f| read_u64(&r.fields, f.offset as usize));
let args = s
.fields
.iter()
.map(|f| (f.name.as_str(), decode_json(f, &r.fields, &d.intern)))
.collect();
flat.push(Flat {
instance_id: d.instance_id,
shard_id: r.shard_id,
ts_nanos: r.ts_nanos,
phase: s.phase,
span_id,
name: &s.qualified_name,
args,
});
}
}
let mut has_enter: HashMap<(u64, u64), bool> = HashMap::new();
let mut has_exit: HashMap<(u64, u64), bool> = HashMap::new();
for f in &flat {
if let Some(sid) = f.span_id {
match f.phase {
Phase::Enter => *has_enter.entry((f.instance_id, sid)).or_default() = true,
Phase::Exit => *has_exit.entry((f.instance_id, sid)).or_default() = true,
_ => {}
}
}
}
let mut events = Vec::with_capacity(flat.len());
for f in &flat {
let cat = f.name.split("::").next().unwrap_or("");
let args: serde_json::Map<String, Json> = f
.args
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect();
let pid = f.instance_id;
let tid = f.shard_id;
match (f.phase, f.span_id) {
(Phase::Enter, Some(sid)) => {
events.push(async_event("b", f, sid, cat, pid, tid, args.clone()));
if !has_exit
.get(&(f.instance_id, sid))
.copied()
.unwrap_or(false)
{
let mut a = args.clone();
a.insert("backbeat_open".to_string(), json!(true));
events.push(async_event_at("e", f, sid, cat, pid, tid, max_ts, a));
}
}
(Phase::Exit, Some(sid)) => {
if !has_enter
.get(&(f.instance_id, sid))
.copied()
.unwrap_or(false)
{
let mut a = args.clone();
a.insert("backbeat_orphan_exit".to_string(), json!(true));
events.push(instant_event(f, cat, pid, tid, a));
} else {
events.push(async_event("e", f, sid, cat, pid, tid, args));
}
}
_ => events.push(instant_event(f, cat, pid, tid, args)),
}
}
events
}
fn async_event(
ph: &str,
f: &Flat,
sid: u64,
cat: &str,
pid: u64,
tid: u32,
args: serde_json::Map<String, Json>,
) -> Json {
async_event_at(ph, f, sid, cat, pid, tid, f.ts_nanos, args)
}
#[allow(clippy::too_many_arguments)]
fn async_event_at(
ph: &str,
f: &Flat,
sid: u64,
cat: &str,
pid: u64,
tid: u32,
ts_nanos: u64,
args: serde_json::Map<String, Json>,
) -> Json {
json!({
"ph": ph,
"name": f.name,
"cat": cat,
"id": format!("{sid:#018x}"),
"pid": pid,
"tid": tid,
"ts": ts_micros(ts_nanos),
"args": args,
})
}
fn instant_event(
f: &Flat,
cat: &str,
pid: u64,
tid: u32,
args: serde_json::Map<String, Json>,
) -> Json {
json!({
"ph": "i",
"name": f.name,
"cat": cat,
"pid": pid,
"tid": tid,
"ts": ts_micros(f.ts_nanos),
"s": "t", "args": args,
})
}
fn ts_micros(ts_nanos: u64) -> f64 {
ts_nanos as f64 / 1000.0
}
fn read_u64(bytes: &[u8], offset: usize) -> Option<u64> {
bytes
.get(offset..offset + 8)
.map(|s| u64::from_le_bytes(s.try_into().unwrap()))
}
fn decode_json(
field: &backbeat::wire::OwnedField,
bytes: &[u8],
intern: &HashMap<u32, String>,
) -> Json {
let start = field.offset as usize;
let Some(slice) = bytes.get(start..start + field.width as usize) else {
return Json::Null;
};
match field.ty {
FieldType::U8 => le::<1>(slice).map_or(Json::Null, |b| json!(b[0])),
FieldType::U16 => le::<2>(slice).map_or(Json::Null, |b| json!(u16::from_le_bytes(b))),
FieldType::U32 => le::<4>(slice).map_or(Json::Null, |b| json!(u32::from_le_bytes(b))),
FieldType::U64 => le::<8>(slice).map_or(Json::Null, |b| json!(u64::from_le_bytes(b))),
FieldType::I8 => le::<1>(slice).map_or(Json::Null, |b| json!(b[0] as i8)),
FieldType::I16 => le::<2>(slice).map_or(Json::Null, |b| json!(i16::from_le_bytes(b))),
FieldType::I32 => le::<4>(slice).map_or(Json::Null, |b| json!(i32::from_le_bytes(b))),
FieldType::I64 => le::<8>(slice).map_or(Json::Null, |b| json!(i64::from_le_bytes(b))),
FieldType::Bool => slice.first().map_or(Json::Null, |b| json!(*b != 0)),
FieldType::Bytes => json!(hex(slice)),
FieldType::Enum { repr } => {
let Some(raw_bytes) = slice.get(..repr as usize) else {
return Json::Null;
};
let mut buf = [0u8; 8];
buf[..repr as usize].copy_from_slice(raw_bytes);
let raw = u64::from_le_bytes(buf);
match field.enum_labels.iter().find(|l| l.value == raw) {
Some(l) => json!(l.label),
None => json!(raw),
}
}
FieldType::Interned { .. } => match le::<4>(slice) {
Some(b) => {
let id = u32::from_le_bytes(b);
match intern.get(&id) {
Some(s) => json!(s),
None => json!(format!("#{id}")),
}
}
None => Json::Null,
},
_ => json!(hex(slice)),
}
}
fn le<const N: usize>(slice: &[u8]) -> Option<[u8; N]> {
slice.get(..N)?.try_into().ok()
}
fn hex(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push_str(&format!("{b:02x}"));
}
s
}