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 
main() -> Result<()>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