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