use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::thread;
use anyhow::{Context, Result, bail};
use clap::Parser;
use regex::Regex;
use super::parse_hex_or_dec;
#[cfg(target_os = "linux")]
use super::which;
#[derive(Parser, Debug)]
pub struct ReportArgs {
pub guest_binary: PathBuf,
#[arg(
short,
long,
default_value = "perf.data.guest",
default_value_if("host", "true", "perf.data.kvm")
)]
pub input: PathBuf,
#[arg(long)]
pub host: bool,
#[arg(long, requires = "host")]
pub group: bool,
#[arg(long, default_value = "0x1000", value_parser = parse_hex_or_dec)]
pub base_address: u64,
}
pub fn run(args: ReportArgs) -> Result<()> {
check_prerequisites()?;
let kallsyms_file = super::prepare_kallsyms(&args.guest_binary, args.base_address)?;
report_perf(&args, kallsyms_file.path())?;
Ok(())
}
fn report_perf(args: &ReportArgs, kallsyms: &std::path::Path) -> Result<()> {
if args.host {
eprintln!("Host + guest profile:");
if !args.group {
eprintln!(" [g] = guest VM [k] = host kernel [.] = host userspace");
}
} else {
eprintln!("Guest profile:");
}
let mut perf_args = super::perf_kvm_args(args.host, kallsyms);
perf_args.extend([
"report".into(),
"--stdio".into(),
"--no-children".into(),
"-i".into(),
args.input.as_os_str().to_owned(),
]);
perf_args.extend(["-F".into(), "overhead,sym".into()]);
let mut child = Command::new("perf")
.args(&perf_args)
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.context("Failed to execute perf report")?;
let stdout = child.stdout.take().expect("stdout piped");
let mut stderr = child.stderr.take().expect("stderr piped");
let stderr_handle = thread::spawn(move || {
let mut buf = String::new();
std::io::Read::read_to_string(&mut stderr, &mut buf).ok();
buf
});
let events_result = collect_report_events(stdout);
let stderr_output = stderr_handle.join().unwrap_or_default();
let status = child.wait().context("Failed to wait for perf report")?;
let events = events_result?;
if !status.success() {
let stderr_msg = stderr_output.trim();
if !stderr_msg.is_empty() {
eprintln!("perf report stderr: {stderr_msg}");
}
if !args.host {
eprintln!(
"Hint: if the data was recorded with --host, you must also pass --host to report."
);
}
bail!("perf report exited with status {status}");
}
let demangle_re = Regex::new(r"_(?:ZN|R)\S+").unwrap();
for event in &events {
if events.len() > 1 {
eprintln!("\n {} ({} samples):", event.name, event.sample_count);
}
let lines: Vec<String> = event
.lines
.iter()
.map(|l| demangle_line(l, &demangle_re))
.collect();
if args.group {
print_grouped(&lines);
} else if args.host {
for line in &lines {
println!("{}", line.trim_end());
}
} else {
for line in &lines {
println!("{}", line.replacen("[g] ", "", 1).trim_end());
}
}
}
Ok(())
}
fn demangle_line(line: &str, re: &Regex) -> String {
re.replace_all(line, |caps: ®ex::Captures| {
let mangled = &caps[0];
let demangled = rustc_demangle::demangle(mangled);
format!("{demangled:#}")
})
.into_owned()
}
struct ReportEvent {
name: String,
sample_count: String,
lines: Vec<String>,
}
fn collect_report_events(stdout: impl std::io::Read) -> Result<Vec<ReportEvent>> {
let mut events: Vec<ReportEvent> = Vec::new();
let mut current_name = String::from("cycles");
let mut current_samples = String::new();
let mut current_lines: Vec<String> = Vec::new();
for line in BufReader::new(stdout).lines() {
let line = line.context("Failed to read perf output")?;
if let Some(rest) = line.strip_prefix("# Samples:") {
if !current_lines.is_empty() {
events.push(ReportEvent {
name: current_name.clone(),
sample_count: current_samples.clone(),
lines: std::mem::take(&mut current_lines),
});
}
let rest = rest.trim();
if let Some(of_pos) = rest.find(" of event '") {
current_samples = rest[..of_pos].trim().to_string();
let event_start = of_pos + " of event '".len();
current_name = rest[event_start..].trim_end_matches('\'').to_string();
} else {
current_samples.clear();
current_name = "unknown".to_string();
}
continue;
}
if line.starts_with('#') || line.is_empty() {
continue;
}
current_lines.push(line);
}
if !current_lines.is_empty() {
events.push(ReportEvent {
name: current_name,
sample_count: current_samples,
lines: current_lines,
});
}
Ok(events)
}
fn print_grouped(lines: &[String]) {
let (mut guest, mut kernel, mut user) = (Vec::new(), Vec::new(), Vec::new());
for line in lines {
if line.contains("[g]") {
guest.push(line);
} else if line.contains("[k]") {
kernel.push(line);
} else {
user.push(line);
}
}
for (header, group) in [
("Guest VM", &guest),
("Host kernel", &kernel),
("Host userspace", &user),
] {
if group.is_empty() {
continue;
}
println!("\n {header}:");
for line in group {
println!("{}", line.trim_end());
}
}
}
fn check_prerequisites() -> Result<()> {
#[cfg(not(target_os = "linux"))]
{
bail!("cargo hyperlight perf requires Linux");
}
#[cfg(target_os = "linux")]
{
which("perf").context("perf not found (install linux-perf / perf-tools / linux-tools)")?;
Ok(())
}
}