use crate::model::{self, Loaded};
use anyhow::{Context, Result};
use backbeat::schema::{FieldType, Phase};
use serde_json::{json, Value as Json};
use std::{
collections::{HashMap, HashSet},
fs::File,
io::{BufWriter, Write},
path::Path,
};
pub fn to_trace(dumps: &[Loaded], output: &Path) -> Result<usize> {
let records = model::unique_records(dumps);
let mut max_ts = 0u64;
let mut has_enter: HashSet<(u64, u64)> = HashSet::new();
let mut has_exit: HashSet<(u64, u64)> = HashSet::new();
for (d, r) in &records {
let s = &d.schemas[r.schema_idx];
max_ts = max_ts.max(r.ts_nanos);
if let Some(sid) = s
.span_id()
.and_then(|f| read_u64(&r.fields, f.offset as usize))
{
match s.phase {
Phase::Enter => {
has_enter.insert((d.instance_id, sid));
}
Phase::Exit => {
has_exit.insert((d.instance_id, sid));
}
_ => {}
}
}
}
let file = File::create(output).with_context(|| format!("creating {}", output.display()))?;
let mut w = BufWriter::new(file);
write!(w, "{{\"displayTimeUnit\":\"ns\",\"traceEvents\":[").context("writing trace JSON")?;
let mut count = 0usize;
for (d, r) in &records {
let s = &d.schemas[r.schema_idx];
let span_id = s
.span_id()
.and_then(|f| read_u64(&r.fields, f.offset as usize));
let args: serde_json::Map<String, Json> = s
.fields
.iter()
.map(|f| (f.name.to_string(), decode_json(f, &r.fields, &d.intern)))
.collect();
let f = 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,
};
emit_record(&mut w, &f, args, max_ts, &has_enter, &has_exit, &mut count)?;
}
write!(w, "]}}").context("writing trace JSON")?;
w.flush().context("flushing trace JSON")?;
Ok(count)
}
struct Flat<'a> {
instance_id: u64,
shard_id: u32,
ts_nanos: u64,
phase: Phase,
span_id: Option<u64>,
name: &'a str,
}
fn write_event(w: &mut impl Write, event: &Json, count: &mut usize) -> Result<()> {
if *count > 0 {
write!(w, ",").context("writing trace JSON")?;
}
serde_json::to_writer(&mut *w, event).context("writing trace JSON")?;
*count += 1;
Ok(())
}
fn emit_record(
w: &mut impl Write,
f: &Flat,
args: serde_json::Map<String, Json>,
max_ts: u64,
has_enter: &HashSet<(u64, u64)>,
has_exit: &HashSet<(u64, u64)>,
count: &mut usize,
) -> Result<()> {
let cat = f.name.split("::").next().unwrap_or("");
let pid = f.instance_id;
let tid = f.shard_id;
match (f.phase, f.span_id) {
(Phase::Enter, Some(sid)) => {
if !has_exit.contains(&(f.instance_id, sid)) {
let mut a = args.clone();
a.insert("backbeat_open".to_string(), json!(true));
let close = async_event_at("e", f, sid, cat, pid, tid, max_ts, a);
write_event(w, &async_event("b", f, sid, cat, pid, tid, args), count)?;
write_event(w, &close, count)?;
} else {
write_event(w, &async_event("b", f, sid, cat, pid, tid, args), count)?;
}
}
(Phase::Exit, Some(sid)) => {
if !has_enter.contains(&(f.instance_id, sid)) {
let mut a = args;
a.insert("backbeat_orphan_exit".to_string(), json!(true));
write_event(w, &instant_event(f, cat, pid, tid, a), count)?;
} else {
write_event(w, &async_event("e", f, sid, cat, pid, tid, args), count)?;
}
}
_ => write_event(w, &instant_event(f, cat, pid, tid, args), count)?,
}
Ok(())
}
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
}