use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChromeTraceSchemaOutcome {
Ok { event_count: usize },
NotAnObject,
MissingTraceEvents,
TraceEventsNotArray,
TraceEventsEmpty,
EventNotAnObject { index: usize },
EventMissingField { index: usize, field: &'static str },
InvalidDisplayTimeUnit { got: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AllocFreePairingOutcome {
Ok,
OrphanAllocs { addresses: Vec<String> },
OrphanFrees { addresses: Vec<String> },
AllocMissingAddr { index: usize },
}
#[derive(Debug, Clone, PartialEq)]
pub enum MonotonicTimestampsOutcome {
Ok,
NonMonotonic {
index: usize,
pid: i64,
tid: i64,
prev_ts: f64,
got_ts: f64,
},
}
pub fn classify_schema(body: &Value) -> ChromeTraceSchemaOutcome {
let Some(obj) = body.as_object() else {
return ChromeTraceSchemaOutcome::NotAnObject;
};
if let Some(unit) = obj.get("displayTimeUnit").and_then(Value::as_str) {
if unit != "ns" && unit != "ms" {
return ChromeTraceSchemaOutcome::InvalidDisplayTimeUnit {
got: unit.to_string(),
};
}
}
let Some(events_val) = obj.get("traceEvents") else {
return ChromeTraceSchemaOutcome::MissingTraceEvents;
};
let Some(events) = events_val.as_array() else {
return ChromeTraceSchemaOutcome::TraceEventsNotArray;
};
if events.is_empty() {
return ChromeTraceSchemaOutcome::TraceEventsEmpty;
}
for (i, e) in events.iter().enumerate() {
if !e.is_object() {
return ChromeTraceSchemaOutcome::EventNotAnObject { index: i };
}
for field in ["ph", "ts", "name"] {
if e.get(field).is_none() {
return ChromeTraceSchemaOutcome::EventMissingField { index: i, field };
}
}
}
ChromeTraceSchemaOutcome::Ok {
event_count: events.len(),
}
}
pub fn classify_alloc_free_pairing(body: &Value) -> AllocFreePairingOutcome {
let mut allocs: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut frees: std::collections::HashSet<String> = std::collections::HashSet::new();
let Some(events) = body.get("traceEvents").and_then(Value::as_array) else {
return AllocFreePairingOutcome::Ok;
};
for (i, e) in events.iter().enumerate() {
let name = e.get("name").and_then(Value::as_str).unwrap_or("");
if name != "alloc" && name != "free" {
continue;
}
let addr = e
.get("args")
.and_then(|a| a.get("addr"))
.and_then(Value::as_str);
let Some(addr) = addr else {
if name == "alloc" {
return AllocFreePairingOutcome::AllocMissingAddr { index: i };
}
continue;
};
if name == "alloc" {
allocs.insert(addr.to_string());
} else {
frees.insert(addr.to_string());
}
}
let mut orphan_a: Vec<String> = allocs.difference(&frees).cloned().collect();
let mut orphan_f: Vec<String> = frees.difference(&allocs).cloned().collect();
if !orphan_a.is_empty() {
orphan_a.sort();
return AllocFreePairingOutcome::OrphanAllocs {
addresses: orphan_a,
};
}
if !orphan_f.is_empty() {
orphan_f.sort();
return AllocFreePairingOutcome::OrphanFrees {
addresses: orphan_f,
};
}
AllocFreePairingOutcome::Ok
}
pub fn classify_monotonic_timestamps(body: &Value) -> MonotonicTimestampsOutcome {
use std::collections::HashMap;
let mut last: HashMap<(i64, i64), f64> = HashMap::new();
let Some(events) = body.get("traceEvents").and_then(Value::as_array) else {
return MonotonicTimestampsOutcome::Ok;
};
for (i, e) in events.iter().enumerate() {
let pid = e.get("pid").and_then(Value::as_i64).unwrap_or(0);
let tid = e.get("tid").and_then(Value::as_i64).unwrap_or(0);
let ts = e.get("ts").and_then(Value::as_f64).unwrap_or(0.0);
let key = (pid, tid);
if let Some(&prev) = last.get(&key) {
if ts < prev {
return MonotonicTimestampsOutcome::NonMonotonic {
index: i,
pid,
tid,
prev_ts: prev,
got_ts: ts,
};
}
}
last.insert(key, ts);
}
MonotonicTimestampsOutcome::Ok
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn good_body() -> Value {
json!({
"displayTimeUnit": "ns",
"traceEvents": [
{"ph": "i", "ts": 0, "name": "alloc", "pid": 0, "tid": 1, "args": {"bytes": 1024, "addr": "0xAAAA"}},
{"ph": "i", "ts": 100, "name": "alloc", "pid": 0, "tid": 1, "args": {"bytes": 2048, "addr": "0xBBBB"}},
{"ph": "i", "ts": 200, "name": "free", "pid": 0, "tid": 1, "args": {"bytes": 1024, "addr": "0xAAAA"}},
{"ph": "i", "ts": 300, "name": "free", "pid": 0, "tid": 1, "args": {"bytes": 2048, "addr": "0xBBBB"}},
]
})
}
#[test]
fn schema_ok_on_well_formed_body() {
assert_eq!(
classify_schema(&good_body()),
ChromeTraceSchemaOutcome::Ok { event_count: 4 }
);
}
#[test]
fn schema_rejects_not_an_object() {
assert_eq!(
classify_schema(&json!([1, 2])),
ChromeTraceSchemaOutcome::NotAnObject
);
}
#[test]
fn schema_rejects_missing_trace_events() {
assert_eq!(
classify_schema(&json!({"otherKey": []})),
ChromeTraceSchemaOutcome::MissingTraceEvents
);
}
#[test]
fn schema_rejects_empty_trace_events() {
assert_eq!(
classify_schema(&json!({"traceEvents": []})),
ChromeTraceSchemaOutcome::TraceEventsEmpty
);
}
#[test]
fn schema_rejects_event_missing_ph() {
let body = json!({"traceEvents": [{"ts": 0, "name": "alloc"}]});
match classify_schema(&body) {
ChromeTraceSchemaOutcome::EventMissingField {
index: 0,
field: "ph",
} => {}
other => panic!("expected EventMissingField(ph), got {other:?}"),
}
}
#[test]
fn schema_rejects_invalid_display_time_unit() {
let body = json!({
"displayTimeUnit": "sec",
"traceEvents": [{"ph": "i", "ts": 0, "name": "alloc"}]
});
assert!(matches!(
classify_schema(&body),
ChromeTraceSchemaOutcome::InvalidDisplayTimeUnit { .. }
));
}
#[test]
fn alloc_free_pairing_ok_on_balanced_body() {
assert_eq!(
classify_alloc_free_pairing(&good_body()),
AllocFreePairingOutcome::Ok
);
}
#[test]
fn alloc_free_pairing_reports_orphan_alloc() {
let body = json!({
"traceEvents": [
{"ph": "i", "ts": 0, "name": "alloc", "args": {"addr": "0xLEAK"}},
{"ph": "i", "ts": 1, "name": "alloc", "args": {"addr": "0xOK"}},
{"ph": "i", "ts": 2, "name": "free", "args": {"addr": "0xOK"}},
]
});
match classify_alloc_free_pairing(&body) {
AllocFreePairingOutcome::OrphanAllocs { addresses } => {
assert_eq!(addresses, vec!["0xLEAK".to_string()]);
}
other => panic!("expected OrphanAllocs, got {other:?}"),
}
}
#[test]
fn alloc_free_pairing_reports_orphan_free() {
let body = json!({
"traceEvents": [
{"ph": "i", "ts": 0, "name": "alloc", "args": {"addr": "0xAAA"}},
{"ph": "i", "ts": 1, "name": "free", "args": {"addr": "0xAAA"}},
{"ph": "i", "ts": 2, "name": "free", "args": {"addr": "0xPHANTOM"}},
]
});
match classify_alloc_free_pairing(&body) {
AllocFreePairingOutcome::OrphanFrees { addresses } => {
assert_eq!(addresses, vec!["0xPHANTOM".to_string()]);
}
other => panic!("expected OrphanFrees, got {other:?}"),
}
}
#[test]
fn alloc_free_pairing_reports_alloc_missing_addr() {
let body = json!({
"traceEvents": [
{"ph": "i", "ts": 0, "name": "alloc", "args": {"bytes": 10}}
]
});
assert!(matches!(
classify_alloc_free_pairing(&body),
AllocFreePairingOutcome::AllocMissingAddr { index: 0 }
));
}
#[test]
fn monotonic_timestamps_ok_on_good_body() {
assert_eq!(
classify_monotonic_timestamps(&good_body()),
MonotonicTimestampsOutcome::Ok
);
}
#[test]
fn monotonic_timestamps_reports_violation() {
let body = json!({
"traceEvents": [
{"ph": "i", "ts": 100, "name": "alloc", "pid": 0, "tid": 1, "args": {"addr": "0xA"}},
{"ph": "i", "ts": 50, "name": "free", "pid": 0, "tid": 1, "args": {"addr": "0xA"}},
]
});
match classify_monotonic_timestamps(&body) {
MonotonicTimestampsOutcome::NonMonotonic {
index, pid, tid, ..
} => {
assert_eq!(index, 1);
assert_eq!(pid, 0);
assert_eq!(tid, 1);
}
other => panic!("expected NonMonotonic, got {other:?}"),
}
}
#[test]
fn monotonic_timestamps_allows_independent_streams_to_interleave() {
let body = json!({
"traceEvents": [
{"ph": "i", "ts": 100, "name": "alloc", "pid": 0, "tid": 1, "args": {"addr": "0xA"}},
{"ph": "i", "ts": 50, "name": "alloc", "pid": 0, "tid": 2, "args": {"addr": "0xB"}},
]
});
assert_eq!(
classify_monotonic_timestamps(&body),
MonotonicTimestampsOutcome::Ok
);
}
}