1 //! Example program of parsing and displaying pulley profiles. 2 //! 3 //! To use this program first build Wasmtime with support for profiling Pulley: 4 //! 5 //! ```text 6 //! $ cargo build --release --features profile-pulley 7 //! ``` 8 //! 9 //! Next record a profile 10 //! 11 //! ```text 12 //! $ ./target/release/wasmtime run --profile pulley --target pulley64 \ 13 //! your_wasm_file.wasm 14 //! ``` 15 //! 16 //! This will emit `pulley-$pid.data` to the current working directory. That 17 //! file is then fed to this program: 18 //! 19 //! ```text 20 //! $ cargo run -p pulley-interpreter --example profiler-html --all-features \ 21 //! ./pulley-$pid.data 22 //! ``` 23 //! 24 //! This will print all functions and their disassemblies to stdout. Functions 25 //! are annotated with the % of samples that fell in that function. Instructions 26 //! in functions are annotated with the % of samples in that function that fell 27 //! on that instruction. Functions are dropped if their sample rate is below the 28 //! CLI threshold and instructions are un-annotated if they're below the 29 //! threshold. 30 31 use anyhow::{Context, Result, bail}; 32 use clap::Parser; 33 use pulley_interpreter::decode::{Decoder, OpVisitor}; 34 use pulley_interpreter::disas::Disassembler; 35 use pulley_interpreter::profile::{Event, decode}; 36 use std::collections::BTreeMap; 37 use std::io::Write; 38 use std::path::PathBuf; 39 use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; 40 41 #[derive(Parser)] 42 struct ProfilerHtml { 43 /// The profile data to load which was generated by a `--profile pulley` run 44 /// of Wasmtime previously. 45 profile: PathBuf, 46 47 /// The minimum threshold to display a function or annotate an instruction. 48 #[clap(long, default_value = "0.5")] 49 threshold: f32, 50 51 /// Whether or not to show instruction disassemblies. 52 #[clap(long)] 53 instructions: Option<bool>, 54 } 55 56 struct Function<'a> { 57 addr: u64, 58 hits: u64, 59 name: &'a str, 60 body: &'a [u8], 61 instructions: BTreeMap<u32, u32>, 62 } 63 64 fn main() -> Result<()> { 65 let args = ProfilerHtml::parse(); 66 let profile = std::fs::read(&args.profile) 67 .with_context(|| format!("failed to read {:?}", args.profile))?; 68 69 // All known functions and the total of all samples taken. 70 let mut functions = BTreeMap::new(); 71 let mut total = 0; 72 73 let mut found_samples = false; 74 for event in decode(&profile) { 75 match event? { 76 Event::Function(addr, name, body) => { 77 let prev = functions.insert( 78 addr, 79 Function { 80 addr, 81 name, 82 body, 83 hits: 0, 84 instructions: BTreeMap::new(), 85 }, 86 ); 87 assert!(prev.is_none()); 88 } 89 Event::Samples(samples) => { 90 found_samples = true; 91 for sample in samples { 92 let addr = sample.0; 93 let (_, function) = functions.range_mut(..=addr).next_back().unwrap(); 94 assert!(addr >= function.addr); 95 assert!(addr < function.addr + (function.body.len() as u64)); 96 97 total += 1; 98 function.hits += 1; 99 *function 100 .instructions 101 .entry(u32::try_from(addr - function.addr).unwrap()) 102 .or_insert(0) += 1; 103 } 104 } 105 } 106 } 107 108 if functions.is_empty() { 109 bail!("no functions found in profile"); 110 } 111 if !found_samples { 112 bail!("no samples found in profile"); 113 } 114 115 let mut funcs = functions 116 .into_iter() 117 .map(|(_, func)| func) 118 .collect::<Vec<_>>(); 119 funcs.sort_by_key(|f| f.hits); 120 121 let mut term = StandardStream::stdout(ColorChoice::Auto); 122 let mut reset = ColorSpec::new(); 123 reset.set_reset(true); 124 125 for mut func in funcs { 126 let func_pct = (func.hits as f32) / (total as f32) * 100.0; 127 if func_pct < args.threshold { 128 continue; 129 } 130 writeln!( 131 term, 132 "{:6.02}% {}", 133 (func.hits as f32) / (total as f32) * 100.0, 134 func.name, 135 )?; 136 137 if !args.instructions.unwrap_or(true) { 138 continue; 139 } 140 141 let mut disas = Disassembler::new(func.body); 142 disas.hexdump(false); 143 disas.offsets(false); 144 disas.br_tables(false); 145 let mut decoder = Decoder::new(); 146 let mut prev = 0; 147 let mut offset = 0; 148 let mut remaining = func.body.len(); 149 150 let min_instruction = func 151 .instructions 152 .iter() 153 .map(|(_, hits)| *hits) 154 .min() 155 .unwrap_or(0); 156 let max_instruction = func 157 .instructions 158 .iter() 159 .map(|(_, hits)| *hits) 160 .max() 161 .unwrap_or(0); 162 163 while !disas.bytecode().as_slice().is_empty() { 164 decoder.decode_one(&mut disas)?; 165 let instr = &disas.disas()[prev..].trim(); 166 let hits = func.instructions.remove(&offset).unwrap_or(0); 167 let pct = (hits as f32) / (func.hits as f32) * 100.; 168 if pct < args.threshold { 169 term.set_color(&reset)?; 170 writeln!(term, "\t {:6x}: {instr}", u64::from(offset))?; 171 } else { 172 // Attempt to do a bit of a gradient from red-to-green from 173 // least-hit to most-hit instruction. Note that un-annotated 174 // instructions will have no color still (e.g. they aren't 175 // green). 176 let mut color = ColorSpec::new(); 177 color.set_bold(hits == max_instruction); 178 let pct_r = 179 (hits - min_instruction) as f32 / (max_instruction - min_instruction) as f32; 180 181 let r = ((0xff as f32) * pct_r) as u8; 182 let g = ((0xff as f32) * (1. - pct_r)) as u8; 183 let b = 0; 184 color.set_fg(Some(Color::Rgb(r, g, b))); 185 term.set_color(&color)?; 186 writeln!(term, "\t{pct:6.02}% {:6x}: {instr}", u64::from(offset))?; 187 } 188 offset += u32::try_from(remaining - disas.bytecode().as_slice().len()).unwrap(); 189 remaining = disas.bytecode().as_slice().len(); 190 prev = disas.disas().len(); 191 } 192 193 term.set_color(&reset)?; 194 195 assert!(func.instructions.is_empty(), "{:?}", func.instructions); 196 } 197 198 Ok(()) 199 } 200