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 13 use std::fmt::Debug; 14 use std::fs::{File, OpenOptions}; 15 use std::io; 16 use std::io::Write; 17 use std::path::Path; 18 use std::ptr; 19 use std::{mem, process}; 20 21 /// Defines jitdump record types 22 #[repr(u32)] 23 pub 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)] 39 pub 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 48 unsafe impl object::Pod for RecordHeader {} 49 50 /// The CodeLoadRecord is used for describing jitted functions 51 #[derive(Debug, Default, Clone, Copy)] 52 #[repr(C)] 53 pub 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 70 unsafe impl object::Pod for CodeLoadRecord {} 71 72 /// Describes source line information for a jitted function 73 #[derive(Debug, Default)] 74 #[repr(C)] 75 pub 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)] 91 pub 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 100 unsafe impl object::Pod for DebugInfoRecord {} 101 102 /// Fixed-sized header for each jitdump file 103 #[derive(Debug, Default, Clone, Copy)] 104 #[repr(C)] 105 pub 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 126 unsafe impl object::Pod for FileHeader {} 127 128 /// Interface for driving the creation of jitdump files 129 pub 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 142 impl 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 185 impl 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 286 impl 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 } 293