Skip to content

Commit b00d70d

Browse files
feat: add exec-harness binary
This binary is in charge of providing a minimal instrument-hooks wrapper around the command it is given.
1 parent c91ec46 commit b00d70d

File tree

5 files changed

+263
-3
lines changed

5 files changed

+263
-3
lines changed

Cargo.lock

Lines changed: 68 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ path = "src/main.rs"
1212

1313
[dependencies]
1414
anyhow = "1.0.75"
15-
clap = { version = "4.4.8", features = ["derive", "env", "color"] }
15+
clap = { workspace = true }
1616
itertools = "0.11.0"
1717
lazy_static = "1.4.0"
1818
log = "0.4.20"
@@ -76,7 +76,10 @@ rstest_reuse = "0.7.0"
7676
shell-quote = "0.7.2"
7777

7878
[workspace]
79-
members = ["crates/runner-shared"]
79+
members = ["crates/exec-harness", "crates/runner-shared"]
80+
81+
[workspace.dependencies]
82+
clap = { version = "4.4.8", features = ["derive", "env", "color"] }
8083

8184
[workspace.metadata.release]
8285
sign-tag = true

crates/exec-harness/Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "exec-harness"
3+
version = "4.4.1"
4+
edition = "2024"
5+
6+
[dependencies]
7+
anyhow = { workspace = true }
8+
codspeed = "4.1.0"
9+
clap = { workspace = true }
10+
serde_json = { workspace = true }
11+
serde = { workspace = true }
12+
nix = { workspace = true, features = ["signal"] }

crates/exec-harness/src/main.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
use crate::walltime::WalltimeResults;
2+
use clap::Parser;
3+
use codspeed::instrument_hooks::InstrumentHooks;
4+
use codspeed::walltime_results::WalltimeBenchmark;
5+
use std::path::PathBuf;
6+
use std::process;
7+
8+
mod walltime;
9+
10+
#[derive(Parser, Debug)]
11+
#[command(name = "exec-harness")]
12+
#[command(about = "CodSpeed exec harness - wraps commands with performance instrumentation")]
13+
struct Args {
14+
/// Optional benchmark name (defaults to command filename)
15+
#[arg(long)]
16+
name: Option<String>,
17+
18+
/// The command and arguments to execute
19+
command: Vec<String>,
20+
}
21+
22+
fn main() {
23+
let args = Args::parse();
24+
25+
if args.command.is_empty() {
26+
eprintln!("Error: No command provided");
27+
process::exit(1);
28+
}
29+
30+
// Derive benchmark name from command if not provided
31+
let bench_name = args.name.unwrap_or_else(|| {
32+
// Extract filename from command path
33+
let cmd = &args.command[0];
34+
std::path::Path::new(cmd)
35+
.file_name()
36+
.and_then(|n| n.to_str())
37+
.map(|s| s.to_string())
38+
.unwrap_or_else(|| "exec_benchmark".to_string())
39+
});
40+
// TODO: Better URI generation
41+
let bench_uri = format!("standalone_run::{bench_name}");
42+
43+
let hooks = InstrumentHooks::instance();
44+
45+
// TODO: Change this to avoid impersonating `codspeed-rust`
46+
hooks
47+
.set_integration("codspeed-rust", env!("CARGO_PKG_VERSION"))
48+
.unwrap();
49+
50+
const NUM_ITERATIONS: usize = 1;
51+
let mut times_per_round_ns = Vec::with_capacity(NUM_ITERATIONS);
52+
53+
hooks.start_benchmark().unwrap();
54+
for _ in 0..NUM_ITERATIONS {
55+
// Spawn the command
56+
let mut child = match process::Command::new(&args.command[0])
57+
.args(&args.command[1..])
58+
.spawn()
59+
{
60+
Ok(child) => child,
61+
Err(e) => {
62+
eprintln!("Failed to spawn command: {e}");
63+
process::exit(1);
64+
}
65+
};
66+
// Start monotonic timer for this iteration
67+
let bench_start = InstrumentHooks::current_timestamp();
68+
69+
// Wait for the process to complete
70+
let status = match child.wait() {
71+
Ok(status) => status,
72+
Err(e) => {
73+
eprintln!("Failed to wait for command: {e}");
74+
process::exit(1);
75+
}
76+
};
77+
78+
// Measure elapsed time
79+
let bench_end = InstrumentHooks::current_timestamp();
80+
hooks.add_benchmark_timestamps(bench_start, bench_end);
81+
82+
// Exit immediately if any iteration fails
83+
if !status.success() {
84+
eprintln!("Command failed with exit code: {:?}", status.code());
85+
process::exit(status.code().unwrap_or(1));
86+
}
87+
88+
// Calculate and store the elapsed time in nanoseconds
89+
let elapsed_ns = (bench_end - bench_start) as u128;
90+
times_per_round_ns.push(elapsed_ns);
91+
}
92+
93+
hooks.stop_benchmark().unwrap();
94+
hooks.set_executed_benchmark(&bench_uri).unwrap();
95+
96+
// Collect walltime results
97+
let max_time_ns = times_per_round_ns.iter().copied().max();
98+
let walltime_benchmark = WalltimeBenchmark::from_runtime_data(
99+
bench_name.clone(),
100+
bench_uri.clone(),
101+
vec![1; NUM_ITERATIONS],
102+
times_per_round_ns,
103+
max_time_ns,
104+
);
105+
106+
let walltime_results = WalltimeResults::from_benchmarks(vec![walltime_benchmark])
107+
.expect("Failed to create walltime results");
108+
109+
walltime_results
110+
.save_to_file(
111+
std::env::var("CODSPEED_PROFILE_FOLDER")
112+
.map(PathBuf::from)
113+
.unwrap_or_else(|_| std::env::current_dir().unwrap().join(".codspeed")),
114+
)
115+
.expect("Failed to save walltime results");
116+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use anyhow::Context;
2+
use anyhow::Result;
3+
use codspeed::walltime_results::WalltimeBenchmark;
4+
use serde::Deserialize;
5+
use serde::Serialize;
6+
use std::path::Path;
7+
8+
#[derive(Debug, Serialize, Deserialize)]
9+
struct Instrument {
10+
#[serde(rename = "type")]
11+
type_: String,
12+
}
13+
14+
#[derive(Debug, Serialize, Deserialize)]
15+
struct Creator {
16+
name: String,
17+
version: String,
18+
pid: u32,
19+
}
20+
21+
#[derive(Debug, Serialize, Deserialize)]
22+
pub struct WalltimeResults {
23+
creator: Creator,
24+
instrument: Instrument,
25+
benchmarks: Vec<WalltimeBenchmark>,
26+
}
27+
28+
impl WalltimeResults {
29+
pub fn from_benchmarks(benchmarks: Vec<WalltimeBenchmark>) -> Result<Self> {
30+
Ok(WalltimeResults {
31+
instrument: Instrument {
32+
type_: "walltime".to_string(),
33+
},
34+
creator: Creator {
35+
name: "codspeed-rust".to_string(),
36+
version: env!("CARGO_PKG_VERSION").to_string(),
37+
pid: std::process::id(),
38+
},
39+
benchmarks,
40+
})
41+
}
42+
43+
pub fn save_to_file<P: AsRef<Path>>(&self, profile_folder: P) -> Result<()> {
44+
let results_path = {
45+
let results_dir = profile_folder.as_ref().join("results");
46+
std::fs::create_dir_all(&results_dir).with_context(|| {
47+
format!(
48+
"Failed to create results directory: {}",
49+
results_dir.display()
50+
)
51+
})?;
52+
53+
results_dir.join(format!("{}.json", self.creator.pid))
54+
};
55+
56+
let file = std::fs::File::create(&results_path)
57+
.with_context(|| format!("Failed to create file: {}", results_path.display()))?;
58+
serde_json::to_writer_pretty(file, &self)
59+
.with_context(|| format!("Failed to write JSON to file: {}", results_path.display()))?;
60+
Ok(())
61+
}
62+
}

0 commit comments

Comments
 (0)