wasmtime_jit_debug/
perf_jitdump.rs

1//! Support for jitdump files which can be used by perf for profiling jitted code.
2//! Spec definitions for the output format is as described here:
3//! <https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/tools/perf/Documentation/jitdump-specification.txt>
4//!
5//! Usage Example:
6//!     Record
7//!         sudo perf record -k 1 -e instructions:u target/debug/wasmtime -g --profile=jitdump test.wasm
8//!     Combine
9//!         sudo perf inject -v -j -i perf.data -o perf.jit.data
10//!     Report
11//!         sudo perf report -i perf.jit.data -F+period,srcline
12
13use std::fmt::Debug;
14use std::fs::{File, OpenOptions};
15use std::io;
16use std::io::Write;
17use std::path::Path;
18use std::ptr;
19use std::{mem, process};
20
21/// Defines jitdump record types
22#[repr(u32)]
23pub enum RecordId {
24    /// Value 0: JIT_CODE_LOAD: record describing a jitted function
25    JitCodeLoad = 0,
26    /// Value 1: JIT_CODE_MOVE: record describing an already jitted function which is moved
27    _JitCodeMove = 1,
28    /// Value 2: JIT_CODE_DEBUG_INFO: record describing the debug information for a jitted function
29    JitCodeDebugInfo = 2,
30    /// Value 3: JIT_CODE_CLOSE: record marking the end of the jit runtime (optional)
31    _JitCodeClose = 3,
32    /// Value 4: JIT_CODE_UNWINDING_INFO: record describing a function unwinding information
33    _JitCodeUnwindingInfo = 4,
34}
35
36/// Each record starts with this fixed size record header which describes the record that follows
37#[derive(Debug, Default, Clone, Copy)]
38#[repr(C)]
39pub struct RecordHeader {
40    /// uint32_t id: a value identifying the record type (see below)
41    pub id: u32,
42    /// uint32_t total_size: the size in bytes of the record including the header.
43    pub record_size: u32,
44    /// uint64_t timestamp: a timestamp of when the record was created.
45    pub timestamp: u64,
46}
47
48unsafe impl object::Pod for RecordHeader {}
49
50/// The CodeLoadRecord is used for describing jitted functions
51#[derive(Debug, Default, Clone, Copy)]
52#[repr(C)]
53pub struct CodeLoadRecord {
54    /// Fixed sized header that describes this record
55    pub header: RecordHeader,
56    /// `uint32_t pid`: OS process id of the runtime generating the jitted code
57    pub pid: u32,
58    /// `uint32_t tid`: OS thread identification of the runtime thread generating the jitted code
59    pub tid: u32,
60    /// `uint64_t vma`: virtual address of jitted code start
61    pub virtual_address: u64,
62    /// `uint64_t code_addr`: code start address for the jitted code. By default vma = code_addr
63    pub address: u64,
64    /// `uint64_t code_size`: size in bytes of the generated jitted code
65    pub size: u64,
66    /// `uint64_t code_index`: unique identifier for the jitted code (see below)
67    pub index: u64,
68}
69
70unsafe impl object::Pod for CodeLoadRecord {}
71
72/// Describes source line information for a jitted function
73#[derive(Debug, Default)]
74#[repr(C)]
75pub struct DebugEntry {
76    /// `uint64_t code_addr`: address of function for which the debug information is generated
77    pub address: u64,
78    /// `uint32_t line`: source file line number (starting at 1)
79    pub line: u32,
80    /// `uint32_t discrim`: column discriminator, 0 is default
81    pub discriminator: u32,
82    /// `char name[n]`: source file name in ASCII, including null termination
83    pub filename: String,
84}
85
86/// Describes debug information for a jitted function. An array of debug entries are
87/// appended to this record during writing. Note, this record must precede the code
88/// load record that describes the same jitted function.
89#[derive(Debug, Default, Clone, Copy)]
90#[repr(C)]
91pub struct DebugInfoRecord {
92    /// Fixed sized header that describes this record
93    pub header: RecordHeader,
94    /// `uint64_t code_addr`: address of function for which the debug information is generated
95    pub address: u64,
96    /// `uint64_t nr_entry`: number of debug entries for the function appended to this record
97    pub count: u64,
98}
99
100unsafe impl object::Pod for DebugInfoRecord {}
101
102/// Fixed-sized header for each jitdump file
103#[derive(Debug, Default, Clone, Copy)]
104#[repr(C)]
105pub struct FileHeader {
106    /// `uint32_t magic`: a magic number tagging the file type. The value is 4-byte long and represents the
107    /// string "JiTD" in ASCII form. It is 0x4A695444 or 0x4454694a depending on the endianness. The field can
108    /// be used to detect the endianness of the file
109    pub magic: u32,
110    /// `uint32_t version`: a 4-byte value representing the format version. It is currently set to 2
111    pub version: u32,
112    /// `uint32_t total_size`: size in bytes of file header
113    pub size: u32,
114    /// `uint32_t elf_mach`: ELF architecture encoding (ELF e_machine value as specified in /usr/include/elf.h)
115    pub e_machine: u32,
116    /// `uint32_t pad1`: padding. Reserved for future use
117    pub pad1: u32,
118    /// `uint32_t pid`: JIT runtime process identification (OS specific)
119    pub pid: u32,
120    /// `uint64_t timestamp`: timestamp of when the file was created
121    pub timestamp: u64,
122    /// `uint64_t flags`: a bitmask of flags
123    pub flags: u64,
124}
125
126unsafe impl object::Pod for FileHeader {}
127
128/// Interface for driving the creation of jitdump files
129pub struct JitDumpFile {
130    /// File instance for the jit dump file
131    jitdump_file: File,
132
133    map_addr: usize,
134
135    /// Unique identifier for jitted code
136    code_index: u64,
137
138    e_machine: u32,
139}
140
141impl JitDumpFile {
142    /// Initialize a JitDumpAgent and write out the header
143    pub fn new(filename: impl AsRef<Path>, e_machine: u32) -> io::Result<Self> {
144        let jitdump_file = OpenOptions::new()
145            .read(true)
146            .write(true)
147            .create(true)
148            .truncate(true)
149            .open(filename.as_ref())?;
150
151        // After we make our `*.dump` file we execute an `mmap` syscall,
152        // specifically with executable permissions, to map it into our address
153        // space. This is required so `perf inject` will work later. The `perf
154        // inject` command will see that an mmap syscall happened, and it'll see
155        // the filename we mapped, and that'll trigger it to actually read and
156        // parse the file.
157        //
158        // To match what some perf examples are doing we keep this `mmap` alive
159        // until this agent goes away.
160        let map_addr = unsafe {
161            let ptr = rustix::mm::mmap(
162                ptr::null_mut(),
163                rustix::param::page_size(),
164                rustix::mm::ProtFlags::EXEC | rustix::mm::ProtFlags::READ,
165                rustix::mm::MapFlags::PRIVATE,
166                &jitdump_file,
167                0,
168            )?;
169            ptr as usize
170        };
171        let mut state = JitDumpFile {
172            jitdump_file,
173            map_addr,
174            code_index: 0,
175            e_machine,
176        };
177        state.write_file_header()?;
178        Ok(state)
179    }
180}
181
182impl JitDumpFile {
183    /// Returns timestamp from a single source
184    pub fn get_time_stamp(&self) -> u64 {
185        // We need to use `CLOCK_MONOTONIC` on Linux which is what `Instant`
186        // conveniently also uses, but `Instant` doesn't allow us to get access
187        // to nanoseconds as an internal detail, so we calculate the nanoseconds
188        // ourselves here.
189        let ts = rustix::time::clock_gettime(rustix::time::ClockId::Monotonic);
190        // TODO: What does it mean for either sec or nsec to be negative?
191        (ts.tv_sec * 1_000_000_000 + ts.tv_nsec) as u64
192    }
193
194    /// Returns the next code index
195    pub fn next_code_index(&mut self) -> u64 {
196        let code_index = self.code_index;
197        self.code_index += 1;
198        code_index
199    }
200
201    pub fn write_file_header(&mut self) -> io::Result<()> {
202        let header = FileHeader {
203            timestamp: self.get_time_stamp(),
204            e_machine: self.e_machine,
205            magic: 0x4A695444,
206            version: 1,
207            size: mem::size_of::<FileHeader>() as u32,
208            pad1: 0,
209            pid: process::id(),
210            flags: 0,
211        };
212
213        self.jitdump_file.write_all(object::bytes_of(&header))?;
214        Ok(())
215    }
216
217    pub fn write_code_load_record(
218        &mut self,
219        record_name: &str,
220        cl_record: CodeLoadRecord,
221        code_buffer: &[u8],
222    ) -> io::Result<()> {
223        self.jitdump_file.write_all(object::bytes_of(&cl_record))?;
224        self.jitdump_file.write_all(record_name.as_bytes())?;
225        self.jitdump_file.write_all(b"\0")?;
226        self.jitdump_file.write_all(code_buffer)?;
227        Ok(())
228    }
229
230    /// Write DebugInfoRecord to open jit dump file.
231    /// Must be written before the corresponding CodeLoadRecord.
232    pub fn write_debug_info_record(&mut self, dir_record: DebugInfoRecord) -> io::Result<()> {
233        self.jitdump_file.write_all(object::bytes_of(&dir_record))?;
234        Ok(())
235    }
236
237    /// Write DebugInfoRecord to open jit dump file.
238    /// Must be written before the corresponding CodeLoadRecord.
239    pub fn write_debug_info_entries(&mut self, die_entries: Vec<DebugEntry>) -> io::Result<()> {
240        for entry in die_entries.iter() {
241            self.jitdump_file
242                .write_all(object::bytes_of(&entry.address))?;
243            self.jitdump_file.write_all(object::bytes_of(&entry.line))?;
244            self.jitdump_file
245                .write_all(object::bytes_of(&entry.discriminator))?;
246            self.jitdump_file.write_all(entry.filename.as_bytes())?;
247            self.jitdump_file.write_all(b"\0")?;
248        }
249        Ok(())
250    }
251
252    pub fn dump_code_load_record(
253        &mut self,
254        method_name: &str,
255        code: &[u8],
256        timestamp: u64,
257        pid: u32,
258        tid: u32,
259    ) -> io::Result<()> {
260        let name_len = method_name.len() + 1;
261        let size_limit = mem::size_of::<CodeLoadRecord>();
262
263        let rh = RecordHeader {
264            id: RecordId::JitCodeLoad as u32,
265            record_size: size_limit as u32 + name_len as u32 + code.len() as u32,
266            timestamp,
267        };
268
269        let clr = CodeLoadRecord {
270            header: rh,
271            pid,
272            tid,
273            virtual_address: code.as_ptr() as u64,
274            address: code.as_ptr() as u64,
275            size: code.len() as u64,
276            index: self.next_code_index(),
277        };
278
279        self.write_code_load_record(method_name, clr, code)
280    }
281}
282
283impl Drop for JitDumpFile {
284    fn drop(&mut self) {
285        unsafe {
286            rustix::mm::munmap(self.map_addr as *mut _, rustix::param::page_size()).unwrap();
287        }
288    }
289}