Brief one today. I got asked “does YJIT/ZJIT have support for [Linux] perf?”
The answer is yes, and it also works with samply (including on macOS!), because both understand the perf map interface.
This is the entirety of the implementation in ZJIT1:
fn register_with_perf(iseq_name: String, start_ptr: usize, code_size: usize) {
use std::io::Write;
let perf_map = format!("/tmp/perf-{}.map", std::process::id());
let Ok(file) = std::fs::OpenOptions::new().create(true).append(true).open(&perf_map) else {
debug!("Failed to open perf map file: {perf_map}");
return;
};
let mut file = std::io::BufWriter::new(file);
let Ok(_) = writeln!(file, "{start_ptr:x} {code_size:x} zjit::{iseq_name}") else {
debug!("Failed to write {iseq_name} to perf map file: {perf_map}");
return;
};
}
Whenever you generate a function, append a one-line entry consisting of
START SIZE symbolname
to /tmp/perf-{PID}.map. Per the Linux docs linked above,
START and SIZE are hex numbers without 0x.
symbolname is the rest of the line, so it could contain special characters.
You can now happily run perf record your_jit [...] or samply record your_jit
[...] and have JIT frames be named in the output. We hide this behind
the --zjit-perf flag to avoid file I/O overhead when we don’t need it.
Perf map is the older way to interact with perf: a newer, more complicated way
involves generating a “dump” file and then perf injecting it.
We actually use {:#x}, which I noticed today is wrong. {:#x} leaves
in the 0x, and it shouldn’t; instead use {:x}. ↩