@@ -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