wasmtime_internal_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    map_len: usize,
135
136    /// Unique identifier for jitted code
137    code_index: u64,
138
139    e_machine: u32,
140}
141
142impl JitDumpFile {
143    /// Initialize a JitDumpAgent and write out the header
144    pub fn new(filename: impl AsRef<Path>, e_machine: u32) -> io::Result<Self> {
145        let jitdump_file = OpenOptions::new()
146            .read(true)
147            .write(true)
148            .create(true)
149            .truncate(true)
150            .open(filename.as_ref())?;
151
152        // After we make our `*.dump` file we execute an `mmap` syscall,
153        // specifically with executable permissions, to map it into our address
154        // space. This is required so `perf inject` will work later. The `perf
155        // inject` command will see that an mmap syscall happened, and it'll see
156        // the filename we mapped, and that'll trigger it to actually read and
157        // parse the file.
158        //
159        // To match what some perf examples are doing we keep this `mmap` alive
160        // until this agent goes away.
161        let map_len = 1024;
162        let map_addr = unsafe {
163            let ptr = rustix::mm::mmap(
164                ptr::null_mut(),
165                map_len,
166                rustix::mm::ProtFlags::EXEC | rustix::mm::ProtFlags::READ,
167                rustix::mm::MapFlags::PRIVATE,
168                &jitdump_file,
169                0,
170            )?;
171            ptr as usize
172        };
173        let mut state = JitDumpFile {
174            jitdump_file,
175            map_addr,
176            map_len,
177            code_index: 0,
178            e_machine,
179        };
180        state.write_file_header()?;
181        Ok(state)
182    }
183}
184
185impl JitDumpFile {
186    /// Returns timestamp from a single source
187    pub fn get_time_stamp(&self) -> u64 {
188        // We need to use `CLOCK_MONOTONIC` on Linux which is what `Instant`
189        // conveniently also uses, but `Instant` doesn't allow us to get access
190        // to nanoseconds as an internal detail, so we calculate the nanoseconds
191        // ourselves here.
192        let ts = rustix::time::clock_gettime(rustix::time::ClockId::Monotonic);
193        // TODO: What does it mean for either sec or nsec to be negative?
194        (ts.tv_sec * 1_000_000_000 + ts.tv_nsec) as u64
195    }
196
197    /// Returns the next code index
198    pub fn next_code_index(&mut self) -> u64 {
199        let code_index = self.code_index;
200        self.code_index += 1;
201        code_index
202    }
203
204    pub fn write_file_header(&mut self) -> io::Result<()> {
205        let header = FileHeader {
206            timestamp: self.get_time_stamp(),
207            e_machine: self.e_machine,
208            magic: 0x4A695444,
209            version: 1,
210            size: mem::size_of::<FileHeader>() as u32,
211            pad1: 0,
212            pid: process::id(),
213            flags: 0,
214        };
215
216        self.jitdump_file.write_all(object::bytes_of(&header))?;
217        Ok(())
218    }
219
220    pub fn write_code_load_record(
221        &mut self,
222        record_name: &str,
223        cl_record: CodeLoadRecord,
224        code_buffer: &[u8],
225    ) -> io::Result<()> {
226        self.jitdump_file.write_all(object::bytes_of(&cl_record))?;
227        self.jitdump_file.write_all(record_name.as_bytes())?;
228        self.jitdump_file.write_all(b"\0")?;
229        self.jitdump_file.write_all(code_buffer)?;
230        Ok(())
231    }
232
233    /// Write DebugInfoRecord to open jit dump file.
234    /// Must be written before the corresponding CodeLoadRecord.
235    pub fn write_debug_info_record(&mut self, dir_record: DebugInfoRecord) -> io::Result<()> {
236        self.jitdump_file.write_all(object::bytes_of(&dir_record))?;
237        Ok(())
238    }
239
240    /// Write DebugInfoRecord to open jit dump file.
241    /// Must be written before the corresponding CodeLoadRecord.
242    pub fn write_debug_info_entries(&mut self, die_entries: Vec<DebugEntry>) -> io::Result<()> {
243        for entry in die_entries.iter() {
244            self.jitdump_file
245                .write_all(object::bytes_of(&entry.address))?;
246            self.jitdump_file.write_all(object::bytes_of(&entry.line))?;
247            self.jitdump_file
248                .write_all(object::bytes_of(&entry.discriminator))?;
249            self.jitdump_file.write_all(entry.filename.as_bytes())?;
250            self.jitdump_file.write_all(b"\0")?;
251        }
252        Ok(())
253    }
254
255    pub fn dump_code_load_record(
256        &mut self,
257        method_name: &str,
258        code: &[u8],
259        timestamp: u64,
260        pid: u32,
261        tid: u32,
262    ) -> io::Result<()> {
263        let name_len = method_name.len() + 1;
264        let size_limit = mem::size_of::<CodeLoadRecord>();
265
266        let rh = RecordHeader {
267            id: RecordId::JitCodeLoad as u32,
268            record_size: size_limit as u32 + name_len as u32 + code.len() as u32,
269            timestamp,
270        };
271
272        let clr = CodeLoadRecord {
273            header: rh,
274            pid,
275            tid,
276            virtual_address: code.as_ptr() as u64,
277            address: code.as_ptr() as u64,
278            size: code.len() as u64,
279            index: self.next_code_index(),
280        };
281
282        self.write_code_load_record(method_name, clr, code)
283    }
284}
285
286impl Drop for JitDumpFile {
287    fn drop(&mut self) {
288        unsafe {
289            rustix::mm::munmap(self.map_addr as *mut _, self.map_len).unwrap();
290        }
291    }
292}