use std::path::{Path, PathBuf};
use anyhow::{anyhow, Context};
use cargo_metadata::{Artifact, ArtifactDebuginfo, Message, MetadataCommand, Package, TargetKind};
use clap::{Args, Parser};
use flamegraph::Workload;
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
#[clap(rename_all = "snake_case")]
enum UnitTestTargetKind {
Bin,
Lib,
}
#[derive(Args, Debug)]
struct Opt {
#[clap(long)]
dev: bool,
#[clap(long)]
profile: Option<String>,
#[clap(short, long)]
package: Option<String>,
#[clap(short, long, group = "exec-args")]
bin: Option<String>,
#[clap(long, group = "exec-args")]
target: Option<String>,
#[clap(long, group = "exec-args")]
example: Option<String>,
#[clap(long, group = "exec-args")]
test: Option<String>,
#[clap(long, group = "exec-args")]
unit_test: Option<Option<String>>,
#[clap(long)]
unit_test_kind: Option<UnitTestTargetKind>,
#[clap(long, group = "exec-args")]
unit_bench: Option<Option<String>>,
#[clap(long, group = "exec-args")]
bench: Option<String>,
#[clap(long)]
manifest_path: Option<PathBuf>,
#[clap(short, long)]
features: Option<String>,
#[clap(long)]
no_default_features: bool,
#[clap(short, long)]
release: bool,
#[clap(flatten)]
graph: flamegraph::Options,
#[clap(last = true)]
trailing_arguments: Vec<String>,
}
#[derive(Parser, Debug)]
#[clap(bin_name = "cargo")]
enum Cli {
#[clap(version)]
Flamegraph(Opt),
}
fn build(opt: &Opt, kind: Vec<TargetKind>) -> anyhow::Result<Vec<Artifact>> {
use std::process::{Command, Output, Stdio};
let mut cmd = Command::new("cargo");
if !opt.dev && (opt.bench.is_some() || opt.unit_bench.is_some()) {
cmd.args(["bench", "--no-run"]);
} else if opt.unit_test.is_some() {
cmd.args(["test", "--no-run"]);
} else {
cmd.arg("build");
}
if let Some(profile) = &opt.profile {
cmd.arg("--profile").arg(profile);
} else if !opt.dev && opt.bench.is_none() && opt.unit_bench.is_none() {
cmd.arg("--release");
}
if let Some(ref package) = opt.package {
cmd.arg("--package");
cmd.arg(package);
}
if let Some(ref bin) = opt.bin {
cmd.arg("--bin");
cmd.arg(bin);
}
if let Some(ref target) = opt.target {
cmd.arg("--target");
cmd.arg(target);
}
if let Some(ref example) = opt.example {
cmd.arg("--example");
cmd.arg(example);
}
if let Some(ref test) = opt.test {
cmd.arg("--test");
cmd.arg(test);
}
if let Some(ref bench) = opt.bench {
cmd.arg("--bench");
cmd.arg(bench);
}
if let Some(Some(ref unit_test)) = opt.unit_test {
match kind
.iter()
.any(|k| matches!(k, TargetKind::Lib | TargetKind::RLib))
{
true => cmd.arg("--lib"),
false => cmd.args(["--bin", unit_test]),
};
}
if let Some(Some(ref unit_bench)) = opt.unit_bench {
match kind
.iter()
.any(|k| matches!(k, TargetKind::Lib | TargetKind::RLib))
{
true => cmd.arg("--lib"),
false => cmd.args(["--bin", unit_bench]),
};
}
if let Some(ref manifest_path) = opt.manifest_path {
cmd.arg("--manifest-path");
cmd.arg(manifest_path);
}
if let Some(ref features) = opt.features {
cmd.arg("--features");
cmd.arg(features);
}
if opt.no_default_features {
cmd.arg("--no-default-features");
}
cmd.arg("--message-format=json-render-diagnostics");
if opt.graph.verbose {
println!("build command: {:?}", cmd);
}
let Output { status, stdout, .. } = cmd
.stderr(Stdio::inherit())
.output()
.context("failed to execute cargo build command")?;
if !status.success() {
return Err(anyhow!("cargo build failed"));
}
Message::parse_stream(&*stdout)
.filter_map(|m| match m {
Ok(Message::CompilerArtifact(artifact)) => Some(Ok(artifact)),
Ok(_) => None,
Err(e) => Some(Err(e).context("failed to parse cargo build output")),
})
.collect()
}
fn workload(opt: &Opt, artifacts: &[Artifact]) -> anyhow::Result<Vec<String>> {
let mut trailing_arguments = opt.trailing_arguments.clone();
if artifacts.iter().all(|a| a.executable.is_none()) {
return Err(anyhow!(
"build artifacts do not contain any executable to profile"
));
}
let (kind, target): (&[TargetKind], _) = match opt {
Opt { bin: Some(t), .. } => (&[TargetKind::Bin], t),
Opt {
example: Some(t), ..
} => (&[TargetKind::Example], t),
Opt { test: Some(t), .. } => (&[TargetKind::Test], t),
Opt { bench: Some(t), .. } => (&[TargetKind::Bench], t),
Opt {
unit_test: Some(Some(t)),
..
} => (&[TargetKind::Lib, TargetKind::RLib, TargetKind::Bin], t),
Opt {
unit_bench: Some(Some(t)),
..
} => {
trailing_arguments.push("--bench".to_string());
(&[TargetKind::Lib, TargetKind::RLib, TargetKind::Bin], t)
}
_ => return Err(anyhow!("no target for profiling")),
};
let (debug_level, binary_path) = artifacts
.iter()
.find_map(|a| {
a.executable
.as_deref()
.filter(|_| {
a.target.name == *target && a.target.kind.iter().any(|k| kind.contains(k))
})
.map(|e| (&a.profile.debuginfo, e))
})
.ok_or_else(|| {
let targets: Vec<_> = artifacts
.iter()
.map(|a| (&a.target.kind, &a.target.name))
.collect();
anyhow!(
"could not find desired target {:?} in the targets for this crate: {:?}",
(kind, target),
targets
)
})?;
if !opt.dev && debug_level == &ArtifactDebuginfo::None {
let profile = match opt
.example
.as_ref()
.or(opt.bin.as_ref())
.or_else(|| opt.unit_test.as_ref().unwrap_or(&None).as_ref())
{
Some(_) => "release",
_ => "bench",
};
eprintln!("\nWARNING: profiling without debuginfo. Enable symbol information by adding the following lines to Cargo.toml:\n");
eprintln!("[profile.{}]", profile);
eprintln!("debug = true\n");
eprintln!("Or set this environment variable:\n");
eprintln!("CARGO_PROFILE_{}_DEBUG=true\n", profile.to_uppercase());
}
let mut command = Vec::with_capacity(1 + trailing_arguments.len());
command.push(binary_path.to_string());
command.extend(trailing_arguments);
Ok(command)
}
#[derive(Clone, Debug)]
struct BinaryTarget {
package: String,
target: String,
kind: Vec<TargetKind>,
}
impl std::fmt::Display for BinaryTarget {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "target {} in package {}", self.target, self.package)
}
}
pub fn find_crate_root(manifest_path: Option<&Path>) -> anyhow::Result<PathBuf> {
match manifest_path {
Some(path) => {
let path = path.parent().ok_or_else(|| {
anyhow!(
"the manifest path '{}' must point to a Cargo.toml file",
path.display()
)
})?;
path.canonicalize().with_context(|| {
anyhow!(
"failed to canonicalize manifest parent directory '{}'\nHint: make sure your manifest path exists and points to a Cargo.toml file",
path.display()
)
})
}
None => {
let cargo_toml = "Cargo.toml";
let cwd = std::env::current_dir().context("failed to determine working directory")?;
for current in cwd.ancestors() {
if current.join(cargo_toml).exists() {
return Ok(current.to_path_buf());
}
}
Err(anyhow!(
"could not find '{}' in '{}' or any parent directory",
cargo_toml,
cwd.display()
))
}
}
}
fn find_unique_target(
kind: &[TargetKind],
pkg: Option<&str>,
manifest_path: Option<&Path>,
target_name: Option<&str>,
) -> anyhow::Result<BinaryTarget> {
let mut metadata_command = MetadataCommand::new();
metadata_command.no_deps();
if let Some(ref manifest_path) = manifest_path {
metadata_command.manifest_path(manifest_path);
}
let crate_root = find_crate_root(manifest_path)?;
let mut packages = metadata_command
.exec()
.context("failed to access crate metadata")?
.packages
.into_iter()
.filter(|p| match pkg {
Some(pkg) => pkg == *p.name,
None => p.manifest_path.starts_with(&crate_root),
})
.peekable();
if packages.peek().is_none() {
return Err(match pkg {
Some(pkg) => anyhow!("workspace has no package named {}", pkg),
None => anyhow!(
"failed to find any package in '{}' or below",
crate_root.display()
),
});
}
let mut num_packages = 0;
let mut is_default = false;
let mut targets: Vec<_> = packages
.flat_map(|p| {
let Package {
targets,
name,
default_run,
..
} = p;
num_packages += 1;
if default_run.is_some() {
is_default = true;
}
targets.into_iter().filter_map(move |t| {
if !t.kind.iter().any(|s| kind.contains(s)) {
return None;
}
match &default_run {
Some(name) if name != &t.name => return None,
_ => {}
}
match target_name {
Some(name) if name != t.name => return None,
_ => {}
}
Some(BinaryTarget {
package: name.as_ref().to_owned(),
target: t.name,
kind: t.kind,
})
})
})
.collect();
match targets.as_slice() {
[_] => {
let target = targets.remove(0);
if num_packages != 1 || !is_default {
eprintln!(
"automatically selected {} as it is the only valid target",
target
);
}
Ok(target)
}
[] => Err(anyhow!(
"crate has no automatically selectable target:\nHint: try passing `--example <example>` \
or similar to choose a binary"
)),
_ => Err(anyhow!(
"several possible targets found: {:#?}, please pass an explicit target.",
targets
)),
}
}
fn main() -> anyhow::Result<()> {
let Cli::Flamegraph(mut opt) = Cli::parse();
opt.graph.check()?;
let kind = if opt.bin.is_none()
&& opt.bench.is_none()
&& opt.example.is_none()
&& opt.test.is_none()
&& opt.unit_test.is_none()
&& opt.unit_bench.is_none()
{
let target = find_unique_target(
&[TargetKind::Bin],
opt.package.as_deref(),
opt.manifest_path.as_deref(),
None,
)?;
opt.bin = Some(target.target);
opt.package = Some(target.package);
target.kind
} else if let Some(unit_test) = opt.unit_test {
let kinds = match opt.unit_test_kind {
Some(UnitTestTargetKind::Bin) => &[TargetKind::Bin][..], Some(UnitTestTargetKind::Lib) => &[TargetKind::Lib, TargetKind::RLib],
None => &[TargetKind::Bin, TargetKind::Lib, TargetKind::RLib],
};
let target = find_unique_target(
kinds,
opt.package.as_deref(),
opt.manifest_path.as_deref(),
unit_test.as_deref(),
)?;
opt.unit_test = Some(Some(target.target));
opt.package = Some(target.package);
target.kind
} else if let Some(unit_bench) = opt.unit_bench {
let kinds = match opt.unit_test_kind {
Some(UnitTestTargetKind::Bin) => &[TargetKind::Bin][..],
Some(UnitTestTargetKind::Lib) => &[TargetKind::Lib, TargetKind::RLib],
None => &[TargetKind::Bin, TargetKind::Lib, TargetKind::RLib],
};
let target = find_unique_target(
kinds,
opt.package.as_deref(),
opt.manifest_path.as_deref(),
unit_bench.as_deref(),
)?;
opt.unit_bench = Some(Some(target.target));
opt.package = Some(target.package);
target.kind
} else {
Vec::new()
};
let artifacts = build(&opt, kind)?;
let workload = workload(&opt, &artifacts)?;
flamegraph::generate_flamegraph_for_workload(Workload::Command(workload), opt.graph)
}