use anyhow::{Context, Result, bail};
use clap::Parser;
use pulley_interpreter::decode::{Decoder, OpVisitor};
use pulley_interpreter::disas::Disassembler;
use pulley_interpreter::profile::{Event, decode};
use std::collections::BTreeMap;
use std::io::Write;
use std::path::PathBuf;
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
#[derive(Parser)]
struct ProfilerHtml {
profile: PathBuf,
#[clap(long, default_value = "0.5")]
threshold: f32,
#[clap(long)]
instructions: Option<bool>,
}
struct Function<'a> {
addr: u64,
hits: u64,
name: &'a str,
body: &'a [u8],
instructions: BTreeMap<u32, u32>,
}
fn main() -> Result<()> {
let args = ProfilerHtml::parse();
let profile = std::fs::read(&args.profile)
.with_context(|| format!("failed to read {:?}", args.profile))?;
let mut functions = BTreeMap::new();
let mut total = 0;
let mut found_samples = false;
for event in decode(&profile) {
match event? {
Event::Function(addr, name, body) => {
let prev = functions.insert(
addr,
Function {
addr,
name,
body,
hits: 0,
instructions: BTreeMap::new(),
},
);
assert!(prev.is_none());
}
Event::Samples(samples) => {
found_samples = true;
for sample in samples {
let addr = sample.0;
let (_, function) = functions.range_mut(..=addr).next_back().unwrap();
assert!(addr >= function.addr);
assert!(addr < function.addr + (function.body.len() as u64));
total += 1;
function.hits += 1;
*function
.instructions
.entry(u32::try_from(addr - function.addr).unwrap())
.or_insert(0) += 1;
}
}
}
}
if functions.is_empty() {
bail!("no functions found in profile");
}
if !found_samples {
bail!("no samples found in profile");
}
let mut funcs = functions
.into_iter()
.map(|(_, func)| func)
.collect::<Vec<_>>();
funcs.sort_by_key(|f| f.hits);
let mut term = StandardStream::stdout(ColorChoice::Auto);
let mut reset = ColorSpec::new();
reset.set_reset(true);
for mut func in funcs {
let func_pct = (func.hits as f32) / (total as f32) * 100.0;
if func_pct < args.threshold {
continue;
}
writeln!(
term,
"{:6.02}% {}",
(func.hits as f32) / (total as f32) * 100.0,
func.name,
)?;
if !args.instructions.unwrap_or(true) {
continue;
}
let mut disas = Disassembler::new(func.body);
disas.hexdump(false);
disas.offsets(false);
disas.br_tables(false);
let mut decoder = Decoder::new();
let mut prev = 0;
let mut offset = 0;
let mut remaining = func.body.len();
let min_instruction = func
.instructions
.iter()
.map(|(_, hits)| *hits)
.min()
.unwrap_or(0);
let max_instruction = func
.instructions
.iter()
.map(|(_, hits)| *hits)
.max()
.unwrap_or(0);
while !disas.bytecode().as_slice().is_empty() {
decoder.decode_one(&mut disas)?;
let instr = &disas.disas()[prev..].trim();
let hits = func.instructions.remove(&offset).unwrap_or(0);
let pct = (hits as f32) / (func.hits as f32) * 100.;
if pct < args.threshold {
term.set_color(&reset)?;
writeln!(term, "\t {:6x}: {instr}", u64::from(offset))?;
} else {
let mut color = ColorSpec::new();
color.set_bold(hits == max_instruction);
let pct_r =
(hits - min_instruction) as f32 / (max_instruction - min_instruction) as f32;
let r = ((0xff as f32) * pct_r) as u8;
let g = ((0xff as f32) * (1. - pct_r)) as u8;
let b = 0;
color.set_fg(Some(Color::Rgb(r, g, b)));
term.set_color(&color)?;
writeln!(term, "\t{pct:6.02}% {:6x}: {instr}", u64::from(offset))?;
}
offset += u32::try_from(remaining - disas.bytecode().as_slice().len()).unwrap();
remaining = disas.bytecode().as_slice().len();
prev = disas.disas().len();
}
term.set_color(&reset)?;
assert!(func.instructions.is_empty(), "{:?}", func.instructions);
}
Ok(())
}