Path: blob/main/pulley/examples/profiler-html.rs
1692 views
//! Example program of parsing and displaying pulley profiles.1//!2//! To use this program first build Wasmtime with support for profiling Pulley:3//!4//! ```text5//! $ cargo build --release --features profile-pulley6//! ```7//!8//! Next record a profile9//!10//! ```text11//! $ ./target/release/wasmtime run --profile pulley --target pulley64 \12//! your_wasm_file.wasm13//! ```14//!15//! This will emit `pulley-$pid.data` to the current working directory. That16//! file is then fed to this program:17//!18//! ```text19//! $ cargo run -p pulley-interpreter --example profiler-html --all-features \20//! ./pulley-$pid.data21//! ```22//!23//! This will print all functions and their disassemblies to stdout. Functions24//! are annotated with the % of samples that fell in that function. Instructions25//! in functions are annotated with the % of samples in that function that fell26//! on that instruction. Functions are dropped if their sample rate is below the27//! CLI threshold and instructions are un-annotated if they're below the28//! threshold.2930use anyhow::{Context, Result, bail};31use clap::Parser;32use pulley_interpreter::decode::{Decoder, OpVisitor};33use pulley_interpreter::disas::Disassembler;34use pulley_interpreter::profile::{Event, decode};35use std::collections::BTreeMap;36use std::io::Write;37use std::path::PathBuf;38use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};3940#[derive(Parser)]41struct ProfilerHtml {42/// The profile data to load which was generated by a `--profile pulley` run43/// of Wasmtime previously.44profile: PathBuf,4546/// The minimum threshold to display a function or annotate an instruction.47#[clap(long, default_value = "0.5")]48threshold: f32,4950/// Whether or not to show instruction disassemblies.51#[clap(long)]52instructions: Option<bool>,53}5455struct Function<'a> {56addr: u64,57hits: u64,58name: &'a str,59body: &'a [u8],60instructions: BTreeMap<u32, u32>,61}6263fn main() -> Result<()> {64let args = ProfilerHtml::parse();65let profile = std::fs::read(&args.profile)66.with_context(|| format!("failed to read {:?}", args.profile))?;6768// All known functions and the total of all samples taken.69let mut functions = BTreeMap::new();70let mut total = 0;7172let mut found_samples = false;73for event in decode(&profile) {74match event? {75Event::Function(addr, name, body) => {76let prev = functions.insert(77addr,78Function {79addr,80name,81body,82hits: 0,83instructions: BTreeMap::new(),84},85);86assert!(prev.is_none());87}88Event::Samples(samples) => {89found_samples = true;90for sample in samples {91let addr = sample.0;92let (_, function) = functions.range_mut(..=addr).next_back().unwrap();93assert!(addr >= function.addr);94assert!(addr < function.addr + (function.body.len() as u64));9596total += 1;97function.hits += 1;98*function99.instructions100.entry(u32::try_from(addr - function.addr).unwrap())101.or_insert(0) += 1;102}103}104}105}106107if functions.is_empty() {108bail!("no functions found in profile");109}110if !found_samples {111bail!("no samples found in profile");112}113114let mut funcs = functions115.into_iter()116.map(|(_, func)| func)117.collect::<Vec<_>>();118funcs.sort_by_key(|f| f.hits);119120let mut term = StandardStream::stdout(ColorChoice::Auto);121let mut reset = ColorSpec::new();122reset.set_reset(true);123124for mut func in funcs {125let func_pct = (func.hits as f32) / (total as f32) * 100.0;126if func_pct < args.threshold {127continue;128}129writeln!(130term,131"{:6.02}% {}",132(func.hits as f32) / (total as f32) * 100.0,133func.name,134)?;135136if !args.instructions.unwrap_or(true) {137continue;138}139140let mut disas = Disassembler::new(func.body);141disas.hexdump(false);142disas.offsets(false);143disas.br_tables(false);144let mut decoder = Decoder::new();145let mut prev = 0;146let mut offset = 0;147let mut remaining = func.body.len();148149let min_instruction = func150.instructions151.iter()152.map(|(_, hits)| *hits)153.min()154.unwrap_or(0);155let max_instruction = func156.instructions157.iter()158.map(|(_, hits)| *hits)159.max()160.unwrap_or(0);161162while !disas.bytecode().as_slice().is_empty() {163decoder.decode_one(&mut disas)?;164let instr = &disas.disas()[prev..].trim();165let hits = func.instructions.remove(&offset).unwrap_or(0);166let pct = (hits as f32) / (func.hits as f32) * 100.;167if pct < args.threshold {168term.set_color(&reset)?;169writeln!(term, "\t {:6x}: {instr}", u64::from(offset))?;170} else {171// Attempt to do a bit of a gradient from red-to-green from172// least-hit to most-hit instruction. Note that un-annotated173// instructions will have no color still (e.g. they aren't174// green).175let mut color = ColorSpec::new();176color.set_bold(hits == max_instruction);177let pct_r =178(hits - min_instruction) as f32 / (max_instruction - min_instruction) as f32;179180let r = ((0xff as f32) * pct_r) as u8;181let g = ((0xff as f32) * (1. - pct_r)) as u8;182let b = 0;183color.set_fg(Some(Color::Rgb(r, g, b)));184term.set_color(&color)?;185writeln!(term, "\t{pct:6.02}% {:6x}: {instr}", u64::from(offset))?;186}187offset += u32::try_from(remaining - disas.bytecode().as_slice().len()).unwrap();188remaining = disas.bytecode().as_slice().len();189prev = disas.disas().len();190}191192term.set_color(&reset)?;193194assert!(func.instructions.is_empty(), "{:?}", func.instructions);195}196197Ok(())198}199200201