Skip to content

Commit e569d34

Browse files
tekknolagiclaude
andcommitted
ZJIT: Add jitdump emission with HIR source-level debug info
When --zjit-perf is enabled, emit a jitdump file alongside the existing perf map. The jitdump includes JIT_CODE_LOAD records with machine code and JIT_CODE_DEBUG_INFO records that map code offsets to line numbers in a synthetic HIR source file (/tmp/zjit-hir-{pid}.hir). This allows profilers like samply to show HIR instructions as "source code" in the Firefox Profiler, enabling per-HIR-instruction sample attribution. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 231bd61 commit e569d34

3 files changed

Lines changed: 384 additions & 3 deletions

File tree

zjit/src/codegen.rs

Lines changed: 170 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::cell::{Cell, RefCell};
66
use std::rc::Rc;
77
use std::ffi::{c_int, c_long, c_void};
88
use std::slice;
9+
use std::sync::OnceLock;
910

1011
use crate::backend::current::ALLOC_REGS;
1112
use crate::invariants::{
@@ -25,6 +26,30 @@ use crate::hir::{Const, FrameState, Function, Insn, InsnId, SendFallbackReason};
2526
use crate::hir_type::{types, Type};
2627
use crate::options::get_option;
2728
use crate::cast::IntoUsize;
29+
use crate::jitdump::{JitdumpWriter, DebugEntry};
30+
31+
/// Global jitdump writer, initialized on first use when --zjit-perf is set.
32+
static JITDUMP: OnceLock<JitdumpWriter> = OnceLock::new();
33+
34+
/// Path to the single HIR source file for all methods.
35+
/// Written to /tmp/zjit-hir-{pid}.hir
36+
static HIR_FILE_PATH: OnceLock<String> = OnceLock::new();
37+
38+
fn get_jitdump() -> Option<&'static JitdumpWriter> {
39+
if !get_option!(perf) {
40+
return None;
41+
}
42+
Some(JITDUMP.get_or_init(|| {
43+
JitdumpWriter::open().expect("Failed to open jitdump file")
44+
}))
45+
}
46+
47+
fn get_hir_file_path() -> &'static str {
48+
HIR_FILE_PATH.get_or_init(|| {
49+
let pid = std::process::id();
50+
format!("/tmp/zjit-hir-{pid}.hir")
51+
})
52+
}
2853

2954
/// At the moment, we support recompiling each ISEQ only once.
3055
pub const MAX_ISEQ_VERSIONS: usize = 2;
@@ -163,6 +188,134 @@ pub fn gen_iseq_call(cb: &mut CodeBlock, iseq_call: &IseqCallRef) -> Result<(),
163188
Ok(())
164189
}
165190

191+
/// Emit jitdump records with HIR source-level debug info for a compiled function.
192+
fn emit_jitdump_for_function(
193+
cb: &CodeBlock,
194+
function: &Function,
195+
iseq_name: &str,
196+
start_ptr: CodePtr,
197+
code_size: usize,
198+
pos_markers: &[(CodePtr, InsnId)],
199+
) {
200+
let Some(jitdump) = get_jitdump() else { return };
201+
202+
let start_addr = start_ptr.raw_addr(cb) as u64;
203+
204+
// Read the generated code bytes for the CODE_LOAD record
205+
let code_bytes = unsafe { std::slice::from_raw_parts(start_ptr.raw_ptr(cb), code_size) };
206+
let func_name = format!("zjit::{iseq_name}");
207+
if let Err(e) = jitdump.write_code_load(&func_name, start_addr, &code_bytes) {
208+
debug!("Failed to write jitdump code load: {e}");
209+
return;
210+
}
211+
212+
// Build HIR text and line mapping for this function.
213+
// We write HIR as text to a single shared file, appending each method.
214+
// The line numbers in the debug entries reference lines in this file.
215+
let hir_file_path = get_hir_file_path();
216+
let (hir_text, insn_id_to_line) = format_hir_for_jitdump(function);
217+
218+
// Append HIR text to the shared file and get the starting line offset
219+
let line_offset = append_hir_to_file(hir_file_path, &hir_text);
220+
221+
// Build debug entries: map each pos_marker's code offset to the HIR line
222+
let mut debug_entries: Vec<DebugEntry> = Vec::new();
223+
for &(code_ptr, insn_id) in pos_markers {
224+
if let Some(&line) = insn_id_to_line.get(&insn_id) {
225+
debug_entries.push(DebugEntry {
226+
code_addr: code_ptr.raw_addr(cb) as u64 - start_addr,
227+
line: line_offset + line,
228+
filename: hir_file_path,
229+
});
230+
}
231+
}
232+
233+
if let Err(e) = jitdump.write_debug_info(start_addr, &debug_entries) {
234+
debug!("Failed to write jitdump debug info: {e}");
235+
}
236+
}
237+
238+
/// Format a function's HIR as text for the jitdump source file.
239+
/// Returns (text, map from InsnId to 1-based line number within the text).
240+
fn format_hir_for_jitdump(function: &Function) -> (String, std::collections::HashMap<InsnId, u32>) {
241+
use std::fmt::Write;
242+
use crate::hir::PtrPrintMap;
243+
let mut text = String::new();
244+
let mut insn_id_to_line: std::collections::HashMap<InsnId, u32> = std::collections::HashMap::new();
245+
let mut line: u32 = 1;
246+
247+
let iseq = function.iseq();
248+
let iseq_name = if iseq.is_null() {
249+
String::from("<manual>")
250+
} else {
251+
iseq_get_location(iseq, 0)
252+
};
253+
writeln!(text, "fn {iseq_name}:").unwrap();
254+
line += 1;
255+
256+
let ptr_map = PtrPrintMap::identity();
257+
258+
for block_id in function.rpo() {
259+
let block = function.block(block_id);
260+
write!(text, "{block_id}(").unwrap();
261+
let mut sep = "";
262+
for &param in block.params() {
263+
let insn_type = function.type_of(param);
264+
if insn_type.is_subtype(types::Empty) {
265+
write!(text, "{sep}{param}").unwrap();
266+
} else {
267+
write!(text, "{sep}{param}:{}", insn_type.print(&ptr_map)).unwrap();
268+
}
269+
sep = ", ";
270+
}
271+
writeln!(text, "):").unwrap();
272+
line += 1;
273+
274+
for &insn_id in block.insns() {
275+
let insn = function.find(insn_id);
276+
if matches!(insn, Insn::Snapshot { .. }) {
277+
continue;
278+
}
279+
280+
insn_id_to_line.insert(insn_id, line);
281+
282+
write!(text, " ").unwrap();
283+
if insn.has_output() {
284+
let insn_type = function.type_of(insn_id);
285+
if insn_type.is_subtype(types::Empty) {
286+
write!(text, "{insn_id} = ").unwrap();
287+
} else {
288+
write!(text, "{insn_id}:{} = ", insn_type.print(&ptr_map)).unwrap();
289+
}
290+
}
291+
writeln!(text, "{}", insn.print(&ptr_map, Some(iseq))).unwrap();
292+
line += 1;
293+
}
294+
}
295+
296+
(text, insn_id_to_line)
297+
}
298+
299+
/// Append HIR text to the shared file and return the 1-based line offset
300+
/// (i.e., how many lines were in the file before this append).
301+
fn append_hir_to_file(path: &str, text: &str) -> u32 {
302+
use std::io::{Write, BufRead};
303+
304+
// Count existing lines
305+
let existing_lines = if let Ok(file) = std::fs::File::open(path) {
306+
std::io::BufReader::new(file).lines().count() as u32
307+
} else {
308+
0
309+
};
310+
311+
// Append the new text
312+
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
313+
let _ = file.write_all(text.as_bytes());
314+
}
315+
316+
existing_lines
317+
}
318+
166319
/// Write an entry to the perf map in /tmp
167320
fn register_with_perf(iseq_name: String, start_ptr: usize, code_size: usize) {
168321
use std::io::Write;
@@ -272,6 +425,9 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, version: IseqVersionRef, func
272425
let mut jit = JITState::new(iseq, version, function.num_insns(), function.num_blocks());
273426
let mut asm = Assembler::new_with_stack_slots(num_spilled_params);
274427

428+
// Collect (CodePtr, InsnId) pairs for jitdump debug info
429+
let hir_pos_markers: Rc<RefCell<Vec<(CodePtr, InsnId)>>> = Rc::new(RefCell::new(Vec::new()));
430+
275431
// Mapping from HIR block IDs to LIR block IDs.
276432
// This is is a one-to-one mapping from HIR to LIR blocks used for finding
277433
// jump targets in LIR (LIR should always jump to the head of an HIR block)
@@ -330,6 +486,15 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, version: IseqVersionRef, func
330486
// Compile all instructions
331487
for &insn_id in block.insns() {
332488
let insn = function.find(insn_id);
489+
490+
// Record code position for each non-snapshot HIR instruction (for jitdump)
491+
if get_option!(perf) && !matches!(insn, Insn::Snapshot { .. }) {
492+
let markers = Rc::clone(&hir_pos_markers);
493+
asm.pos_marker(move |code_ptr, _cb| {
494+
markers.borrow_mut().push((code_ptr, insn_id));
495+
});
496+
}
497+
333498
match insn {
334499
Insn::IfFalse { val, target } => {
335500
let val_opnd = jit.get_opnd(val);
@@ -411,15 +576,17 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, version: IseqVersionRef, func
411576
// Generate code if everything can be compiled
412577
let result = asm.compile(cb);
413578
if let Ok((start_ptr, _)) = result {
579+
let iseq_name = iseq_get_location(iseq, 0);
414580
if get_option!(perf) {
415581
let start_usize = start_ptr.raw_addr(cb);
416582
let end_usize = cb.get_write_ptr().raw_addr(cb);
417583
let code_size = end_usize - start_usize;
418-
let iseq_name = iseq_get_location(iseq, 0);
419-
register_with_perf(iseq_name, start_usize, code_size);
584+
register_with_perf(iseq_name.clone(), start_usize, code_size);
585+
586+
// Emit jitdump records with HIR debug info
587+
emit_jitdump_for_function(cb, function, &iseq_name, start_ptr, code_size, &hir_pos_markers.borrow());
420588
}
421589
if ZJITState::should_log_compiled_iseqs() {
422-
let iseq_name = iseq_get_location(iseq, 0);
423590
ZJITState::log_compile(iseq_name);
424591
}
425592
}

0 commit comments

Comments
 (0)