summaryrefslogtreecommitdiff
path: root/zjit/src/stats.rs
diff options
context:
space:
mode:
Diffstat (limited to 'zjit/src/stats.rs')
-rw-r--r--zjit/src/stats.rs205
1 files changed, 169 insertions, 36 deletions
diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs
index 68379e90cb..28bd623893 100644
--- a/zjit/src/stats.rs
+++ b/zjit/src/stats.rs
@@ -573,7 +573,11 @@ pub fn side_exit_counter(reason: crate::hir::SideExitReason) -> Counter {
UnhandledCallType(Splat) => exit_unhandled_splat,
UnhandledCallType(Kwarg) => exit_unhandled_kwarg,
UnknownSpecialVariable(_) => exit_unknown_special_variable,
- UnhandledHIRInsn(_) => exit_unhandled_hir_insn,
+ UnhandledHIRArrayMax => exit_unhandled_hir_insn,
+ UnhandledHIRFixnumDiv => exit_unhandled_hir_insn,
+ UnhandledHIRThrow => exit_unhandled_hir_insn,
+ UnhandledHIRInvokeBuiltin => exit_unhandled_hir_insn,
+ UnhandledHIRUnknown(_) => exit_unhandled_hir_insn,
UnhandledYARVInsn(_) => exit_unhandled_yarv_insn,
UnhandledBlockArg => exit_unhandled_block_arg,
FixnumAddOverflow => exit_fixnum_add_overflow,
@@ -975,15 +979,170 @@ pub fn zjit_alloc_bytes() -> usize {
jit::GLOBAL_ALLOCATOR.alloc_size.load(Ordering::SeqCst)
}
-/// Struct of arrays for --zjit-trace-exits.
-#[derive(Default)]
-pub struct SideExitLocations {
- /// Control frames of method entries.
- pub raw_samples: Vec<VALUE>,
- /// Line numbers of the iseq caller.
- pub line_samples: Vec<i32>,
- /// Skipped samples
- pub skipped_samples: usize
+/// Fuchsia Trace Format (FXT) binary writer for --zjit-trace-exits.
+/// Produces .fxt files that can be opened directly in Perfetto UI.
+/// Uses the string table for deduplication of repeated reason/frame strings.
+/// See: <https://fuchsia.dev/fuchsia-src/reference/tracing/trace-format>
+pub struct PerfettoTracer {
+ writer: std::io::BufWriter<std::fs::File>,
+ start_time: std::time::Instant,
+ event_count: usize,
+ pub skipped_samples: usize,
+ /// String table: string content -> interned index (1..32767)
+ string_table: std::collections::HashMap<String, u16>,
+ next_string_index: u16,
+ pid: u32,
+}
+
+impl PerfettoTracer {
+ /// Write a single 64-bit little-endian word.
+ fn write_word(&mut self, val: u64) {
+ use std::io::Write;
+ let _ = self.writer.write_all(&val.to_le_bytes());
+ }
+
+ /// Write bytes padded to 8-byte alignment.
+ fn write_padded_bytes(&mut self, bytes: &[u8]) {
+ use std::io::Write;
+ let _ = self.writer.write_all(bytes);
+ let remainder = bytes.len() % 8;
+ if remainder != 0 {
+ let _ = self.writer.write_all(&[0u8; 7][..8 - remainder]);
+ }
+ }
+
+ /// Number of 8-byte words needed for `len` bytes (rounded up).
+ fn word_count(len: usize) -> u64 {
+ ((len + 7) / 8) as u64
+ }
+
+ pub fn new() -> Self {
+ let pid = std::process::id();
+ let path = format!("/tmp/perfetto-{pid}.fxt");
+ let file = std::fs::File::create(&path)
+ .unwrap_or_else(|e| panic!("ZJIT: failed to create {path}: {e}"));
+ let mut tracer = PerfettoTracer {
+ writer: std::io::BufWriter::new(file),
+ start_time: std::time::Instant::now(),
+ event_count: 0,
+ skipped_samples: 0,
+ string_table: std::collections::HashMap::new(),
+ next_string_index: 1, // index 0 = empty string
+ pid,
+ };
+
+ // Magic number record: metadata type=4 (trace info), trace info type=0,
+ // magic=0x16547846 at bits [24..55]
+ tracer.write_word((1u64 << 4) | (4u64 << 16) | (0x16547846u64 << 24));
+
+ // Initialization record: 1 tick = 1 nanosecond
+ tracer.write_word(1u64 | (2u64 << 4));
+ tracer.write_word(1_000_000_000u64);
+
+ // Register thread at index 1: (process_koid=pid, thread_koid=1)
+ tracer.write_word(3u64 | (3u64 << 4) | (1u64 << 16));
+ tracer.write_word(pid as u64);
+ tracer.write_word(1u64);
+
+ // Pre-intern common strings
+ tracer.intern_string("side_exit");
+ // Pre-intern argument names "0".."14" for per-frame arguments
+ for i in 0..15u32 {
+ tracer.intern_string(&i.to_string());
+ }
+
+ // Flush header immediately so something is written even if process exits abruptly
+ {
+ use std::io::Write;
+ let _ = tracer.writer.flush();
+ }
+
+ eprintln!("ZJIT: writing trace exits to {path}");
+ tracer
+ }
+
+ /// Intern a string into the string table, writing a string record if new.
+ /// Returns the string table index (1..32767). Returns 0 for empty strings
+ /// or if the table is full.
+ fn intern_string(&mut self, s: &str) -> u16 {
+ if s.is_empty() {
+ return 0;
+ }
+ if let Some(&idx) = self.string_table.get(s) {
+ return idx;
+ }
+ if self.next_string_index >= 0x8000 {
+ return 0; // table full
+ }
+
+ let idx = self.next_string_index;
+ let bytes = s.as_bytes();
+ let len = bytes.len().min(0x7FFF); // 15-bit max length
+ let record_words = 1 + Self::word_count(len);
+
+ // String record: type=2, index in [16..30], length in [32..46]
+ let header: u64 = 2u64
+ | (record_words << 4)
+ | ((idx as u64) << 16)
+ | ((len as u64) << 32);
+ self.write_word(header);
+ self.write_padded_bytes(&bytes[..len]);
+
+ self.string_table.insert(s.to_string(), idx);
+ self.next_string_index += 1;
+ idx
+ }
+
+ pub fn write_event(&mut self, reason: &str, frames: &[String]) {
+ let ts_nanos = self.start_time.elapsed().as_nanos() as u64;
+
+ // Intern event metadata strings (may emit string records first)
+ let category_ref = self.intern_string("side_exit");
+ let name_ref = self.intern_string(reason);
+
+ // Intern each frame label and collect refs (max 15 due to 4-bit n_args)
+ let n_args = frames.len().min(15) as u64;
+ let mut frame_refs: Vec<(u16, u16)> = Vec::with_capacity(n_args as usize);
+ for (i, frame) in frames.iter().take(15).enumerate() {
+ let name_ref = self.intern_string(&i.to_string());
+ let value_ref = self.intern_string(frame);
+ frame_refs.push((name_ref, value_ref));
+ }
+
+ // Each fully-interned string argument is exactly 1 word
+ let event_words = 2 + n_args;
+ let header: u64 = 4u64
+ | (event_words << 4)
+ | (n_args << 20) // argument count
+ | (1u64 << 24) // thread_ref = 1
+ | ((category_ref as u64) << 32)
+ | ((name_ref as u64) << 48);
+ self.write_word(header);
+ self.write_word(ts_nanos);
+
+ // One 1-word string argument per frame: type=6, size=1, indexed name, indexed value
+ for (name_ref, value_ref) in frame_refs {
+ let arg_header: u64 = 6u64
+ | (1u64 << 4)
+ | ((name_ref as u64) << 16)
+ | ((value_ref as u64) << 32);
+ self.write_word(arg_header);
+ }
+
+ self.event_count += 1;
+
+ // Flush to ensure data reaches disk. Static globals may not be
+ // dropped on process exit, so we can't rely on Drop for flushing.
+ use std::io::Write;
+ let _ = self.writer.flush();
+ }
+}
+
+impl Drop for PerfettoTracer {
+ fn drop(&mut self) {
+ use std::io::Write;
+ let _ = self.writer.flush();
+ }
}
/// Primitive called in zjit.rb
@@ -999,29 +1158,3 @@ pub extern "C" fn rb_zjit_trace_exit_locations_enabled_p(_ec: EcPtr, _ruby_self:
}
}
-/// Call the C function to parse the raw_samples and line_samples
-/// into raw, lines, and frames hash for RubyVM::YJIT.exit_locations.
-#[unsafe(no_mangle)]
-pub extern "C" fn rb_zjit_get_exit_locations(_ec: EcPtr, _ruby_self: VALUE) -> VALUE {
- if !zjit_enabled_p() || get_option!(trace_side_exits).is_none() {
- return Qnil;
- }
-
- // Can safely unwrap since `trace_side_exits` must be true at this point
- let zjit_raw_samples = ZJITState::get_raw_samples().unwrap();
- let zjit_line_samples = ZJITState::get_line_samples().unwrap();
-
- assert_eq!(zjit_raw_samples.len(), zjit_line_samples.len());
-
- // zjit_raw_samples and zjit_line_samples are the same length so
- // pass only one of the lengths in the C function.
- let samples_len = zjit_raw_samples.len() as i32;
-
- unsafe {
- rb_zjit_exit_locations_dict(
- zjit_raw_samples.as_mut_ptr(),
- zjit_line_samples.as_mut_ptr(),
- samples_len
- )
- }
-}