Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ version = "0.14.2"
edition = "2024"
rust-version = "1.88"
description = "Automatic instrumentation-based profiler for Rust. Measures self-time, call counts, and heap allocations per function."
license = "MIT"
license = "GPL-3.0-only"
repository = "https://github.com/rocketman-code/piano"
keywords = ["profiling", "profiler", "instrumentation", "performance", "timing"]
categories = ["development-tools::profiling"]
Expand Down
699 changes: 685 additions & 14 deletions LICENSE

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# piano

[![Crates.io](https://img.shields.io/crates/v/piano.svg)](https://crates.io/crates/piano)
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![GPLv3 licensed](https://img.shields.io/badge/license-GPLv3-blue.svg)](LICENSE)

Automatic instrumentation-based profiler for Rust. Measures self-time, call counts, and heap allocations per function across sync, threaded, and async code.

Expand Down Expand Up @@ -160,4 +160,4 @@ The runtime has zero external dependencies to avoid conflicts with your project.

## License

MIT
GPLv3. See [LICENSE](LICENSE).
93 changes: 72 additions & 21 deletions src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ use toml_edit::DocumentMut;

use crate::error::{Error, io_context};

/// Which cargo target to build.
pub enum CargoTarget<'a> {
Bin(&'a str),
Example(&'a str),
}

// --- Cargo metadata types ---

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -55,38 +61,39 @@ pub fn cargo_metadata(project_dir: &Path) -> Result<CargoMetadata, Error> {
.map_err(|e| Error::BuildFailed(format!("failed to parse cargo metadata: {e}")))
}

/// Find a binary target in the metadata for the given package.
/// Find a target of the given kind in the metadata for the given package.
///
/// When `bin_name` is `Some`, looks for that specific binary target.
/// When `None`, returns the first binary target found.
/// `kind` is the cargo target kind: "bin" or "example".
/// When `name` is `Some`, looks for that specific target.
/// When `None`, returns the first target of that kind found.
///
/// Returns `(package_name, src_path)` where src_path is the absolute path
/// to the binary's entry point.
pub fn find_bin_target(
/// to the target's entry point.
pub fn find_target(
metadata: &CargoMetadata,
package_name: Option<&str>,
bin_name: Option<&str>,
name: Option<&str>,
kind: &str,
) -> Result<(String, PathBuf), Error> {
// Filter packages: if package_name given, match it; otherwise use all.
let candidates: Vec<&MetadataPackage> = if let Some(pkg) = package_name {
metadata.packages.iter().filter(|p| p.name == pkg).collect()
} else {
metadata.packages.iter().collect()
};

if candidates.is_empty() {
let name = package_name.unwrap_or("<any>");
let pkg = package_name.unwrap_or("<any>");
return Err(Error::BuildFailed(format!(
"no package '{name}' found in cargo metadata"
"no package '{pkg}' found in cargo metadata"
)));
}

for pkg in &candidates {
for target in &pkg.targets {
if !target.kind.iter().any(|k| k == "bin") {
if !target.kind.iter().any(|k| k == kind) {
continue;
}
if let Some(wanted) = bin_name {
if let Some(wanted) = name {
if target.name != wanted {
continue;
}
Expand All @@ -95,10 +102,10 @@ pub fn find_bin_target(
}
}

let bin_desc = bin_name.unwrap_or("default");
let target_desc = name.unwrap_or("default");
let pkg_desc = package_name.unwrap_or("<any>");
Err(Error::BuildFailed(format!(
"no binary target '{bin_desc}' found in package '{pkg_desc}'"
"no {kind} target '{target_desc}' found in package '{pkg_desc}'"
)))
}

Expand Down Expand Up @@ -732,7 +739,7 @@ pub fn build_instrumented(
project_dir: &Path,
target_dir: &Path,
package: Option<&str>,
bin: Option<&str>,
target: Option<CargoTarget<'_>>,
config_path: &Path,
modified_files: &std::collections::HashSet<PathBuf>,
) -> Result<PathBuf, Error> {
Expand All @@ -756,8 +763,14 @@ pub fn build_instrumented(
if let Some(pkg) = package {
cmd.arg("-p").arg(pkg);
}
if let Some(bin_name) = bin {
cmd.arg("--bin").arg(bin_name);
match target {
Some(CargoTarget::Bin(name)) => {
cmd.arg("--bin").arg(name);
}
Some(CargoTarget::Example(name)) => {
cmd.arg("--example").arg(name);
}
None => {}
}
let output = cmd.output()?;

Expand Down Expand Up @@ -936,7 +949,7 @@ edition = "2021"
}],
};

let (name, path) = find_bin_target(&metadata, None, None).unwrap();
let (name, path) = find_target(&metadata, None, None, "bin").unwrap();
assert_eq!(name, "demo");
assert_eq!(path, PathBuf::from("/project/src/main.rs"));
}
Expand All @@ -963,7 +976,7 @@ edition = "2021"
}],
};

let (name, path) = find_bin_target(&metadata, None, Some("worker")).unwrap();
let (name, path) = find_target(&metadata, None, Some("worker"), "bin").unwrap();
assert_eq!(name, "demo");
assert_eq!(path, PathBuf::from("/project/src/worker.rs"));
}
Expand All @@ -990,7 +1003,7 @@ edition = "2021"
}],
};

let (_, path) = find_bin_target(&metadata, None, None).unwrap();
let (_, path) = find_target(&metadata, None, None, "bin").unwrap();
assert_eq!(path, PathBuf::from("/project/src/main.rs"));
}

Expand All @@ -1009,7 +1022,7 @@ edition = "2021"
}],
};

let result = find_bin_target(&metadata, None, Some("nonexistent"));
let result = find_target(&metadata, None, Some("nonexistent"), "bin");
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
Expand Down Expand Up @@ -1044,11 +1057,49 @@ edition = "2021"
],
};

let (name, path) = find_bin_target(&metadata, Some("core"), None).unwrap();
let (name, path) = find_target(&metadata, Some("core"), None, "bin").unwrap();
assert_eq!(name, "core");
assert_eq!(path, PathBuf::from("/ws/crates/core/src/main.rs"));
}

#[test]
fn find_target_finds_example() {
let metadata = CargoMetadata {
workspace_root: PathBuf::from("/project"),
packages: vec![MetadataPackage {
name: "demo".to_string(),
manifest_path: PathBuf::from("/project/Cargo.toml"),
targets: vec![
MetadataTarget {
name: "demo".to_string(),
kind: vec!["lib".to_string()],
src_path: PathBuf::from("/project/src/lib.rs"),
},
MetadataTarget {
name: "demo".to_string(),
kind: vec!["bin".to_string()],
src_path: PathBuf::from("/project/src/main.rs"),
},
MetadataTarget {
name: "bench".to_string(),
kind: vec!["example".to_string()],
src_path: PathBuf::from("/project/examples/bench.rs"),
},
],
}],
};

let (name, path) = find_target(&metadata, None, Some("bench"), "example").unwrap();
assert_eq!(name, "demo");
assert_eq!(path, PathBuf::from("/project/examples/bench.rs"));

let result = find_target(&metadata, None, None, "example");
assert!(result.is_ok());

let result = find_target(&metadata, None, None, "bin");
assert_eq!(result.unwrap().1, PathBuf::from("/project/src/main.rs"));
}

#[test]
fn find_current_package_matches_dir() {
let tmp = TempDir::new().unwrap();
Expand Down
92 changes: 54 additions & 38 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use std::time::Duration;
use clap::{Parser, Subcommand};

use piano::build::{
build_instrumented, cargo_metadata, clean_stale_piano_files, find_bin_target,
find_current_package, find_project_root, prebuild_runtime, prebuild_runtime_from_path,
CargoTarget, build_instrumented, cargo_metadata, clean_stale_piano_files, find_current_package,
find_project_root, find_target, prebuild_runtime, prebuild_runtime_from_path,
};
use piano::error::{Error, io_context};
use piano::report::{
Expand Down Expand Up @@ -74,9 +74,13 @@ struct BuildOpts {

/// Build and profile a specific binary target (for projects with multiple
/// [[bin]] entries). Matches cargo's --bin flag.
#[arg(long)]
#[arg(long, conflicts_with = "example")]
bin: Option<String>,

/// Build and profile an example target. Matches cargo's --example flag.
#[arg(long, conflicts_with = "bin")]
example: Option<String>,

/// Capture per-thread CPU time alongside wall time (Unix only).
#[arg(long)]
cpu_time: bool,
Expand Down Expand Up @@ -424,6 +428,7 @@ fn build_project(
project,
runtime_path,
bin,
example,
cpu_time,
output_dir,
list_skipped,
Expand Down Expand Up @@ -471,25 +476,25 @@ fn build_project(
))
})?;

// Find the target package and binary.
// If the user specified --bin, look for it. Otherwise find the current
// package from the project directory.
let (package_name, bin_src_path) = if bin.is_some() || metadata.packages.len() == 1 {
// Find the target package and binary/example.
let target_kind = if example.is_some() { "example" } else { "bin" };
let target_name = example.as_deref().or(bin.as_deref());
let (package_name, bin_src_path) = if target_name.is_some() || metadata.packages.len() == 1 {
let pkg_filter = if metadata.packages.len() == 1 {
None
} else {
find_current_package(&metadata, &project).map(|p| p.name.as_str())
};
find_bin_target(&metadata, pkg_filter, bin.as_deref())?
find_target(&metadata, pkg_filter, target_name, target_kind)?
} else {
// Multiple packages, no --bin: find the package matching project dir.
// Multiple packages, no --bin/--example: find the package matching project dir.
let pkg = find_current_package(&metadata, &project).ok_or_else(|| {
Error::BuildFailed(format!(
"could not determine which package to build in workspace at {}",
workspace_root.display()
))
})?;
find_bin_target(&metadata, Some(&pkg.name), None)?
find_target(&metadata, Some(&pkg.name), None, "bin")?
};

// Derive source directory from the binary's src_path.
Expand Down Expand Up @@ -752,11 +757,16 @@ fn build_project(
} else {
None
};
let cargo_target = if let Some(ref ex) = example {
Some(CargoTarget::Example(ex.as_str()))
} else {
bin.as_deref().map(CargoTarget::Bin)
};
let binary = build_instrumented(
&workspace_root,
&target_dir,
pkg_arg,
bin.as_deref(),
cargo_target,
&config_path,
&modified_files,
)?;
Expand Down Expand Up @@ -789,39 +799,45 @@ fn is_binary_extension(ext: &std::ffi::OsStr) -> bool {

fn find_latest_binary(project_root: &Option<PathBuf>) -> Result<PathBuf, Error> {
let project = project_root.as_ref().ok_or(Error::NoBinary)?;
let dir = project.join("target/piano/release");
if !dir.is_dir() {
let release_dir = project.join("target/piano/release");
if !release_dir.is_dir() {
return Err(Error::NoBinary);
}
let mut dirs = vec![release_dir.clone()];
let examples_dir = release_dir.join("examples");
if examples_dir.is_dir() {
dirs.push(examples_dir);
}
let mut best: Option<(PathBuf, std::time::SystemTime)> = None;
for entry in std::fs::read_dir(&dir).map_err(io_context("read directory", &dir))? {
let entry = entry.map_err(io_context("read directory entry", &dir))?;
let path = entry.path();
if !path.is_file() {
continue;
}
// Skip non-binary files by extension. On Unix, binaries have no extension.
// On Windows, binaries have .exe extension -- allow those through.
if let Some(ext) = path.extension() {
if !is_binary_extension(ext) {
for dir in &dirs {
let entries = std::fs::read_dir(dir).map_err(io_context("read directory", dir))?;
for entry in entries {
let entry = entry.map_err(io_context("read directory entry", dir))?;
let path = entry.path();
if !path.is_file() {
continue;
}
}
let meta = entry
.metadata()
.map_err(io_context("read metadata", &path))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if meta.permissions().mode() & 0o111 == 0 {
continue; // not executable
if let Some(ext) = path.extension() {
if !is_binary_extension(ext) {
continue;
}
}
let meta = entry
.metadata()
.map_err(io_context("read metadata", &path))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if meta.permissions().mode() & 0o111 == 0 {
continue;
}
}
let mtime = meta
.modified()
.map_err(io_context("read modified time", &path))?;
if best.as_ref().is_none_or(|(_, t)| mtime > *t) {
best = Some((path, mtime));
}
}
let mtime = meta
.modified()
.map_err(io_context("read modified time", &path))?;
if best.as_ref().is_none_or(|(_, t)| mtime > *t) {
best = Some((path, mtime));
}
}
best.map(|(p, _)| p).ok_or(Error::NoBinary)
Expand Down
Loading
Loading