Skip to content

Commit 08878c1

Browse files
feat: allow defining targets in codspeed yaml
1 parent 9f5b1d0 commit 08878c1

7 files changed

Lines changed: 173 additions & 10 deletions

File tree

src/exec/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::run::uploader::UploadResult;
99
use clap::Args;
1010
use std::path::Path;
1111

12+
pub mod multi_targets;
1213
mod poll_results;
1314

1415
/// We temporarily force this name for all exec runs
@@ -78,8 +79,8 @@ pub async fn run(
7879
setup_cache_dir: Option<&Path>,
7980
) -> Result<()> {
8081
let merged_args = args.merge_with_project_config(project_config);
81-
8282
let config = crate::executor::Config::try_from(merged_args)?;
83+
8384
let mut execution_context = executor::ExecutionContext::try_from((config, codspeed_config))?;
8485
debug!("config: {:#?}", execution_context.config);
8586
let executor = executor::get_executor_from_mode(

src/exec/multi_targets.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
use super::EXEC_HARNESS_COMMAND;
2+
use crate::prelude::*;
3+
use crate::project_config::Target;
4+
use crate::project_config::WalltimeOptions;
5+
use exec_harness::BenchmarkCommand;
6+
7+
/// Convert targets from project config to exec-harness JSON input format
8+
pub fn targets_to_exec_harness_json(
9+
targets: &[Target],
10+
default_walltime: Option<&WalltimeOptions>,
11+
) -> Result<String> {
12+
let inputs: Vec<BenchmarkCommand> = targets
13+
.iter()
14+
.map(|target| {
15+
// Parse the exec string into command parts
16+
let command = shell_words::split(&target.exec)
17+
.with_context(|| format!("Failed to parse command: {}", target.exec))?;
18+
19+
// Merge target-specific walltime options with defaults
20+
let target_walltime = target.options.as_ref().and_then(|o| o.walltime.as_ref());
21+
let walltime_args = merge_walltime_options(default_walltime, target_walltime);
22+
23+
Ok(BenchmarkCommand {
24+
command,
25+
name: target.name.clone(),
26+
walltime_args,
27+
})
28+
})
29+
.collect::<Result<Vec<_>>>()?;
30+
31+
serde_json::to_string(&inputs).context("Failed to serialize targets to JSON")
32+
}
33+
34+
/// Merge default walltime options with target-specific overrides
35+
fn merge_walltime_options(
36+
default: Option<&WalltimeOptions>,
37+
target: Option<&WalltimeOptions>,
38+
) -> exec_harness::walltime::WalltimeExecutionArgs {
39+
let default_args = default.map(walltime_options_to_args);
40+
let target_args = target.map(walltime_options_to_args);
41+
42+
match (default_args, target_args) {
43+
(None, None) => exec_harness::walltime::WalltimeExecutionArgs::default(),
44+
(Some(d), None) => d,
45+
(None, Some(t)) => t,
46+
(Some(d), Some(t)) => exec_harness::walltime::WalltimeExecutionArgs {
47+
warmup_time: t.warmup_time.or(d.warmup_time),
48+
max_time: t.max_time.or(d.max_time),
49+
min_time: t.min_time.or(d.min_time),
50+
max_rounds: t.max_rounds.or(d.max_rounds),
51+
min_rounds: t.min_rounds.or(d.min_rounds),
52+
},
53+
}
54+
}
55+
56+
/// Convert project config WalltimeOptions to exec-harness WalltimeExecutionArgs
57+
fn walltime_options_to_args(
58+
opts: &WalltimeOptions,
59+
) -> exec_harness::walltime::WalltimeExecutionArgs {
60+
exec_harness::walltime::WalltimeExecutionArgs {
61+
warmup_time: opts.warmup_time.clone(),
62+
max_time: opts.max_time.clone(),
63+
min_time: opts.min_time.clone(),
64+
max_rounds: opts.max_rounds,
65+
min_rounds: opts.min_rounds,
66+
}
67+
}
68+
69+
/// Build a command that pipes targets JSON to exec-harness via stdin
70+
pub fn build_pipe_command(
71+
targets: &[Target],
72+
default_walltime: Option<&WalltimeOptions>,
73+
) -> Result<Vec<String>> {
74+
let json = targets_to_exec_harness_json(targets, default_walltime)?;
75+
// Use a heredoc to safely pass the JSON to exec-harness
76+
Ok(vec![
77+
EXEC_HARNESS_COMMAND.to_owned(),
78+
"-".to_owned(),
79+
"<<".to_owned(),
80+
"'CODSPEED_EOF'\n".to_owned(),
81+
json,
82+
"\nCODSPEED_EOF".to_owned(),
83+
])
84+
}

src/executor/config.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,18 +125,16 @@ impl TryFrom<RunArgs> for Config {
125125
}
126126
}
127127

128-
impl TryFrom<crate::exec::ExecArgs> for Config {
129-
type Error = Error;
130-
fn try_from(args: crate::exec::ExecArgs) -> Result<Self> {
128+
impl Config {
129+
/// Create a Config from ExecArgs with a custom command (used for targets mode)
130+
pub fn try_from_with_command(args: crate::exec::ExecArgs, command: String) -> Result<Self> {
131131
let raw_upload_url = args
132132
.shared
133133
.upload_url
134134
.unwrap_or_else(|| DEFAULT_UPLOAD_URL.into());
135135
let upload_url = Url::parse(&raw_upload_url)
136136
.map_err(|e| anyhow!("Invalid upload URL: {raw_upload_url}, {e}"))?;
137137

138-
let wrapped_command = wrap_with_exec_harness(&args.walltime_args, &args.command);
139-
140138
Ok(Self {
141139
upload_url,
142140
token: args.shared.token,
@@ -150,7 +148,7 @@ impl TryFrom<crate::exec::ExecArgs> for Config {
150148
instruments: Instruments { mongodb: None }, // exec doesn't support MongoDB
151149
perf_unwinding_mode: args.shared.perf_run_args.perf_unwinding_mode,
152150
enable_perf: args.shared.perf_run_args.enable_perf,
153-
command: wrapped_command,
151+
command,
154152
profile_folder: args.shared.profile_folder,
155153
skip_upload: args.shared.skip_upload,
156154
skip_run: args.shared.skip_run,
@@ -160,6 +158,14 @@ impl TryFrom<crate::exec::ExecArgs> for Config {
160158
}
161159
}
162160

161+
impl TryFrom<crate::exec::ExecArgs> for Config {
162+
type Error = Error;
163+
fn try_from(args: crate::exec::ExecArgs) -> Result<Self> {
164+
let wrapped_command = wrap_with_exec_harness(&args.walltime_args, &args.command);
165+
Self::try_from_with_command(args, wrapped_command)
166+
}
167+
}
168+
163169
#[cfg(test)]
164170
mod tests {
165171
use crate::instruments::MongoDBConfig;

src/executor/shared/fifo.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ impl RunnerFifo {
156156
}
157157
Err(_) => continue,
158158
};
159-
trace!("Received command: {cmd:?}");
159+
debug!("Received command: {cmd:?}");
160160

161161
match &cmd {
162162
FifoCommand::CurrentBenchmark { pid, uri } => {

src/project_config/interfaces.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,25 @@ use serde::{Deserialize, Serialize};
1010
pub struct ProjectConfig {
1111
/// Default options to apply to all benchmark runs
1212
pub options: Option<ProjectOptions>,
13+
/// List of benchmark targets to execute
14+
pub targets: Option<Vec<Target>>,
15+
}
16+
17+
/// A benchmark target to execute
18+
#[derive(Debug, Deserialize, Serialize, PartialEq)]
19+
#[serde(rename_all = "kebab-case")]
20+
pub struct Target {
21+
/// Optional name for this target
22+
pub name: Option<String>,
23+
/// Command to execute
24+
pub exec: String,
25+
/// Target-specific options
26+
pub options: Option<TargetOptions>,
27+
}
28+
29+
#[derive(Debug, Deserialize, Serialize, PartialEq)]
30+
pub struct TargetOptions {
31+
pub walltime: Option<WalltimeOptions>,
1332
}
1433

1534
/// Root-level options that apply to all benchmark runs unless overridden by CLI

src/project_config/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ options:
240240
working_directory: None,
241241
mode: None,
242242
}),
243+
targets: None,
243244
};
244245

245246
let result = config.validate();
@@ -266,6 +267,7 @@ options:
266267
working_directory: None,
267268
mode: None,
268269
}),
270+
targets: None,
269271
};
270272

271273
let result = config.validate();
@@ -292,6 +294,7 @@ options:
292294
working_directory: Some("./bench".to_string()),
293295
mode: Some(RunnerMode::Walltime),
294296
}),
297+
targets: None,
295298
};
296299

297300
assert!(config.validate().is_ok());

src/run/mod.rs

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,21 @@ impl RunArgs {
194194
}
195195
}
196196

197+
use crate::project_config::Target;
198+
use crate::project_config::WalltimeOptions;
199+
/// Determines the execution mode based on CLI args and project config
200+
enum RunTarget<'a> {
201+
/// Single command from CLI args
202+
SingleCommand(RunArgs),
203+
/// Multiple targets from project config
204+
/// Note: for now, only `codspeed exec` targets are supported in the project config
205+
ConfigTargets {
206+
args: RunArgs,
207+
targets: &'a [Target],
208+
default_walltime: Option<&'a WalltimeOptions>,
209+
},
210+
}
211+
197212
pub async fn run(
198213
args: RunArgs,
199214
api_client: &CodSpeedAPIClient,
@@ -203,9 +218,44 @@ pub async fn run(
203218
) -> Result<()> {
204219
let output_json = args.message_format == Some(MessageFormat::Json);
205220

206-
let merged_args = args.merge_with_project_config(project_config);
221+
let args = args.merge_with_project_config(project_config);
222+
223+
let run_target = if args.command.is_empty() {
224+
// No command provided - check for targets in project config
225+
let targets = project_config
226+
.and_then(|c| c.targets.as_ref())
227+
.filter(|t| !t.is_empty())
228+
.ok_or_else(|| {
229+
anyhow!("No command provided and no targets defined in codspeed.yaml")
230+
})?;
231+
232+
let default_walltime = project_config
233+
.and_then(|c| c.options.as_ref())
234+
.and_then(|o| o.walltime.as_ref());
235+
236+
RunTarget::ConfigTargets {
237+
args,
238+
targets,
239+
default_walltime,
240+
}
241+
} else {
242+
RunTarget::SingleCommand(args)
243+
};
244+
245+
let config = match run_target {
246+
RunTarget::SingleCommand(args) => Config::try_from(args)?,
247+
248+
RunTarget::ConfigTargets {
249+
mut args,
250+
targets,
251+
default_walltime,
252+
} => {
253+
args.command =
254+
crate::exec::multi_targets::build_pipe_command(targets, default_walltime)?;
207255

208-
let config = Config::try_from(merged_args)?;
256+
Config::try_from(args)?
257+
}
258+
};
209259

210260
// Create execution context
211261
let mut execution_context = executor::ExecutionContext::try_from((config, codspeed_config))?;

0 commit comments

Comments
 (0)