Skip to content

Commit 1e885ff

Browse files
art049adriencaccia
andcommitted
feat(cargo-codspeed): support walltime runs from with cargo-codspeed
Co-authored-by: Adrien Cacciaguerra <adrien.caccia@gmail.com>
1 parent 21efe25 commit 1e885ff

File tree

8 files changed

+538
-23
lines changed

8 files changed

+538
-23
lines changed

Cargo.lock

Lines changed: 251 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cargo-codspeed/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,16 @@ keywords = ["codspeed", "benchmark", "cargo"]
1919

2020
[dependencies]
2121
cargo_metadata = "0.19.1"
22-
clap = { version = "=4.5.17", features = ["derive"] }
22+
clap = { version = "=4.5.17", features = ["derive", "env"] }
2323
termcolor = "1.4"
2424
anyhow = "1.0.86"
2525
itertools = "0.13.0"
2626
anstyle = "1.0.8"
27+
serde = { workspace = true }
28+
serde_json = { workspace = true }
29+
codspeed = { path = "../codspeed", version = "=2.7.2" }
30+
glob = "0.3.2"
31+
statrs = "0.18.0"
2732

2833
[dev-dependencies]
2934
assert_cmd = "2.0.15"

crates/cargo-codspeed/src/app.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{prelude::*, run::run_benches};
1+
use crate::{measurement_mode::MeasurementMode, prelude::*, run::run_benches};
22
use cargo_metadata::MetadataCommand;
33
use clap::{Args, Parser, Subcommand};
44
use std::{ffi::OsString, process::exit};
@@ -12,6 +12,11 @@ struct Cli {
1212
#[arg(short, long, global = true)]
1313
quiet: bool,
1414

15+
/// The measurement tool to use for measuring performance
16+
/// Automatically set to `walltime` on macro runners
17+
#[arg(short, long, global = true, env = "CODSPEED_RUNNER_MODE")]
18+
measurement_mode: Option<MeasurementMode>,
19+
1520
#[command(subcommand)]
1621
command: Commands,
1722
}
@@ -63,6 +68,9 @@ pub fn run(args: impl Iterator<Item = OsString>) -> Result<()> {
6368
let metadata = MetadataCommand::new().exec()?;
6469
let cli = Cli::try_parse_from(args)?;
6570

71+
let measurement_mode = cli.measurement_mode.unwrap_or_default();
72+
eprintln!("[cargo-codspeed] Measurement mode: {measurement_mode:?}\n");
73+
6674
let res = match cli.command {
6775
Commands::Build {
6876
filters,
@@ -71,9 +79,16 @@ pub fn run(args: impl Iterator<Item = OsString>) -> Result<()> {
7179
} => {
7280
let features =
7381
features.map(|f| f.split([' ', ',']).map(|s| s.to_string()).collect_vec());
74-
build_benches(&metadata, filters, features, profile, cli.quiet)
82+
build_benches(
83+
&metadata,
84+
filters,
85+
features,
86+
profile,
87+
cli.quiet,
88+
measurement_mode,
89+
)
7590
}
76-
Commands::Run { filters } => run_benches(&metadata, filters),
91+
Commands::Run { filters } => run_benches(&metadata, filters, measurement_mode),
7792
};
7893

7994
if let Err(e) = res {

crates/cargo-codspeed/src/build.rs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::{
22
app::{Filters, PackageFilters},
33
helpers::{clear_dir, get_codspeed_target_dir},
4+
measurement_mode::MeasurementMode,
45
prelude::*,
56
};
67
use cargo_metadata::{camino::Utf8PathBuf, Message, Metadata, TargetKind};
@@ -21,10 +22,15 @@ struct BuiltBench {
2122
impl BuildOptions<'_> {
2223
/// Builds the benchmarks by invoking cargo
2324
/// Returns a list of built benchmarks, with path to associated executables
24-
fn build(&self, metadata: &Metadata, quiet: bool) -> Result<Vec<BuiltBench>> {
25+
fn build(
26+
&self,
27+
metadata: &Metadata,
28+
quiet: bool,
29+
measurement_mode: MeasurementMode,
30+
) -> Result<Vec<BuiltBench>> {
2531
let workspace_packages = metadata.workspace_packages();
2632

27-
let mut cargo = self.build_command();
33+
let mut cargo = self.build_command(measurement_mode);
2834
if quiet {
2935
cargo.arg("--quiet");
3036
}
@@ -98,18 +104,20 @@ impl BuildOptions<'_> {
98104
}
99105

100106
/// Generates a subcommand to build the benchmarks by invoking cargo and forwarding the filters
101-
/// This command explicitely ignores the `self.benches`: all benches are built
102-
fn build_command(&self) -> Command {
107+
/// This command explicitly ignores the `self.benches`: all benches are built
108+
fn build_command(&self, measurement_mode: MeasurementMode) -> Command {
103109
let mut cargo = Command::new("cargo");
104110
cargo.args(["build", "--benches"]);
105111

106-
cargo.env(
107-
"RUSTFLAGS",
108-
format!(
109-
"{} -g --cfg codspeed",
110-
std::env::var("RUSTFLAGS").unwrap_or_else(|_| "".into())
111-
),
112-
);
112+
let mut rust_flags = std::env::var("RUSTFLAGS").unwrap_or_else(|_| "".into());
113+
// Add debug info (equivalent to -g)
114+
rust_flags.push_str(" -C debuginfo=2");
115+
116+
// Add the codspeed cfg flag if instrumentation mode is enabled
117+
if measurement_mode == MeasurementMode::Instrumentation {
118+
rust_flags.push_str(" --cfg codspeed");
119+
}
120+
cargo.env("RUSTFLAGS", rust_flags);
113121

114122
if let Some(features) = self.features {
115123
cargo.arg("--features").arg(features.join(","));
@@ -149,13 +157,14 @@ pub fn build_benches(
149157
features: Option<Vec<String>>,
150158
profile: String,
151159
quiet: bool,
160+
measurement_mode: MeasurementMode,
152161
) -> Result<()> {
153162
let built_benches = BuildOptions {
154163
filters,
155164
features: &features,
156165
profile: &profile,
157166
}
158-
.build(metadata, quiet)?;
167+
.build(metadata, quiet, measurement_mode)?;
159168

160169
if built_benches.is_empty() {
161170
bail!(

crates/cargo-codspeed/src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
mod app;
22
mod build;
33
mod helpers;
4+
mod measurement_mode;
45
mod prelude;
56
mod run;
7+
mod walltime_results;
68

79
use crate::prelude::*;
810
use std::{env::args_os, process::exit};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use clap::ValueEnum;
2+
use serde::Serialize;
3+
use std::env;
4+
5+
#[derive(Debug, Clone, ValueEnum, Serialize, PartialEq, Eq)]
6+
#[serde(rename_all = "lowercase")]
7+
pub enum MeasurementMode {
8+
Walltime,
9+
Instrumentation,
10+
}
11+
12+
impl Default for MeasurementMode {
13+
fn default() -> Self {
14+
if env::var("CODSPEED_ENV").is_ok() {
15+
MeasurementMode::Instrumentation
16+
} else {
17+
MeasurementMode::Walltime
18+
}
19+
}
20+
}

crates/cargo-codspeed/src/run.rs

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
use crate::{
22
app::{Filters, PackageFilters},
33
helpers::get_codspeed_target_dir,
4+
measurement_mode::MeasurementMode,
45
prelude::*,
6+
walltime_results::{WalltimeBenchmark, WalltimeResults},
57
};
8+
use anyhow::Context;
69
use cargo_metadata::{Metadata, Package};
7-
use std::{io, path::PathBuf};
10+
use codspeed::walltime::get_raw_result_dir_from_workspace_root;
11+
use glob::glob;
12+
use std::{
13+
io::{self, Write},
14+
path::{Path, PathBuf},
15+
};
816

917
struct BenchToRun {
1018
bench_path: PathBuf,
@@ -86,8 +94,16 @@ impl PackageFilters {
8694
}
8795
}
8896

89-
pub fn run_benches(metadata: &Metadata, filters: Filters) -> Result<()> {
97+
pub fn run_benches(
98+
metadata: &Metadata,
99+
filters: Filters,
100+
measurement_mode: MeasurementMode,
101+
) -> Result<()> {
90102
let codspeed_target_dir = get_codspeed_target_dir(metadata);
103+
let workspace_root = metadata.workspace_root.as_std_path();
104+
if measurement_mode == MeasurementMode::Walltime {
105+
clear_raw_walltime_data(workspace_root)?;
106+
}
91107
let benches = filters.benches_to_run(codspeed_target_dir, metadata)?;
92108
if benches.is_empty() {
93109
bail!("No benchmarks found. Run `cargo codspeed build` first.");
@@ -125,9 +141,16 @@ pub fn run_benches(metadata: &Metadata, filters: Filters) -> Result<()> {
125141
// while CARGO_MANIFEST_DIR returns the path to the sub package
126142
let workspace_root = metadata.workspace_root.clone();
127143
eprintln!("Running {} {}", &bench.package_name, bench_name);
128-
std::process::Command::new(&bench.bench_path)
144+
let mut command = std::process::Command::new(&bench.bench_path);
145+
command
129146
.env("CODSPEED_CARGO_WORKSPACE_ROOT", workspace_root)
130-
.current_dir(&bench.working_directory)
147+
.current_dir(&bench.working_directory);
148+
149+
if measurement_mode == MeasurementMode::Walltime {
150+
command.arg("--bench"); // Walltime targets need this additional argument (inherited from running them with `cargo bench`)
151+
}
152+
153+
command
131154
.status()
132155
.map_err(|e| anyhow!("failed to execute the benchmark process: {}", e))
133156
.and_then(|status| {
@@ -143,5 +166,54 @@ pub fn run_benches(metadata: &Metadata, filters: Filters) -> Result<()> {
143166
eprintln!("Done running {}", bench_name);
144167
}
145168
eprintln!("Finished running {} benchmark suite(s)", to_run.len());
169+
170+
if measurement_mode == MeasurementMode::Walltime {
171+
aggregate_raw_walltime_data(workspace_root)?;
172+
}
173+
174+
Ok(())
175+
}
176+
177+
fn clear_raw_walltime_data(workspace_root: &Path) -> Result<()> {
178+
let raw_results_dir = get_raw_result_dir_from_workspace_root(workspace_root);
179+
std::fs::remove_dir_all(&raw_results_dir).ok(); // ignore errors when the directory does not exist
180+
std::fs::create_dir_all(&raw_results_dir).context("Failed to create raw_results directory")?;
181+
Ok(())
182+
}
183+
184+
fn aggregate_raw_walltime_data(workspace_root: &Path) -> Result<()> {
185+
let mut walltime_benchmarks: Vec<WalltimeBenchmark> = Vec::new();
186+
// retrieve data from `{workspace_root}/target/codspeed/raw_results/{scope}/*.json
187+
for sample in glob(
188+
get_raw_result_dir_from_workspace_root(workspace_root)
189+
.join("**/*.json")
190+
.to_str()
191+
.unwrap(),
192+
)? {
193+
let sample = sample?;
194+
let raw_walltime_data: codspeed::walltime::RawWallTimeData =
195+
serde_json::from_reader(std::fs::File::open(&sample)?)?;
196+
walltime_benchmarks.push(WalltimeBenchmark::from(raw_walltime_data));
197+
}
198+
199+
if walltime_benchmarks.is_empty() {
200+
eprintln!("No walltime benchmarks found");
201+
return Ok(());
202+
}
203+
204+
let results_folder = std::env::var("CODSPEED_PROFILE_FOLDER")
205+
.map(PathBuf::from)
206+
.unwrap_or_else(|_| workspace_root.join("target/codspeed/profiles"))
207+
.join("results");
208+
std::fs::create_dir_all(&results_folder).context("Failed to create results folder")?;
209+
210+
let results = WalltimeResults::from_benchmarks(walltime_benchmarks);
211+
let results_path = results_folder.join(format!("{}.json", std::process::id()));
212+
let mut results_file =
213+
std::fs::File::create(&results_path).context("Failed to create results file")?;
214+
serde_json::to_writer_pretty(&results_file, &results)?;
215+
results_file
216+
.flush()
217+
.context("Failed to flush results file")?;
146218
Ok(())
147219
}

0 commit comments

Comments
 (0)