Skip to content

Commit e03b80e

Browse files
committed
ZJIT: Add FXT trace disassembler and snapshot tests
1 parent e9e740e commit e03b80e

1 file changed

Lines changed: 277 additions & 2 deletions

File tree

zjit/src/stats.rs

Lines changed: 277 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,7 +1051,13 @@ impl PerfettoTracer {
10511051
pub fn new() -> Self {
10521052
let pid = std::process::id();
10531053
let path = format!("/tmp/perfetto-{pid}.fxt");
1054-
let file = std::fs::File::create(&path)
1054+
let tracer = Self::create(&path, pid);
1055+
eprintln!("ZJIT: writing trace exits to {path}");
1056+
tracer
1057+
}
1058+
1059+
fn create(path: &str, pid: u32) -> Self {
1060+
let file = std::fs::File::create(path)
10551061
.unwrap_or_else(|e| panic!("ZJIT: failed to create {path}: {e}"));
10561062
let mut tracer = PerfettoTracer {
10571063
writer: std::io::BufWriter::new(file),
@@ -1115,7 +1121,6 @@ impl PerfettoTracer {
11151121
let _ = tracer.writer.flush();
11161122
}
11171123

1118-
eprintln!("ZJIT: writing trace exits to {path}");
11191124
tracer
11201125
}
11211126

@@ -1256,6 +1261,171 @@ impl Drop for PerfettoTracer {
12561261
}
12571262
}
12581263

1264+
/// Decode an FXT binary trace into human-readable text.
1265+
/// Used by tests to verify trace output. Replaces raw nanosecond timestamps
1266+
/// with sequential labels (t0, t1, ...) so clock-dependent values become
1267+
/// deterministic in snapshots (similar to PtrPrintMap for pointers in HIR).
1268+
#[cfg(test)]
1269+
fn disassemble_fxt(data: &[u8]) -> String {
1270+
use std::fmt::Write;
1271+
let mut out = String::new();
1272+
let mut strings: std::collections::HashMap<u16, String> = std::collections::HashMap::new();
1273+
strings.insert(0, String::new());
1274+
let mut ts_map: std::collections::HashMap<u64, usize> = std::collections::HashMap::new();
1275+
let mut next_ts_id: usize = 0;
1276+
1277+
let read_word = |pos: usize| -> u64 {
1278+
u64::from_le_bytes(data[pos..pos+8].try_into().unwrap())
1279+
};
1280+
1281+
let resolve = |strings: &std::collections::HashMap<u16, String>, idx: u16| -> String {
1282+
strings.get(&idx).cloned().unwrap_or_else(|| format!("<str:{idx}>"))
1283+
};
1284+
1285+
// Map raw timestamps to deterministic labels (t0, t1, ...)
1286+
// like PtrPrintMap for pointers in HIR tests.
1287+
let mut map_ts = |raw: u64| -> String {
1288+
let id = match ts_map.entry(raw) {
1289+
std::collections::hash_map::Entry::Occupied(e) => *e.get(),
1290+
std::collections::hash_map::Entry::Vacant(e) => {
1291+
let id = next_ts_id;
1292+
next_ts_id += 1;
1293+
e.insert(id);
1294+
id
1295+
}
1296+
};
1297+
format!("t{id}")
1298+
};
1299+
1300+
fn parse_args(data: &[u8], pos: usize, n_args: u64, strings: &std::collections::HashMap<u16, String>) -> (Vec<String>, usize) {
1301+
let mut args = Vec::new();
1302+
let mut p = pos;
1303+
for _ in 0..n_args {
1304+
let arg_header = u64::from_le_bytes(data[p..p+8].try_into().unwrap());
1305+
let arg_type = arg_header & 0xF;
1306+
let arg_size = ((arg_header >> 4) & 0xFFF) as usize;
1307+
let arg_name_ref = ((arg_header >> 16) & 0xFFFF) as u16;
1308+
let arg_name = strings.get(&arg_name_ref).cloned().unwrap_or_else(|| format!("<str:{arg_name_ref}>"));
1309+
match arg_type {
1310+
4 => { // uint64
1311+
let value = u64::from_le_bytes(data[p+8..p+16].try_into().unwrap());
1312+
args.push(format!("{arg_name}={value}"));
1313+
p += arg_size * 8;
1314+
}
1315+
6 => { // string
1316+
let value_ref = ((arg_header >> 32) & 0xFFFF) as u16;
1317+
let value = strings.get(&value_ref).cloned().unwrap_or_else(|| format!("<str:{value_ref}>"));
1318+
args.push(format!("{arg_name}=\"{value}\""));
1319+
p += 8;
1320+
}
1321+
8 => { // koid
1322+
let value = u64::from_le_bytes(data[p+8..p+16].try_into().unwrap());
1323+
args.push(format!("{arg_name}={value}"));
1324+
p += arg_size * 8;
1325+
}
1326+
_ => {
1327+
args.push(format!("unknown_arg(type={arg_type})"));
1328+
p += arg_size * 8;
1329+
}
1330+
}
1331+
}
1332+
(args, p)
1333+
}
1334+
1335+
let mut pos = 0;
1336+
while pos + 8 <= data.len() {
1337+
let header = read_word(pos);
1338+
let record_type = header & 0xF;
1339+
let record_size = ((header >> 4) & 0xFFF) as usize;
1340+
1341+
match record_type {
1342+
0 => {
1343+
// Metadata record
1344+
let metadata_type = (header >> 16) & 0xFF;
1345+
if metadata_type == 4 {
1346+
let magic = (header >> 24) & 0xFFFFFFFF;
1347+
writeln!(out, "Magic(0x{magic:x})").unwrap();
1348+
} else {
1349+
writeln!(out, "Metadata(type={metadata_type})").unwrap();
1350+
}
1351+
pos += 8; // metadata records are always 1 word
1352+
}
1353+
1 => {
1354+
// Initialization record
1355+
let ticks = read_word(pos + 8);
1356+
writeln!(out, "Init(ticks_per_second={ticks})").unwrap();
1357+
pos += record_size * 8;
1358+
}
1359+
2 => {
1360+
// String record
1361+
let idx = ((header >> 16) & 0x7FFF) as u16;
1362+
let len = ((header >> 32) & 0x7FFF) as usize;
1363+
let str_start = pos + 8;
1364+
let s = std::str::from_utf8(&data[str_start..str_start+len])
1365+
.unwrap_or("<invalid utf8>").to_string();
1366+
strings.insert(idx, s.clone());
1367+
writeln!(out, "String({idx}, \"{s}\")").unwrap();
1368+
pos += record_size * 8;
1369+
}
1370+
3 => {
1371+
// Thread record
1372+
let thread_index = (header >> 16) & 0xFF;
1373+
let process_koid = read_word(pos + 8);
1374+
let thread_koid = read_word(pos + 16);
1375+
writeln!(out, "Thread(index={thread_index}, process_koid={process_koid}, thread_koid={thread_koid})").unwrap();
1376+
pos += record_size * 8;
1377+
}
1378+
4 => {
1379+
// Event record
1380+
let event_type = (header >> 16) & 0xF;
1381+
let n_args = (header >> 20) & 0xF;
1382+
let category_ref = ((header >> 32) & 0xFFFF) as u16;
1383+
let name_ref = ((header >> 48) & 0xFFFF) as u16;
1384+
let category = resolve(&strings, category_ref);
1385+
let name = resolve(&strings, name_ref);
1386+
let ts = map_ts(read_word(pos + 8));
1387+
1388+
let (args, _) = parse_args(data, pos + 16, n_args, &strings);
1389+
let args_str = if args.is_empty() { String::new() } else { format!(", {}", args.join(", ")) };
1390+
1391+
match event_type {
1392+
0 => writeln!(out, "Instant(category=\"{category}\", name=\"{name}\", ts={ts}{args_str})").unwrap(),
1393+
1 => {
1394+
// Counter: has counter_id after args
1395+
let counter_id = read_word(pos + 16 + n_args as usize * 8 + 8); // after uint64 arg
1396+
writeln!(out, "Counter(category=\"{category}\", name=\"{name}\", ts={ts}{args_str}, counter_id={counter_id})").unwrap();
1397+
}
1398+
2 => writeln!(out, "DurationBegin(category=\"{category}\", name=\"{name}\", ts={ts}{args_str})").unwrap(),
1399+
3 => writeln!(out, "DurationEnd(category=\"{category}\", name=\"{name}\", ts={ts}{args_str})").unwrap(),
1400+
_ => writeln!(out, "Event(type={event_type}, category=\"{category}\", name=\"{name}\", ts={ts}{args_str})").unwrap(),
1401+
}
1402+
pos += record_size * 8;
1403+
}
1404+
7 => {
1405+
// Kernel object record
1406+
let obj_type = (header >> 16) & 0xFF;
1407+
let name_ref = ((header >> 24) & 0xFFFF) as u16;
1408+
let n_args = (header >> 40) & 0xF;
1409+
let name = resolve(&strings, name_ref);
1410+
let koid = read_word(pos + 8);
1411+
1412+
let type_name = match obj_type { 1 => "process", 2 => "thread", _ => "unknown" };
1413+
let (args, _) = parse_args(data, pos + 16, n_args, &strings);
1414+
let args_str = if args.is_empty() { String::new() } else { format!(", {}", args.join(", ")) };
1415+
1416+
writeln!(out, "KernelObject(type={type_name}, name=\"{name}\", koid={koid}{args_str})").unwrap();
1417+
pos += record_size * 8;
1418+
}
1419+
_ => {
1420+
writeln!(out, "Unknown(type={record_type}, size={record_size})").unwrap();
1421+
if record_size == 0 { break; } // avoid infinite loop
1422+
pos += record_size * 8;
1423+
}
1424+
}
1425+
}
1426+
out
1427+
}
1428+
12591429
/// Primitive called in zjit.rb
12601430
///
12611431
/// Check if trace_exits generation is enabled.
@@ -1269,3 +1439,108 @@ pub extern "C" fn rb_zjit_trace_exit_locations_enabled_p(_ec: EcPtr, _ruby_self:
12691439
}
12701440
}
12711441

1442+
#[cfg(test)]
1443+
mod fxt_tests {
1444+
use super::*;
1445+
use insta::assert_snapshot;
1446+
1447+
fn create_test_tracer() -> (PerfettoTracer, std::path::PathBuf) {
1448+
let dir = std::env::temp_dir();
1449+
let path = dir.join(format!("zjit_fxt_test_{}.fxt", std::process::id()));
1450+
let tracer = PerfettoTracer::create(path.to_str().unwrap(), 42);
1451+
(tracer, path)
1452+
}
1453+
1454+
fn read_and_disassemble(tracer: PerfettoTracer, path: &std::path::Path) -> String {
1455+
drop(tracer); // flush via Drop
1456+
let data = std::fs::read(path).unwrap();
1457+
let _ = std::fs::remove_file(path);
1458+
disassemble_fxt(&data)
1459+
}
1460+
1461+
/// Strip the pre-interned strings from header output to keep snapshots focused
1462+
fn strip_header(output: &str) -> String {
1463+
output.lines()
1464+
.skip_while(|l| !l.starts_with("Duration") && !l.starts_with("Instant") && !l.starts_with("Counter"))
1465+
.collect::<Vec<_>>()
1466+
.join("\n")
1467+
}
1468+
1469+
#[test]
1470+
fn test_fxt_header() {
1471+
let (tracer, path) = create_test_tracer();
1472+
assert_snapshot!(read_and_disassemble(tracer, &path), @r#"
1473+
Magic(0x16547846)
1474+
Init(ticks_per_second=1000000000)
1475+
Thread(index=1, process_koid=42, thread_koid=1)
1476+
String(1, "ruby")
1477+
KernelObject(type=process, name="ruby", koid=42)
1478+
String(2, "main")
1479+
String(3, "process")
1480+
KernelObject(type=thread, name="main", koid=1, process=42)
1481+
String(4, "side_exit")
1482+
String(5, "compile")
1483+
String(6, "invalidation")
1484+
String(7, "0")
1485+
String(8, "1")
1486+
String(9, "2")
1487+
String(10, "3")
1488+
String(11, "4")
1489+
String(12, "5")
1490+
String(13, "6")
1491+
String(14, "7")
1492+
String(15, "8")
1493+
String(16, "9")
1494+
String(17, "10")
1495+
String(18, "11")
1496+
String(19, "12")
1497+
String(20, "13")
1498+
String(21, "14")
1499+
"#);
1500+
}
1501+
1502+
#[test]
1503+
fn test_fxt_duration_events() {
1504+
let (mut tracer, path) = create_test_tracer();
1505+
tracer.write_duration_begin("compile", "test_fn", 1000, &[]);
1506+
tracer.write_duration_end("compile", "test_fn", 2000);
1507+
assert_snapshot!(strip_header(&read_and_disassemble(tracer, &path)), @r#"
1508+
DurationBegin(category="compile", name="test_fn", ts=t0)
1509+
DurationEnd(category="compile", name="test_fn", ts=t1)
1510+
"#);
1511+
}
1512+
1513+
#[test]
1514+
fn test_fxt_duration_with_frames() {
1515+
let (mut tracer, path) = create_test_tracer();
1516+
tracer.write_duration_begin("invalidation", "bop", 500, &["frame_a".into(), "frame_b".into()]);
1517+
tracer.write_duration_end("invalidation", "bop", 600);
1518+
assert_snapshot!(strip_header(&read_and_disassemble(tracer, &path)), @r#"
1519+
DurationBegin(category="invalidation", name="bop", ts=t0, 0="frame_a", 1="frame_b")
1520+
DurationEnd(category="invalidation", name="bop", ts=t1)
1521+
"#);
1522+
}
1523+
1524+
#[test]
1525+
fn test_fxt_instant_event() {
1526+
let (mut tracer, path) = create_test_tracer();
1527+
tracer.write_event("side_exit", "guard_failed", &["caller_fn".into()]);
1528+
assert_snapshot!(strip_header(&read_and_disassemble(tracer, &path)), @r#"Instant(category="side_exit", name="guard_failed", ts=t0, 0="caller_fn")"#);
1529+
}
1530+
1531+
#[test]
1532+
fn test_fxt_string_dedup() {
1533+
let (mut tracer, path) = create_test_tracer();
1534+
tracer.write_duration_begin("compile", "same_fn", 100, &[]);
1535+
tracer.write_duration_end("compile", "same_fn", 200);
1536+
tracer.write_duration_begin("compile", "same_fn", 300, &[]);
1537+
tracer.write_duration_end("compile", "same_fn", 400);
1538+
assert_snapshot!(strip_header(&read_and_disassemble(tracer, &path)), @r#"
1539+
DurationBegin(category="compile", name="same_fn", ts=t0)
1540+
DurationEnd(category="compile", name="same_fn", ts=t1)
1541+
DurationBegin(category="compile", name="same_fn", ts=t2)
1542+
DurationEnd(category="compile", name="same_fn", ts=t3)
1543+
"#);
1544+
}
1545+
}
1546+

0 commit comments

Comments
 (0)