Skip to content

Commit 5c13e6b

Browse files
feat: introduce OrchestratorConfig and ExecutorConfig
We now build Orchestrator config from cli args and profile config, and then the orchestrator is in charge of spawning the appropriate executor config when running an executor. An ExecutorConfig is only valid to run a single command in a single mode, while the OrchestratorConfig is what defines all commands and modes that will be run in a single CLI invocation.
1 parent 9eb35c8 commit 5c13e6b

21 files changed

Lines changed: 455 additions & 433 deletions

File tree

src/cli/exec/mod.rs

Lines changed: 56 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,25 @@
11
use super::ExecAndRunSharedArgs;
22
use crate::api_client::CodSpeedAPIClient;
3-
use crate::binary_installer::ensure_binary_installed;
43
use crate::config::CodSpeedConfig;
54
use crate::executor;
5+
use crate::executor::config::{self, OrchestratorConfig, RepositoryOverride};
6+
use crate::instruments::Instruments;
67
use crate::prelude::*;
78
use crate::project_config::ProjectConfig;
89
use crate::project_config::merger::ConfigMerger;
910
use crate::upload::UploadResult;
1011
use crate::upload::poll_results::{PollResultsOptions, poll_results};
1112
use clap::Args;
1213
use std::path::Path;
14+
use url::Url;
1315

1416
pub mod multi_targets;
1517

1618
/// We temporarily force this name for all exec runs
1719
pub const DEFAULT_REPOSITORY_NAME: &str = "local-runs";
1820

19-
const EXEC_HARNESS_COMMAND: &str = "exec-harness";
20-
const EXEC_HARNESS_VERSION: &str = "1.2.0";
21-
22-
/// Wraps a command with exec-harness and the given walltime arguments.
23-
///
24-
/// This produces a shell command string like:
25-
/// `exec-harness --warmup-time 1s --max-rounds 10 sleep 0.1`
26-
pub fn wrap_with_exec_harness(
27-
walltime_args: &exec_harness::walltime::WalltimeExecutionArgs,
28-
command: &[String],
29-
) -> String {
30-
shell_words::join(
31-
std::iter::once(EXEC_HARNESS_COMMAND)
32-
.chain(walltime_args.to_cli_args().iter().map(|s| s.as_str()))
33-
.chain(command.iter().map(|s| s.as_str())),
34-
)
35-
}
21+
pub const EXEC_HARNESS_COMMAND: &str = "exec-harness";
22+
pub const EXEC_HARNESS_VERSION: &str = "1.2.0";
3623

3724
#[derive(Args, Debug)]
3825
pub struct ExecArgs {
@@ -72,6 +59,42 @@ impl ExecArgs {
7259
}
7360
}
7461

62+
fn build_orchestrator_config(
63+
args: ExecArgs,
64+
target: executor::BenchmarkTarget,
65+
) -> Result<OrchestratorConfig> {
66+
let modes = args.shared.resolve_modes()?;
67+
let raw_upload_url = args
68+
.shared
69+
.upload_url
70+
.unwrap_or_else(|| config::DEFAULT_UPLOAD_URL.into());
71+
let upload_url = Url::parse(&raw_upload_url)
72+
.map_err(|e| anyhow!("Invalid upload URL: {raw_upload_url}, {e}"))?;
73+
74+
Ok(OrchestratorConfig {
75+
upload_url,
76+
token: args.shared.token,
77+
repository_override: args
78+
.shared
79+
.repository
80+
.map(|repo| RepositoryOverride::from_arg(repo, args.shared.provider))
81+
.transpose()?,
82+
working_directory: args.shared.working_directory,
83+
target,
84+
modes,
85+
instruments: Instruments { mongodb: None }, // exec doesn't support MongoDB
86+
perf_unwinding_mode: args.shared.perf_run_args.perf_unwinding_mode,
87+
enable_perf: args.shared.perf_run_args.enable_perf,
88+
simulation_tool: args.shared.simulation_tool.unwrap_or_default(),
89+
profile_folder: args.shared.profile_folder,
90+
skip_upload: args.shared.skip_upload,
91+
skip_run: args.shared.skip_run,
92+
skip_setup: args.shared.skip_setup,
93+
allow_empty: args.shared.allow_empty,
94+
go_runner_version: args.shared.go_runner_version,
95+
})
96+
}
97+
7598
pub async fn run(
7699
args: ExecArgs,
77100
api_client: &CodSpeedAPIClient,
@@ -80,52 +103,41 @@ pub async fn run(
80103
setup_cache_dir: Option<&Path>,
81104
) -> Result<()> {
82105
let merged_args = args.merge_with_project_config(project_config);
83-
let config = crate::executor::Config::try_from(merged_args)?;
106+
let target = executor::BenchmarkTarget::Exec {
107+
command: merged_args.command.clone(),
108+
name: merged_args.name.clone(),
109+
walltime_args: merged_args.walltime_args.clone(),
110+
};
111+
let config = build_orchestrator_config(merged_args, target)?;
84112

85-
execute_with_harness(config, api_client, codspeed_config, setup_cache_dir).await
113+
execute_config(config, api_client, codspeed_config, setup_cache_dir).await
86114
}
87115

88-
/// Core execution logic for exec-harness based runs.
116+
/// Core execution logic shared by `codspeed exec` and `codspeed run` with config targets.
89117
///
90-
/// This function handles exec-harness installation and benchmark execution with exec-style
91-
/// result polling. It is used by both `codspeed exec` directly and by `codspeed run` when
92-
/// executing targets defined in codspeed.yaml.
93-
pub async fn execute_with_harness(
94-
mut config: crate::executor::Config,
118+
/// Sets up the orchestrator and drives execution. Exec-harness installation is handled
119+
/// by the orchestrator when exec targets are present.
120+
pub async fn execute_config(
121+
config: OrchestratorConfig,
95122
api_client: &CodSpeedAPIClient,
96123
codspeed_config: &CodSpeedConfig,
97124
setup_cache_dir: Option<&Path>,
98125
) -> Result<()> {
99-
let orchestrator =
100-
executor::Orchestrator::new(&mut config, codspeed_config, api_client).await?;
126+
let orchestrator = executor::Orchestrator::new(config, codspeed_config, api_client).await?;
101127

102128
if !orchestrator.is_local() {
103129
super::show_banner();
104130
}
105131

106-
debug!("config: {config:#?}");
107-
108-
let get_exec_harness_installer_url = || {
109-
format!(
110-
"https://github.com/CodSpeedHQ/codspeed/releases/download/exec-harness-v{EXEC_HARNESS_VERSION}/exec-harness-installer.sh"
111-
)
112-
};
113-
114-
// Ensure the exec-harness is installed
115-
ensure_binary_installed(
116-
EXEC_HARNESS_COMMAND,
117-
EXEC_HARNESS_VERSION,
118-
get_exec_harness_installer_url,
119-
)
120-
.await?;
132+
debug!("config: {:#?}", orchestrator.config);
121133

122134
let poll_opts = PollResultsOptions::for_exec();
123135
let poll_results_fn = async |upload_result: &UploadResult| {
124136
poll_results(api_client, upload_result, &poll_opts).await
125137
};
126138

127139
orchestrator
128-
.execute(&mut config, setup_cache_dir, poll_results_fn)
140+
.execute(setup_cache_dir, poll_results_fn)
129141
.await?;
130142

131143
Ok(())

src/cli/exec/multi_targets.rs

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,19 +66,41 @@ fn walltime_options_to_args(
6666
}
6767
}
6868

69-
/// Build a command that pipes targets JSON to exec-harness via stdin
69+
/// Build a shell command string that pipes targets JSON to exec-harness via stdin
7070
pub fn build_pipe_command(
7171
targets: &[Target],
7272
default_walltime: Option<&WalltimeOptions>,
73-
) -> Result<Vec<String>> {
73+
) -> Result<String> {
7474
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-
])
75+
Ok(build_pipe_command_from_json(&json))
76+
}
77+
78+
/// Build a shell command string that pipes BenchmarkTarget::Exec variants to exec-harness via stdin
79+
pub fn build_exec_targets_pipe_command(
80+
targets: &[&crate::executor::config::BenchmarkTarget],
81+
) -> Result<String> {
82+
let inputs: Vec<BenchmarkCommand> = targets
83+
.iter()
84+
.map(|target| match target {
85+
crate::executor::config::BenchmarkTarget::Exec {
86+
command,
87+
name,
88+
walltime_args,
89+
} => Ok(BenchmarkCommand {
90+
command: command.clone(),
91+
name: name.clone(),
92+
walltime_args: walltime_args.clone(),
93+
}),
94+
crate::executor::config::BenchmarkTarget::Entrypoint { .. } => {
95+
bail!("Entrypoint targets cannot be used with exec-harness pipe command")
96+
}
97+
})
98+
.collect::<Result<Vec<_>>>()?;
99+
100+
let json = serde_json::to_string(&inputs).context("Failed to serialize targets to JSON")?;
101+
Ok(build_pipe_command_from_json(&json))
102+
}
103+
104+
fn build_pipe_command_from_json(json: &str) -> String {
105+
format!("{EXEC_HARNESS_COMMAND} - <<'CODSPEED_EOF'\n{json}\nCODSPEED_EOF")
84106
}

src/cli/run/mod.rs

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ use super::ExecAndRunSharedArgs;
22
use crate::api_client::CodSpeedAPIClient;
33
use crate::config::CodSpeedConfig;
44
use crate::executor;
5-
use crate::executor::Config;
5+
use crate::executor::config::{self, OrchestratorConfig, RepositoryOverride};
6+
use crate::instruments::Instruments;
67
use crate::prelude::*;
78
use crate::project_config::ProjectConfig;
89
use crate::project_config::merger::ConfigMerger;
@@ -11,6 +12,7 @@ use crate::upload::UploadResult;
1112
use crate::upload::poll_results::{PollResultsOptions, poll_results};
1213
use clap::{Args, ValueEnum};
1314
use std::path::Path;
15+
use url::Url;
1416

1517
pub mod helpers;
1618
pub mod logger;
@@ -91,14 +93,49 @@ impl RunArgs {
9193
}
9294
}
9395

94-
use crate::project_config::Target;
95-
use crate::project_config::WalltimeOptions;
96+
fn build_orchestrator_config(
97+
args: RunArgs,
98+
target: executor::BenchmarkTarget,
99+
) -> Result<OrchestratorConfig> {
100+
let instruments = Instruments::try_from(&args)?;
101+
let modes = args.shared.resolve_modes()?;
102+
let raw_upload_url = args
103+
.shared
104+
.upload_url
105+
.unwrap_or_else(|| config::DEFAULT_UPLOAD_URL.into());
106+
let upload_url = Url::parse(&raw_upload_url)
107+
.map_err(|e| anyhow!("Invalid upload URL: {raw_upload_url}, {e}"))?;
108+
109+
Ok(OrchestratorConfig {
110+
upload_url,
111+
token: args.shared.token,
112+
repository_override: args
113+
.shared
114+
.repository
115+
.map(|repo| RepositoryOverride::from_arg(repo, args.shared.provider))
116+
.transpose()?,
117+
working_directory: args.shared.working_directory,
118+
target,
119+
modes,
120+
instruments,
121+
perf_unwinding_mode: args.shared.perf_run_args.perf_unwinding_mode,
122+
enable_perf: args.shared.perf_run_args.enable_perf,
123+
simulation_tool: args.shared.simulation_tool.unwrap_or_default(),
124+
profile_folder: args.shared.profile_folder,
125+
skip_upload: args.shared.skip_upload,
126+
skip_run: args.shared.skip_run,
127+
skip_setup: args.shared.skip_setup,
128+
allow_empty: args.shared.allow_empty,
129+
go_runner_version: args.shared.go_runner_version,
130+
})
131+
}
132+
133+
use crate::project_config::{Target, WalltimeOptions};
96134
/// Determines the execution mode based on CLI args and project config
97135
enum RunTarget<'a> {
98136
/// Single command from CLI args
99137
SingleCommand(RunArgs),
100138
/// Multiple targets from project config
101-
/// Note: for now, only `codspeed exec` targets are supported in the project config
102139
ConfigTargets {
103140
args: RunArgs,
104141
targets: &'a [Target],
@@ -141,35 +178,56 @@ pub async fn run(
141178

142179
match run_target {
143180
RunTarget::SingleCommand(args) => {
144-
let mut config = Config::try_from(args)?;
145-
181+
let target = executor::BenchmarkTarget::Entrypoint {
182+
command: args.command.join(" "),
183+
name: None,
184+
};
185+
let config = build_orchestrator_config(args, target)?;
146186
let orchestrator =
147-
executor::Orchestrator::new(&mut config, codspeed_config, api_client).await?;
187+
executor::Orchestrator::new(config, codspeed_config, api_client).await?;
148188

149189
if !orchestrator.is_local() {
150190
super::show_banner();
151191
}
152-
debug!("config: {config:#?}");
192+
debug!("config: {:#?}", orchestrator.config);
153193

154194
let poll_opts = PollResultsOptions::for_run(output_json);
155195
let poll_results_fn = async |upload_result: &UploadResult| {
156196
poll_results(api_client, upload_result, &poll_opts).await
157197
};
158198
orchestrator
159-
.execute(&mut config, setup_cache_dir, poll_results_fn)
199+
.execute(setup_cache_dir, poll_results_fn)
160200
.await?;
161201
}
162202

163203
RunTarget::ConfigTargets {
164-
mut args,
204+
args,
165205
targets,
166206
default_walltime,
167207
} => {
168-
args.command =
208+
let pipe_command =
169209
super::exec::multi_targets::build_pipe_command(targets, default_walltime)?;
170-
let config = Config::try_from(args)?;
210+
// Wrap as Entrypoint since the pipe command string already includes exec-harness
211+
let target = executor::BenchmarkTarget::Entrypoint {
212+
command: pipe_command,
213+
name: None,
214+
};
215+
let config = build_orchestrator_config(args, target)?;
216+
217+
// Ensure exec-harness is installed since config targets use it
218+
crate::binary_installer::ensure_binary_installed(
219+
super::exec::EXEC_HARNESS_COMMAND,
220+
super::exec::EXEC_HARNESS_VERSION,
221+
|| {
222+
format!(
223+
"https://github.com/CodSpeedHQ/codspeed/releases/download/exec-harness-v{}/exec-harness-installer.sh",
224+
super::exec::EXEC_HARNESS_VERSION
225+
)
226+
},
227+
)
228+
.await?;
171229

172-
super::exec::execute_with_harness(config, api_client, codspeed_config, setup_cache_dir)
230+
super::exec::execute_config(config, api_client, codspeed_config, setup_cache_dir)
173231
.await?;
174232
}
175233
}

0 commit comments

Comments
 (0)