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}