How to annotate JITed code for perf/samply

December 18, 2025

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.

There is also the JIT dump interface

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.

  1. We actually use {:#x}, which I noticed today is wrong. {:#x} leaves in the 0x, and it shouldn’t; instead use {:x}