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::string::String; 20 use std::vec::Vec; 21 use std::{mem, process}; 22 23 /// Defines jitdump record types 24 #[repr(u32)] 25 pub 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)] 41 pub 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 50 unsafe impl object::Pod for RecordHeader {} 51 52 /// The CodeLoadRecord is used for describing jitted functions 53 #[derive(Debug, Default, Clone, Copy)] 54 #[repr(C)] 55 pub 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 72 unsafe impl object::Pod for CodeLoadRecord {} 73 74 /// Describes source line information for a jitted function 75 #[derive(Debug, Default)] 76 #[repr(C)] 77 pub 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)] 93 pub 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 102 unsafe impl object::Pod for DebugInfoRecord {} 103 104 /// Fixed-sized header for each jitdump file 105 #[derive(Debug, Default, Clone, Copy)] 106 #[repr(C)] 107 pub 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 128 unsafe impl object::Pod for FileHeader {} 129 130 /// Interface for driving the creation of jitdump files 131 pub 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 144 impl 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 187 impl 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 288 impl 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 } 295