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