Skip to content

Commit 11154f4

Browse files
art049adriencaccia
andcommitted
feat(cargo-codspeed): support walltime runs from with cargo-codspeed
Co-authored-by: Adrien Cacciaguerra <[email protected]>
1 parent 88e078c commit 11154f4

File tree

8 files changed

+545
-26
lines changed

8 files changed

+545
-26
lines changed

Cargo.lock

Lines changed: 255 additions & 6 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: 20 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,13 @@ 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+
// This is an Option even if MeasurementMode has a default because
18+
// the default is dynamic and this would mislead the user
19+
#[arg(short, long, global = true, env = "CODSPEED_RUNNER_MODE")]
20+
measurement_mode: Option<MeasurementMode>,
21+
1522
#[command(subcommand)]
1623
command: Commands,
1724
}
@@ -63,6 +70,9 @@ pub fn run(args: impl Iterator<Item = OsString>) -> Result<()> {
6370
let metadata = MetadataCommand::new().exec()?;
6471
let cli = Cli::try_parse_from(args)?;
6572

73+
let measurement_mode = cli.measurement_mode.unwrap_or_default();
74+
eprintln!("[cargo-codspeed] Measurement mode: {measurement_mode:?}\n");
75+
6676
let res = match cli.command {
6777
Commands::Build {
6878
filters,
@@ -71,9 +81,16 @@ pub fn run(args: impl Iterator<Item = OsString>) -> Result<()> {
7181
} => {
7282
let features =
7383
features.map(|f| f.split([' ', ',']).map(|s| s.to_string()).collect_vec());
74-
build_benches(&metadata, filters, features, profile, cli.quiet)
84+
build_benches(
85+
&metadata,
86+
filters,
87+
features,
88+
profile,
89+
cli.quiet,
90+
measurement_mode,
91+
)
7592
}
76-
Commands::Run { filters } => run_benches(&metadata, filters),
93+
Commands::Run { filters } => run_benches(&metadata, filters, measurement_mode),
7794
};
7895

7996
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
}
@@ -99,18 +105,20 @@ impl BuildOptions<'_> {
99105
}
100106

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

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

115123
if let Some(features) = self.features {
116124
cargo.arg("--features").arg(features.join(","));
@@ -150,13 +158,14 @@ pub fn build_benches(
150158
features: Option<Vec<String>>,
151159
profile: String,
152160
quiet: bool,
161+
measurement_mode: MeasurementMode,
153162
) -> Result<()> {
154163
let built_benches = BuildOptions {
155164
filters,
156165
features: &features,
157166
profile: &profile,
158167
}
159-
.build(metadata, quiet)?;
168+
.build(metadata, quiet, measurement_mode)?;
160169

161170
if built_benches.is_empty() {
162171
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: 77 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,55 @@ 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+
// retrieve data from `{workspace_root}/target/codspeed/raw_results/{scope}/*.json
186+
let walltime_benchmarks = glob(&format!(
187+
"{}/**/*.json",
188+
get_raw_result_dir_from_workspace_root(workspace_root)
189+
.to_str()
190+
.unwrap(),
191+
))?
192+
.map(|sample| {
193+
let sample = sample?;
194+
let raw_walltime_data: codspeed::walltime::RawWallTimeData =
195+
serde_json::from_reader(std::fs::File::open(&sample)?)?;
196+
Ok(WalltimeBenchmark::from(raw_walltime_data))
197+
})
198+
.collect::<Result<Vec<_>>>()?;
199+
200+
if walltime_benchmarks.is_empty() {
201+
eprintln!("No walltime benchmarks found");
202+
return Ok(());
203+
}
204+
205+
let results_folder = std::env::var("CODSPEED_PROFILE_FOLDER")
206+
.map(PathBuf::from)
207+
.unwrap_or_else(|_| workspace_root.join("target/codspeed/profiles"))
208+
.join("results");
209+
std::fs::create_dir_all(&results_folder).context("Failed to create results folder")?;
210+
211+
let results = WalltimeResults::from_benchmarks(walltime_benchmarks);
212+
let results_path = results_folder.join(format!("{}.json", std::process::id()));
213+
let mut results_file =
214+
std::fs::File::create(&results_path).context("Failed to create results file")?;
215+
serde_json::to_writer_pretty(&results_file, &results)?;
216+
results_file
217+
.flush()
218+
.context("Failed to flush results file")?;
146219
Ok(())
147220
}

0 commit comments

Comments
 (0)