use crate::common::{RunCommon, RunTarget};
use capstone::arch::BuildsCapstone;
use clap::Parser;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::io::{self, BufWriter, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::str::FromStr;
use tempfile::tempdir;
use wasmtime::{
CodeBuilder, CodeHint, Engine, FuncIndex, ModuleFunction, Result, StaticModuleIndex, bail,
error::Context as _, format_err,
};
#[derive(Parser)]
#[command(name = "hot-blocks")]
pub struct HotBlocksCommand {
#[command(flatten)]
run: RunCommon,
#[clap(short, long, default_value = "50")]
percent: f64,
#[clap(short, long, value_enum, default_value = "cpu-cycles")]
event: Event,
#[clap(short = 'F', long)]
frequency: Option<u64>,
#[clap(short, long)]
output: Option<PathBuf>,
#[arg(required = true, value_name = "MODULE")]
module: PathBuf,
#[arg(trailing_var_arg = true)]
module_args: Vec<String>,
}
#[derive(Clone, Debug, clap::ValueEnum)]
pub enum Event {
Instructions,
CpuCycles,
}
impl Event {
fn perf_event(&self) -> &str {
match self {
Event::Instructions => "instructions",
Event::CpuCycles => "cpu-cycles",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct BlockIndex(usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct FunctionOffset(usize);
impl HotBlocksCommand {
pub fn execute(mut self) -> Result<()> {
self.run.common.init_logging()?;
if !(0.0..=100.0).contains(&self.percent) {
bail!("--percent must be between 0 and 100 inclusive");
}
if self.run.common.debug.address_map == Some(false) {
bail!(
"address maps must be enabled for hot-blocks profiling; do not pass -Daddress-map=n"
);
}
self.run.common.debug.address_map = Some(true);
let tmp_dir = tempdir().context("failed to create temp directory")?;
let clif_dir = tmp_dir.path().join("clif");
std::fs::create_dir(&clif_dir)?;
let cwasm_path = tmp_dir.path().join("module.cwasm");
let wasm_bytes =
Cow::Owned(std::fs::read(&self.module).with_context(|| {
format!("failed to read Wasm module: {}", self.module.display())
})?);
#[cfg(feature = "wat")]
let wasm_bytes = wat::parse_bytes(&wasm_bytes).map_err(|mut e| {
e.set_path(&self.module);
e
})?;
let engine = self.compile_to_cwasm(&clif_dir, &cwasm_path, &wasm_bytes)?;
let perf_data_path = tmp_dir.path().join("perf.data");
self.run_perf_record(&cwasm_path, &perf_data_path)?;
let (samples, total_samples) = self.run_perf_script(&perf_data_path)?;
let target = match self.run.common.target.as_deref() {
None => target_lexicon::Triple::host(),
Some(t) => target_lexicon::Triple::from_str(t)?,
};
let wat_map = build_wat_offset_map(&wasm_bytes);
self.run.allow_precompiled = true;
let run_target = self.run.load_module(&engine, &cwasm_path, None)?;
let (functions, text, address_map) = match &run_target {
RunTarget::Core(module) => (
module.functions().collect::<Vec<_>>(),
module.text(),
module
.address_map()
.ok_or_else(|| {
format_err!("address maps are not available in the compiled module")
})?
.collect::<Vec<_>>(),
),
#[cfg(feature = "component-model")]
RunTarget::Component(component) => (
component.functions().collect::<Vec<_>>(),
component.text(),
component
.address_map()
.ok_or_else(|| {
format_err!("address maps are not available in the compiled component")
})?
.collect::<Vec<_>>(),
),
};
let mut output: Box<dyn Write> = match &self.output {
Some(path) => {
let file = std::fs::File::create(path)
.with_context(|| format!("failed to create output file: {}", path.display()))?;
Box::new(BufWriter::new(file))
}
None => Box::new(io::stdout()),
};
self.format_hot_blocks(
&samples,
total_samples,
&functions,
&text,
&address_map,
&clif_dir,
&wat_map,
&target,
&mut *output,
)?;
Ok(())
}
fn compile_to_cwasm(
&mut self,
clif_dir: &Path,
cwasm_path: &Path,
wasm_bytes: &[u8],
) -> Result<Engine> {
let mut config = self.run.common.config(None)?;
config.emit_clif(clif_dir);
let engine = Engine::new(&config)?;
let mut code = CodeBuilder::new(&engine);
code.wasm_binary_or_text(wasm_bytes, Some(&self.module))?;
let serialized = match code.hint() {
#[cfg(feature = "component-model")]
Some(CodeHint::Component) => code.compile_component_serialized()?,
#[cfg(not(feature = "component-model"))]
Some(CodeHint::Component) => {
bail!("component model support was disabled at compile time")
}
Some(CodeHint::Module) | None => code.compile_module_serialized()?,
};
std::fs::write(cwasm_path, &serialized)
.with_context(|| format!("failed to write cwasm: {}", cwasm_path.display()))?;
Ok(engine)
}
fn run_perf_record(&self, cwasm_path: &Path, perf_data_path: &Path) -> Result<()> {
let current_exe =
std::env::current_exe().context("failed to determine current executable")?;
let mut perf_cmd = Command::new("perf");
perf_cmd
.arg("record")
.arg("-e")
.arg(self.event.perf_event())
.arg("--no-buildid")
.arg("-o")
.arg(perf_data_path);
if let Some(freq) = self.frequency {
perf_cmd.arg("-F").arg(freq.to_string());
}
perf_cmd
.arg("--")
.arg(¤t_exe)
.arg("run")
.arg("--allow-precompiled")
.arg("--profile=perfmap");
for arg in self.run.to_string().split_whitespace() {
perf_cmd.arg(arg);
}
perf_cmd.arg(cwasm_path.as_os_str());
for arg in &self.module_args {
perf_cmd.arg(arg);
}
let perf_output = perf_cmd
.output()
.context("failed to run `perf record`; is `perf` installed?")?;
if !perf_output.status.success() {
let stderr = String::from_utf8_lossy(&perf_output.stderr);
bail!("perf record failed:\n{stderr}");
}
Ok(())
}
fn run_perf_script(&self, perf_data_path: &Path) -> Result<(Vec<PerfSample>, usize)> {
let perf_script_output = Command::new("perf")
.arg("script")
.arg("-i")
.arg(perf_data_path)
.arg("-F")
.arg("ip,sym,symoff,dso")
.output()
.context("failed to run `perf script`")?;
if !perf_script_output.status.success() {
let stderr = String::from_utf8_lossy(&perf_script_output.stderr);
bail!("perf script failed:\n{stderr}");
}
let script_text = String::from_utf8_lossy(&perf_script_output.stdout);
Ok(parse_perf_script(&script_text))
}
fn format_hot_blocks(
&self,
samples: &[PerfSample],
total_samples: usize,
functions: &[ModuleFunction],
text: &[u8],
address_map: &[(usize, Option<u32>)],
clif_dir: &Path,
wat_map: &BTreeMap<u32, String>,
target: &target_lexicon::Triple,
output: &mut dyn Write,
) -> Result<()> {
let wasm_samples = samples.len();
writeln!(
output,
"Collected {total_samples} total samples; {wasm_samples} ({:.2}%) Wasm samples.",
wasm_samples as f64 / total_samples as f64 * 100.0,
)?;
writeln!(output)?;
if wasm_samples == 0 {
writeln!(output, "No samples collected within WebAssembly code.")?;
return Ok(());
}
let func_map: BTreeMap<(StaticModuleIndex, FuncIndex), &ModuleFunction> =
functions.iter().map(|f| ((f.module, f.index), f)).collect();
let mut func_blocks: BTreeMap<(StaticModuleIndex, FuncIndex), Vec<BasicBlock>> =
BTreeMap::new();
let mut block_samples: BTreeMap<(StaticModuleIndex, FuncIndex, BlockIndex), u64> =
BTreeMap::new();
let mut inst_samples: BTreeMap<
(StaticModuleIndex, FuncIndex, BlockIndex, FunctionOffset),
u64,
> = BTreeMap::new();
for sample in samples {
let Some(func) = find_function_for_sample(sample, functions) else {
continue;
};
let key = (func.module, func.index);
let blocks = func_blocks.entry(key).or_insert_with(|| {
let body = &text[func.offset..][..func.len];
let clif_lines =
read_clif_file(clif_dir, func.module, func.index, func.name.as_deref());
build_basic_blocks(body, func.offset, address_map, &clif_lines, wat_map, target)
.unwrap_or_default()
});
let offset_in_func = FunctionOffset(usize::try_from(sample.offset).unwrap());
if let Some(block_idx) = find_block_for_offset(blocks, offset_in_func) {
*block_samples.entry((key.0, key.1, block_idx)).or_default() += 1;
*inst_samples
.entry((key.0, key.1, block_idx, offset_in_func))
.or_default() += 1;
}
}
let mut sorted_blocks: Vec<_> = block_samples.into_iter().collect();
sorted_blocks.sort_by(|a, b| b.1.cmp(&a.1));
let total_f64 = total_samples as f64;
let mut samples_printed: u64 = 0;
for ((mod_idx, func_idx, block_idx), block_sample_count) in &sorted_blocks {
let percent_printed = samples_printed as f64 / total_f64 * 100.0;
if percent_printed >= self.percent {
break;
}
let block_percent = *block_sample_count as f64 / total_f64 * 100.0;
let func_name = func_map
.get(&(*mod_idx, *func_idx))
.and_then(|f| f.name.clone())
.unwrap_or_else(|| {
format!(
"wasm[{}]::function[{}]",
mod_idx.as_u32(),
func_idx.as_u32()
)
});
let blocks = func_blocks.get(&(*mod_idx, *func_idx)).unwrap();
let block = &blocks[block_idx.0];
let first_sampled = block
.instructions
.iter()
.position(|inst| {
inst_samples
.get(&(
*mod_idx,
*func_idx,
*block_idx,
FunctionOffset(inst.offset_in_func),
))
.copied()
.unwrap_or(0)
> 0
})
.unwrap_or(0);
let visible_instructions = &block.instructions[first_sampled..];
writeln!(
output,
"`{func_name}` :: block {:#x} :: {block_percent:.2}% total samples",
block.instructions[first_sampled].offset_in_func,
)?;
writeln!(output)?;
let max_asm_len = visible_instructions
.iter()
.map(|i| i.assembly.len())
.max()
.unwrap_or(10);
let max_clif_len = visible_instructions
.iter()
.map(|i| i.clif.as_ref().map_or(1, |c| c.len()))
.max()
.unwrap_or(6);
let asm_width = max_asm_len.clamp(10, 60);
let clif_width = max_clif_len.clamp(6, 40);
writeln!(
output,
"{:>10} {:<asm_width$} {:<clif_width$} {}",
"[Samples]", "[Assembly]", "[CLIF]", "[Wasm]"
)?;
let mut prev_clif: Option<(&str, Option<u32>)> = None;
let mut prev_wasm: Option<(&str, Option<u32>)> = None;
for inst in visible_instructions {
let sample_count = inst_samples
.get(&(
*mod_idx,
*func_idx,
*block_idx,
FunctionOffset(inst.offset_in_func),
))
.copied()
.unwrap_or(0);
let sample_str = if sample_count > 0 {
format!("{:.2}%", sample_count as f64 / total_f64 * 100.0)
} else {
String::new()
};
let asm_str = &inst.assembly[..inst.assembly.len().min(asm_width)];
let clif_display = if let Some(ref clif_text) = inst.clif {
let current = (clif_text.as_str(), inst.wasm_offset);
if prev_clif == Some(current) {
"\"".to_string()
} else {
prev_clif = Some((clif_text.as_str(), inst.wasm_offset));
clif_text.clone()
}
} else {
prev_clif = None;
"-".to_string()
};
let clif_display = &clif_display[..clif_display.len().min(clif_width)];
let wasm_display = if let Some(ref wasm_text) = inst.wasm {
let current = (wasm_text.as_str(), inst.wasm_offset);
if prev_wasm == Some(current) {
"\"".to_string()
} else {
prev_wasm = Some((wasm_text.as_str(), inst.wasm_offset));
wasm_text.clone()
}
} else {
prev_wasm = None;
"-".to_string()
};
let wasm_display = &wasm_display[..wasm_display.len().min(40)];
writeln!(
output,
"{sample_str:>10} {asm_str:<asm_width$} {clif_display:<clif_width$} {wasm_display}",
)?;
}
writeln!(output)?;
samples_printed += block_sample_count;
}
Ok(())
}
}
#[derive(Debug, Clone)]
struct PerfSample {
symbol: String,
offset: u64,
}
fn parse_perf_script(output: &str) -> (Vec<PerfSample>, usize) {
let mut samples = Vec::new();
let mut total_samples = 0;
for line in output.lines() {
total_samples += 1;
if let Some(sample) = parse_perf_script_line(line.trim()) {
samples.push(sample);
}
}
(samples, total_samples)
}
fn parse_perf_script_line(line: &str) -> Option<PerfSample> {
if !line.ends_with(".map)") && !line.ends_with(".cwasm)") {
return None;
}
let rest = line.trim_start_matches(|c: char| c.is_ascii_hexdigit() || c == ' ');
let (sym_with_offset, _dso) = rest.split_once(" (").unwrap_or((rest, ""));
let sym_with_offset = sym_with_offset.trim();
let (symbol, offset_str) = sym_with_offset
.rsplit_once('+')
.unwrap_or((sym_with_offset, "0x0"));
let offset_str = offset_str.trim_start_matches("0x").trim_start_matches("0X");
let offset = u64::from_str_radix(offset_str, 16).unwrap_or(0);
Some(PerfSample {
symbol: symbol.to_string(),
offset,
})
}
#[derive(Debug, Clone)]
struct BlockInstruction {
offset_in_func: usize,
assembly: String,
clif: Option<String>,
wasm_offset: Option<u32>,
wasm: Option<String>,
}
#[derive(Debug, Clone)]
struct BasicBlock {
instructions: Vec<BlockInstruction>,
}
fn build_capstone(target: &target_lexicon::Triple) -> Result<capstone::Capstone> {
let mut cs = match target.architecture {
target_lexicon::Architecture::Aarch64(_) => capstone::Capstone::new()
.arm64()
.mode(capstone::arch::arm64::ArchMode::Arm)
.detail(true)
.build()
.map_err(|e| format_err!("{e}"))?,
target_lexicon::Architecture::Riscv64(_) => capstone::Capstone::new()
.riscv()
.mode(capstone::arch::riscv::ArchMode::RiscV64)
.detail(true)
.build()
.map_err(|e| format_err!("{e}"))?,
target_lexicon::Architecture::S390x => capstone::Capstone::new()
.sysz()
.mode(capstone::arch::sysz::ArchMode::Default)
.detail(true)
.build()
.map_err(|e| format_err!("{e}"))?,
target_lexicon::Architecture::X86_64 => capstone::Capstone::new()
.x86()
.mode(capstone::arch::x86::ArchMode::Mode64)
.detail(true)
.build()
.map_err(|e| format_err!("{e}"))?,
_ => bail!("unsupported target architecture: {target}"),
};
cs.set_skipdata(true).unwrap();
Ok(cs)
}
fn build_basic_blocks(
func_body: &[u8],
func_offset: usize,
address_map: &[(usize, Option<u32>)],
clif_lines: &[(Option<u32>, String)],
wat_map: &BTreeMap<u32, String>,
target: &target_lexicon::Triple,
) -> Result<Vec<BasicBlock>> {
let cs = build_capstone(target)?;
let insts =
crate::disas::disas_with_capstone(&cs, func_body, u64::try_from(func_offset).unwrap())?;
let mut offset_to_wasm: BTreeMap<usize, Option<u32>> = BTreeMap::new();
for &(code_offset, wasm_offset) in address_map {
if code_offset >= func_offset && code_offset < func_offset + func_body.len() {
offset_to_wasm.insert(code_offset, wasm_offset);
}
}
let mut wasm_to_clif: BTreeMap<u32, Vec<&str>> = BTreeMap::new();
for (wasm_off, clif_text) in clif_lines {
if let Some(off) = wasm_off {
wasm_to_clif.entry(*off).or_default().push(clif_text);
}
}
let mut blocks = Vec::new();
let mut current_block = Vec::new();
for inst in &insts {
let addr = usize::try_from(inst.address).unwrap();
let offset_in_func = addr - func_offset;
let wasm_offset = find_wasm_offset_for_address(&offset_to_wasm, addr);
let clif = wasm_offset
.and_then(|wo| wasm_to_clif.get(&wo))
.map(|lines| lines.join("; "));
let wasm = wasm_offset.and_then(|wo| wat_map.get(&wo).cloned());
current_block.push(BlockInstruction {
offset_in_func,
assembly: inst.disassembly.clone(),
clif,
wasm_offset,
wasm,
});
if inst.is_jump || inst.is_return {
blocks.push(BasicBlock {
instructions: std::mem::take(&mut current_block),
});
}
}
if !current_block.is_empty() {
blocks.push(BasicBlock {
instructions: current_block,
});
}
Ok(blocks)
}
fn find_wasm_offset_for_address(
offset_to_wasm: &BTreeMap<usize, Option<u32>>,
addr: usize,
) -> Option<u32> {
offset_to_wasm
.range(..=addr)
.next_back()
.and_then(|(_, wasm_off)| *wasm_off)
}
fn build_wat_offset_map(wasm_bytes: &[u8]) -> BTreeMap<u32, String> {
let mut map = BTreeMap::new();
let printer = wasmprinter::Config::new();
let mut storage = String::new();
let Ok(chunks) = printer.offsets_and_lines(wasm_bytes, &mut storage) else {
return map;
};
for (offset, wat_line) in chunks {
if let Some(offset) = offset {
let trimmed = wat_line.trim();
if !trimmed.is_empty() && !trimmed.starts_with('(') && !trimmed.starts_with(')') {
map.insert(u32::try_from(offset).unwrap(), trimmed.to_string());
}
}
}
map
}
fn read_clif_file(
clif_dir: &Path,
mod_idx: StaticModuleIndex,
func_index: FuncIndex,
func_name: Option<&str>,
) -> Vec<(Option<u32>, String)> {
let contents = find_and_read_clif(clif_dir, mod_idx, func_index, func_name);
let Some(contents) = contents else {
return Vec::new();
};
let mut result = Vec::new();
for line in contents.lines() {
if line.is_empty() {
continue;
}
let trimmed = line.trim_start();
if trimmed.starts_with('@') {
let offset = u32::from_str_radix(&trimmed[1..5], 16).ok();
let text = trimmed[5..].trim().to_string();
result.push((offset, text));
} else if line.starts_with(' ') {
let text = trimmed.to_string();
result.push((None, text));
} else {
result.push((None, trimmed.to_string()));
}
}
result
}
fn find_and_read_clif(
clif_dir: &Path,
mod_idx: StaticModuleIndex,
func_index: FuncIndex,
func_name: Option<&str>,
) -> Option<String> {
let mod_idx = mod_idx.as_u32();
let func_idx = func_index.as_u32();
if let Some(name) = func_name {
let short_name = name.rsplit("::").next().unwrap_or(name);
let path = clif_dir.join(format!(
"wasm[{mod_idx}]--function[{func_idx}]--{short_name}.clif"
));
if let Ok(contents) = std::fs::read_to_string(&path) {
return Some(contents);
}
}
let path = clif_dir.join(format!("wasm[{mod_idx}]--function[{func_idx}].clif"));
if let Ok(contents) = std::fs::read_to_string(&path) {
return Some(contents);
}
None
}
fn parse_wasm_func_name(name: &str) -> Option<(StaticModuleIndex, FuncIndex)> {
let rest = name.strip_prefix("wasm[")?;
let (mod_idx_str, rest) = rest.split_once(']')?;
let rest = rest.strip_prefix("::function[")?;
let (func_idx_str, _) = rest.split_once(']')?;
let mod_idx: u32 = mod_idx_str.parse().ok()?;
let func_idx: u32 = func_idx_str.parse().ok()?;
Some((
StaticModuleIndex::from_u32(mod_idx),
FuncIndex::from_u32(func_idx),
))
}
fn find_function_for_sample<'a>(
sample: &PerfSample,
functions: &'a [ModuleFunction],
) -> Option<&'a ModuleFunction> {
let (mod_idx, func_idx) = parse_wasm_func_name(&sample.symbol)?;
functions
.binary_search_by_key(&(mod_idx, func_idx), |f| (f.module, f.index))
.ok()
.map(|i| &functions[i])
}
fn find_block_for_offset(
blocks: &[BasicBlock],
offset_in_func: FunctionOffset,
) -> Option<BlockIndex> {
let idx = blocks
.binary_search_by_key(&offset_in_func.0, |b| b.instructions[0].offset_in_func)
.unwrap_or_else(|i| i.saturating_sub(1));
let block = blocks.get(idx)?;
let last_offset = block.instructions.last()?.offset_in_func;
if offset_in_func.0 >= block.instructions[0].offset_in_func && offset_in_func.0 <= last_offset {
Some(BlockIndex(idx))
} else {
None
}
}
#[cfg(all(
test,
// These tests don't exercise any unsafe code and so they are
// generally uninteresting to run under MIRI.
not(miri)
))]
mod test {
use super::*;
#[test]
fn test_parse_perf_script_line_map_dso() {
let line = " 7f1234567890 wasm[0]::function[3]+0x10 (/tmp/perf-1234.map)";
let sample = parse_perf_script_line(line).unwrap();
assert_eq!(sample.symbol, "wasm[0]::function[3]");
assert_eq!(sample.offset, 0x10);
}
#[test]
fn test_parse_perf_script_line_no_offset() {
let line = "7f1234567890 wasm[0]::function[0]+0x0 (/tmp/perf-123.map)";
let sample = parse_perf_script_line(line).unwrap();
assert_eq!(sample.symbol, "wasm[0]::function[0]");
assert_eq!(sample.offset, 0);
}
#[test]
fn test_parse_perf_script_line_non_map_dso() {
let line = "7f1234567890 main+0x10 (/usr/bin/wasmtime)";
assert!(parse_perf_script_line(line).is_none());
}
#[test]
fn test_parse_perf_script_line_cwasm_dso() {
let line = " 7f1234567890 wasm[0]::function[1]+0x20 (/tmp/.tmpABC123/module.cwasm)";
let sample = parse_perf_script_line(line).unwrap();
assert_eq!(sample.symbol, "wasm[0]::function[1]");
assert_eq!(sample.offset, 0x20);
}
#[test]
fn test_parse_perf_script_line_trampoline() {
let line = "7f1234567890 trampoline+0x5 (/tmp/perf-1234.map)";
let sample = parse_perf_script_line(line).unwrap();
assert_eq!(sample.symbol, "trampoline");
assert_eq!(sample.offset, 0x5);
}
#[test]
fn test_parse_wasm_func_name() {
assert_eq!(
parse_wasm_func_name("wasm[0]::function[3]"),
Some((StaticModuleIndex::from_u32(0), FuncIndex::from_u32(3)))
);
assert_eq!(
parse_wasm_func_name("wasm[1]::function[42]"),
Some((StaticModuleIndex::from_u32(1), FuncIndex::from_u32(42)))
);
assert_eq!(parse_wasm_func_name("main"), None);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn test_mocked_hot_blocks() {
let func_body: &[u8] = &[
0x90, 0x90, 0xeb, 0x00, 0x90, 0xc3, ];
let func_offset = 0x1000usize;
let address_map = vec![
(func_offset, Some(0x0010u32)),
(func_offset + 1, Some(0x0011)),
(func_offset + 2, Some(0x0012)),
(func_offset + 4, Some(0x0013)),
(func_offset + 5, Some(0x0014)),
];
let clif_lines = vec![
(Some(0x0010u32), "v1 = iconst.i32 0".to_string()),
(Some(0x0011u32), "v2 = iconst.i32 1".to_string()),
(Some(0x0012u32), "jump block1".to_string()),
(Some(0x0013u32), "v3 = iadd v1, v2".to_string()),
(Some(0x0014u32), "return v3".to_string()),
];
let mut wat_map = BTreeMap::new();
wat_map.insert(0x0010, "i32.const 0".to_string());
wat_map.insert(0x0011, "i32.const 1".to_string());
wat_map.insert(0x0012, "br 0".to_string());
wat_map.insert(0x0013, "i32.add".to_string());
wat_map.insert(0x0014, "return".to_string());
let target = target_lexicon::Triple::host();
let blocks = build_basic_blocks(
func_body,
func_offset,
&address_map,
&clif_lines,
&wat_map,
&target,
)
.unwrap();
assert_eq!(blocks.len(), 2, "expected 2 basic blocks");
assert_eq!(blocks[0].instructions.len(), 3, "block 0: nop, nop, jmp");
assert_eq!(blocks[1].instructions.len(), 2, "block 1: nop, ret");
assert_eq!(
blocks[0].instructions[0].clif.as_deref(),
Some("v1 = iconst.i32 0")
);
assert_eq!(blocks[1].instructions[1].clif.as_deref(), Some("return v3"));
assert_eq!(
blocks[0].instructions[0].wasm.as_deref(),
Some("i32.const 0")
);
assert_eq!(blocks[1].instructions[1].wasm.as_deref(), Some("return"));
assert_eq!(
find_block_for_offset(&blocks, FunctionOffset(0)),
Some(BlockIndex(0))
);
assert_eq!(
find_block_for_offset(&blocks, FunctionOffset(1)),
Some(BlockIndex(0))
);
assert_eq!(
find_block_for_offset(&blocks, FunctionOffset(2)),
Some(BlockIndex(0))
);
assert_eq!(
find_block_for_offset(&blocks, FunctionOffset(4)),
Some(BlockIndex(1))
);
assert_eq!(
find_block_for_offset(&blocks, FunctionOffset(5)),
Some(BlockIndex(1))
);
assert!(blocks[0].instructions[0].assembly.contains("nop"));
assert!(blocks[0].instructions[2].assembly.contains("jmp"));
}
#[test]
fn test_parse_perf_script() {
let input = "\
7f0001001000 wasm[0]::function[3]+0x0 (/tmp/perf-1234.map)
7f0001001005 wasm[0]::function[3]+0x5 (/tmp/perf-1234.map)
7f0001001000 wasm[0]::function[3]+0x0 (/tmp/perf-1234.map)
7f0001002000 some_native_func+0x10 (/usr/bin/wasmtime)
7f0001001010 wasm[0]::function[5]+0x10 (/tmp/perf-1234.map)
";
let (samples, total) = parse_perf_script(input);
assert_eq!(samples.len(), 4);
assert_eq!(total, 5);
assert_eq!(samples[0].symbol, "wasm[0]::function[3]");
assert_eq!(samples[0].offset, 0);
assert_eq!(samples[1].symbol, "wasm[0]::function[3]");
assert_eq!(samples[1].offset, 5);
assert_eq!(samples[2].symbol, "wasm[0]::function[3]");
assert_eq!(samples[2].offset, 0);
assert_eq!(samples[3].symbol, "wasm[0]::function[5]");
assert_eq!(samples[3].offset, 0x10);
}
#[test]
fn test_read_clif_file() {
let tmp = tempdir().unwrap();
let clif_content = "\
@0010 v1 = iconst.i32 0
@0011 v2 = iconst.i32 1
v3 = iadd v1, v2
@0012 return v3
";
std::fs::write(tmp.path().join("wasm[0]--function[0].clif"), clif_content).unwrap();
let lines = read_clif_file(
tmp.path(),
StaticModuleIndex::from_u32(0),
FuncIndex::from_u32(0),
None,
);
assert_eq!(lines.len(), 4);
assert_eq!(lines[0].0, Some(0x0010));
assert!(lines[0].1.contains("iconst.i32 0"));
assert_eq!(lines[2].0, None);
assert!(lines[2].1.contains("iadd"));
}
#[test]
fn test_wat_offset_map() {
let wat = r#"(module (func (param i32 i32) (result i32) local.get 0 local.get 1 i32.add))"#;
let wasm = wat::parse_str(wat).unwrap();
let map = build_wat_offset_map(&wasm);
let has_i32_add = map.values().any(|v| v.contains("i32.add"));
assert!(
has_i32_add,
"expected wat offset map to contain i32.add, got: {map:?}"
);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn test_ditto_marks() {
let func_body: &[u8] = &[
0x90, 0x90, 0x90, 0xc3, ];
let func_offset = 0usize;
let address_map = vec![
(func_offset, Some(0x0010u32)),
(func_offset + 1, Some(0x0010)),
(func_offset + 2, Some(0x0011)),
(func_offset + 3, Some(0x0011)),
];
let clif_lines = vec![
(Some(0x0010u32), "v1 = iconst.i32 42".to_string()),
(Some(0x0011u32), "return v1".to_string()),
];
let mut wat_map = BTreeMap::new();
wat_map.insert(0x0010, "i32.const 42".to_string());
wat_map.insert(0x0011, "return".to_string());
let target = target_lexicon::Triple::host();
let _blocks = build_basic_blocks(
func_body,
func_offset,
&address_map,
&clif_lines,
&wat_map,
&target,
)
.unwrap();
let samples = vec![
PerfSample {
symbol: "wasm[0]::function[0]".to_string(),
offset: 0,
},
PerfSample {
symbol: "wasm[0]::function[0]".to_string(),
offset: 1,
},
PerfSample {
symbol: "wasm[0]::function[0]".to_string(),
offset: 2,
},
PerfSample {
symbol: "wasm[0]::function[0]".to_string(),
offset: 3,
},
];
let functions = vec![ModuleFunction {
module: StaticModuleIndex::from_u32(0),
index: FuncIndex::from_u32(0),
name: Some("wasm[0]::function[0]::test".to_string()),
offset: 0,
len: func_body.len(),
}];
let cmd = HotBlocksCommand {
run: RunCommon {
common: wasmtime_cli_flags::CommonOptions::default(),
allow_precompiled: false,
profile: None,
dirs: Vec::new(),
vars: Vec::new(),
#[cfg(feature = "gdbstub")]
gdbstub: None,
},
percent: 100.0,
event: Event::CpuCycles,
frequency: None,
output: None,
module: PathBuf::from("dummy.wasm"),
module_args: Vec::new(),
};
let mut output = Vec::new();
cmd.format_hot_blocks(
&samples,
samples.len(),
&functions,
func_body,
&address_map,
Path::new("/nonexistent"),
&wat_map,
&target,
&mut output,
)
.unwrap();
let output_str = String::from_utf8(output).unwrap();
assert!(
output_str.contains('"'),
"expected ditto marks in output, got:\n{output_str}"
);
}
#[test]
fn test_find_function_binary_search() {
let functions = vec![
ModuleFunction {
module: StaticModuleIndex::from_u32(0),
index: FuncIndex::from_u32(0),
name: None,
offset: 0x100,
len: 0x50,
},
ModuleFunction {
module: StaticModuleIndex::from_u32(0),
index: FuncIndex::from_u32(1),
name: None,
offset: 0x150,
len: 0x30,
},
ModuleFunction {
module: StaticModuleIndex::from_u32(0),
index: FuncIndex::from_u32(3),
name: None,
offset: 0x200,
len: 0x40,
},
];
let sample = PerfSample {
symbol: "wasm[0]::function[1]".to_string(),
offset: 0x10,
};
let func = find_function_for_sample(&sample, &functions).unwrap();
assert_eq!(func.index, FuncIndex::from_u32(1));
let sample = PerfSample {
symbol: "wasm[0]::function[3]".to_string(),
offset: 0x5,
};
let func = find_function_for_sample(&sample, &functions).unwrap();
assert_eq!(func.index, FuncIndex::from_u32(3));
let sample = PerfSample {
symbol: "wasm[0]::function[99]".to_string(),
offset: 0,
};
assert!(find_function_for_sample(&sample, &functions).is_none());
}
}