use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context, Result, bail};
use clap::Parser;
#[cfg(target_os = "linux")]
use super::which;
use super::{DEFAULT_BASE_ADDRESS, parse_hex_or_dec};
#[derive(Parser, Debug)]
pub struct RecordArgs {
pub guest_binary: PathBuf,
#[arg(last = true, required = true, num_args = 1..)]
pub workload: Vec<OsString>,
#[arg(long)]
pub freq: Option<u32>,
#[arg(
short,
long,
default_value = "perf.data.guest",
default_value_if("host", "true", "perf.data.kvm")
)]
pub output: PathBuf,
#[arg(long)]
pub host: bool,
#[arg(long, default_value = "0x1000", value_parser = parse_hex_or_dec)]
pub base_address: u64,
}
pub fn run(args: RecordArgs) -> Result<()> {
check_prerequisites()?;
let kallsyms_file = super::prepare_kallsyms(&args.guest_binary, args.base_address)?;
let mode_label = if args.host {
"host+guest"
} else {
"guest-only"
};
if let Some(freq) = args.freq {
eprintln!(
"Recording {mode_label} cycles @ {freq} Hz -> {}",
args.output.display()
);
} else {
eprintln!("Recording {mode_label} cycles -> {}", args.output.display());
}
eprintln!(
"Workload: {}",
args.workload
.iter()
.map(|a| a.to_string_lossy())
.collect::<Vec<_>>()
.join(" ")
);
eprintln!();
record_perf(&args, &args.output, kallsyms_file.path())?;
eprintln!();
eprintln!("Data saved to {}", args.output.display());
let mut report_cmd = format!(
"cargo hyperlight perf report {}",
args.guest_binary.display()
);
report_cmd.push_str(&format!(" -i {}", args.output.display()));
if args.host {
report_cmd.push_str(" --host");
}
if args.base_address != DEFAULT_BASE_ADDRESS {
report_cmd.push_str(&format!(" --base-address {:#x}", args.base_address));
}
eprintln!("To view the profile, run:\n {report_cmd}");
Ok(())
}
fn record_perf(args: &RecordArgs, output: &Path, kallsyms: &std::path::Path) -> Result<()> {
let mut perf_args = super::perf_kvm_args(args.host, kallsyms);
perf_args.extend([
"record".into(),
"-e".into(), "cycles".into(), ]);
if let Some(freq) = args.freq {
perf_args.push("-F".into());
perf_args.push(freq.to_string().into());
}
perf_args.push("-o".into());
perf_args.push(output.as_os_str().to_owned());
perf_args.push("--".into());
perf_args.extend(args.workload.iter().cloned());
let status = Command::new("perf")
.args(&perf_args)
.status()
.context("Failed to execute perf")?;
if let Some(code) = status.code()
&& code != 0
{
eprintln!("Warning: perf kvm record exited with status {code} (workload may have failed)");
}
Ok(())
}
fn check_prerequisites() -> Result<()> {
#[cfg(not(target_os = "linux"))]
{
bail!("cargo hyperlight perf requires Linux with KVM");
}
#[cfg(target_os = "linux")]
{
use std::fs;
which("perf").context("perf not found (install linux-perf / perf-tools / linux-tools)")?;
let kvm = std::path::Path::new("/dev/kvm");
if !kvm.exists() {
bail!("No KVM device found at /dev/kvm");
}
if let Ok(val) = fs::read_to_string("/proc/sys/kernel/perf_event_paranoid")
&& let Ok(n) = val.trim().parse::<i32>()
&& n > 1
{
eprintln!(
"Warning: perf_event_paranoid={n} (need <=1). Run: sudo sysctl kernel.perf_event_paranoid=-1"
);
}
if let Ok(cpuinfo) = fs::read_to_string("/proc/cpuinfo")
&& cpuinfo
.lines()
.any(|l| l.starts_with("flags") && l.contains(" hypervisor"))
{
eprintln!(
"Warning: running inside a VM (hypervisor CPU flag detected). \
The virtualized PMU may not support KVM guest profiling — \
you may get zero guest samples. For reliable results, run \
on bare-metal hardware."
);
}
const PRE_ICELAKE_PMUS: &[&str] = &[
"nehalem",
"westmere",
"sandybridge",
"ivybridge",
"haswell",
"broadwell",
"skylake",
"knl", ];
if let Ok(pmu_name) = fs::read_to_string("/sys/bus/event_source/devices/cpu/caps/pmu_name")
{
let pmu = pmu_name.trim();
if PRE_ICELAKE_PMUS.contains(&pmu) {
eprintln!(
"Warning: CPU PMU is '{pmu}' (pre-Ice Lake). Guest PEBS is not \
available — guest IP samples will have significant NMI skid, \
making function-level attribution unreliable."
);
}
}
Ok(())
}
}