Skip to content

Commit ed7f77d

Browse files
refactor: flatten orchestrator execution loop over (command, mode) pairs
Replace the nested command → modes iteration (run_all_modes) with a single flattened loop over ExecutorTarget pairs. This fixes a bug where single-mode + multiple entrypoint targets would reuse the same profile folder, causing runs to overwrite each other. Profile folder resolution is now centralized in resolve_profile_folder: - Single run part + --profile-folder: use as-is - Multiple run parts + --profile-folder: deterministic subdirs (<mode>-<index>) - No --profile-folder: random temp folder Also removes profile_folder from ExecutorConfig since it's now resolved by the orchestrator and passed directly to ExecutionContext::new.
1 parent be73048 commit ed7f77d

6 files changed

Lines changed: 101 additions & 78 deletions

File tree

src/executor/config.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ pub struct ExecutorConfig {
9292

9393
pub simulation_tool: SimulationTool,
9494

95-
pub profile_folder: Option<PathBuf>,
9695
pub skip_run: bool,
9796
pub skip_setup: bool,
9897
/// If true, allow execution even when no benchmarks are found
@@ -161,7 +160,6 @@ impl OrchestratorConfig {
161160
enable_perf: self.enable_perf,
162161
perf_unwinding_mode: self.perf_unwinding_mode,
163162
simulation_tool: self.simulation_tool,
164-
profile_folder: self.profile_folder.clone(),
165163
skip_run: self.skip_run,
166164
skip_setup: self.skip_setup,
167165
allow_empty: self.allow_empty,

src/executor/execution_context.rs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
use super::ExecutorConfig;
22
use std::path::PathBuf;
33

4-
use super::create_profile_folder;
5-
64
/// Per-mode execution context.
75
///
86
/// Contains only the mode-specific configuration and the profile folder path.
@@ -14,16 +12,10 @@ pub struct ExecutionContext {
1412
}
1513

1614
impl ExecutionContext {
17-
pub fn new(config: ExecutorConfig) -> anyhow::Result<Self> {
18-
let profile_folder = if let Some(profile_folder) = &config.profile_folder {
19-
profile_folder.clone()
20-
} else {
21-
create_profile_folder()?
22-
};
23-
24-
Ok(ExecutionContext {
15+
pub fn new(config: ExecutorConfig, profile_folder: PathBuf) -> Self {
16+
ExecutionContext {
2517
config,
2618
profile_folder,
27-
})
19+
}
2820
}
2921
}

src/executor/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ use crate::system::SystemInfo;
1919
use async_trait::async_trait;
2020
pub use config::{BenchmarkTarget, ExecutorConfig, OrchestratorConfig};
2121
pub use execution_context::ExecutionContext;
22-
pub use helpers::profile_folder::create_profile_folder;
2322
pub use interfaces::ExecutorName;
2423
pub use orchestrator::Orchestrator;
2524

src/executor/orchestrator.rs

Lines changed: 88 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::cli::run::logger::Logger;
66
use crate::config::CodSpeedConfig;
77
use crate::executor::config::BenchmarkTarget;
88
use crate::executor::config::OrchestratorConfig;
9+
use crate::executor::helpers::profile_folder::create_profile_folder;
910
use crate::local_logger::rolling_buffer::{activate_rolling_buffer, deactivate_rolling_buffer};
1011
use crate::prelude::*;
1112
use crate::run_environment::{self, RunEnvironment, RunEnvironmentProvider};
@@ -14,7 +15,7 @@ use crate::system::{self, SystemInfo};
1415
use crate::upload::{UploadResult, upload};
1516
use serde_json::Value;
1617
use std::collections::BTreeMap;
17-
use std::path::Path;
18+
use std::path::{Path, PathBuf};
1819

1920
/// Shared orchestration state created once per CLI invocation.
2021
///
@@ -67,34 +68,28 @@ impl Orchestrator {
6768

6869
/// Execute all benchmark targets for all configured modes, then upload results.
6970
///
70-
/// Processes `self.config.targets` as follows:
71-
/// - All `Exec` targets are combined into a single exec-harness invocation (one executor per mode)
72-
/// - Each `Entrypoint` target is run independently (one executor per mode per target)
71+
/// Flattens all `(command, mode)` pairs into a single iteration:
72+
/// - All `Exec` targets are combined into a single exec-harness command
73+
/// - Each `Entrypoint` target produces its own command
74+
/// - Each command is crossed with every configured mode
75+
///
76+
/// Each `(command, mode)` pair gets its own profile folder. When the user
77+
/// specifies `--profile-folder` and there are multiple pairs, deterministic
78+
/// subdirectories (`<mode>-<index>`) are created under that folder.
7379
pub async fn execute<F>(&self, setup_cache_dir: Option<&Path>, poll_results: F) -> Result<()>
7480
where
7581
F: AsyncFn(&UploadResult) -> Result<()>,
7682
{
83+
// Build (command, label) pairs while we still know the target type
84+
let mut command_labels: Vec<(String, String)> = vec![];
85+
7786
let exec_targets: Vec<&BenchmarkTarget> = self
7887
.config
7988
.targets
8089
.iter()
8190
.filter(|t| matches!(t, BenchmarkTarget::Exec { .. }))
8291
.collect();
8392

84-
let entrypoint_targets: Vec<&BenchmarkTarget> = self
85-
.config
86-
.targets
87-
.iter()
88-
.filter(|t| matches!(t, BenchmarkTarget::Entrypoint { .. }))
89-
.collect();
90-
91-
let mut all_completed_runs = vec![];
92-
93-
if !self.config.skip_run {
94-
start_opened_group!("Running the benchmarks");
95-
}
96-
97-
// All exec targets combined into a single exec-harness invocation
9893
if !exec_targets.is_empty() {
9994
ensure_binary_installed(EXEC_HARNESS_COMMAND, EXEC_HARNESS_VERSION, || {
10095
format!(
@@ -104,17 +99,61 @@ impl Orchestrator {
10499
.await?;
105100

106101
let pipe_cmd = multi_targets::build_exec_targets_pipe_command(&exec_targets)?;
107-
let completed_runs = self.run_all_modes(pipe_cmd, setup_cache_dir).await?;
108-
all_completed_runs.extend(completed_runs);
102+
let label = match exec_targets.as_slice() {
103+
[BenchmarkTarget::Exec { command, .. }] => {
104+
format!("Running `{}` with exec-harness", command.join(" "))
105+
}
106+
targets => format!("Running {} commands with exec-harness", targets.len()),
107+
};
108+
command_labels.push((pipe_cmd, label));
109109
}
110110

111-
// Each entrypoint target runs independently
112-
for target in entrypoint_targets {
113-
let BenchmarkTarget::Entrypoint { command, .. } = target else {
114-
unreachable!()
115-
};
116-
let completed_runs = self.run_all_modes(command.clone(), setup_cache_dir).await?;
117-
all_completed_runs.extend(completed_runs);
111+
for target in &self.config.targets {
112+
if let BenchmarkTarget::Entrypoint { command, .. } = target {
113+
command_labels.push((command.clone(), command.clone()));
114+
}
115+
}
116+
117+
struct ExecutorTarget<'a> {
118+
command: String,
119+
mode: &'a RunnerMode,
120+
label: String,
121+
}
122+
123+
// Flatten into (command, mode) run parts
124+
let modes = &self.config.modes;
125+
let run_parts: Vec<ExecutorTarget> = command_labels
126+
.iter()
127+
.flat_map(|(cmd, label)| {
128+
modes.iter().map(move |mode| ExecutorTarget {
129+
command: cmd.clone(),
130+
mode,
131+
label: format!("[{mode}] {label}"),
132+
})
133+
})
134+
.collect();
135+
136+
let total_parts = run_parts.len();
137+
let mut all_completed_runs = vec![];
138+
139+
if !self.config.skip_run {
140+
start_opened_group!("Running the benchmarks");
141+
}
142+
143+
for (run_part_index, part) in run_parts.into_iter().enumerate() {
144+
let config = self.config.executor_config_for_command(part.command);
145+
let profile_folder =
146+
self.resolve_profile_folder(part.mode, run_part_index, total_parts)?;
147+
148+
let ctx = ExecutionContext::new(config, profile_folder);
149+
let executor = get_executor_from_mode(part.mode);
150+
151+
activate_rolling_buffer(&part.label);
152+
153+
run_executor(executor.as_ref(), self, &ctx, setup_cache_dir).await?;
154+
155+
deactivate_rolling_buffer();
156+
all_completed_runs.push((ctx, executor.name()));
118157
}
119158

120159
if !self.config.skip_run {
@@ -127,34 +166,31 @@ impl Orchestrator {
127166
Ok(())
128167
}
129168

130-
/// Run the given command across all configured modes, returning completed run contexts.
131-
async fn run_all_modes(
169+
/// Resolve the profile folder for a given run part.
170+
///
171+
/// - Single run part + user-specified folder: use as-is
172+
/// - Multiple run parts + user-specified folder: `<folder>/<mode>-<index>`
173+
/// - No user-specified folder: create a random temp folder
174+
fn resolve_profile_folder(
132175
&self,
133-
command: String,
134-
setup_cache_dir: Option<&Path>,
135-
) -> Result<Vec<(ExecutionContext, ExecutorName)>> {
136-
let modes = &self.config.modes;
137-
let is_multi_mode = modes.len() > 1;
138-
let mut completed_runs: Vec<(ExecutionContext, ExecutorName)> = vec![];
139-
for mode in modes.iter() {
140-
let mut per_mode_config = self.config.executor_config_for_command(command.clone());
141-
// For multi-mode runs, always create a fresh profile folder per mode
142-
// even if the user specified one (to avoid modes overwriting each other).
143-
if is_multi_mode {
144-
per_mode_config.profile_folder = None;
176+
mode: &RunnerMode,
177+
run_part_index: usize,
178+
total_parts: usize,
179+
) -> Result<PathBuf> {
180+
match (&self.config.profile_folder, total_parts) {
181+
(Some(folder), 1) => Ok(folder.clone()),
182+
(Some(folder), _) => {
183+
let subfolder = folder.join(format!("{mode}-{run_part_index}"));
184+
std::fs::create_dir_all(&subfolder).with_context(|| {
185+
format!(
186+
"Failed to create profile subfolder: {}",
187+
subfolder.display()
188+
)
189+
})?;
190+
Ok(subfolder)
145191
}
146-
let ctx = ExecutionContext::new(per_mode_config)?;
147-
let executor = get_executor_from_mode(mode);
148-
149-
let rolling_title = format!("[{mode}] Running benchmarks");
150-
activate_rolling_buffer(&rolling_title);
151-
152-
run_executor(executor.as_ref(), self, &ctx, setup_cache_dir).await?;
153-
154-
deactivate_rolling_buffer();
155-
completed_runs.push((ctx, executor.name()));
192+
(None, _) => create_profile_folder(),
156193
}
157-
Ok(completed_runs)
158194
}
159195

160196
/// Upload completed runs and poll results.

src/executor/tests.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,16 +120,15 @@ fn env_test_cases(#[case] env_case: (&str, &str)) {}
120120
async fn create_test_setup(config: ExecutorConfig) -> (ExecutionContext, TempDir) {
121121
let temp_dir = TempDir::new().unwrap();
122122

123-
let mut config_with_folder = config;
124-
config_with_folder.profile_folder = Some(temp_dir.path().to_path_buf());
123+
let mut config = config;
125124

126125
// Provide a test token so authentication doesn't fail
127-
if config_with_folder.token.is_none() {
128-
config_with_folder.token = Some("test-token".to_string());
126+
if config.token.is_none() {
127+
config.token = Some("test-token".to_string());
129128
}
130129

131-
let execution_context =
132-
ExecutionContext::new(config_with_folder).expect("Failed to create ExecutionContext");
130+
let profile_folder = temp_dir.path().to_path_buf();
131+
let execution_context = ExecutionContext::new(config, profile_folder);
133132

134133
(execution_context, temp_dir)
135134
}

src/upload/uploader.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -321,13 +321,13 @@ mod tests {
321321
))),
322322
..OrchestratorConfig::test()
323323
};
324+
let profile_folder = PathBuf::from(format!(
325+
"{}/src/uploader/samples/adrien-python-test",
326+
env!("CARGO_MANIFEST_DIR")
327+
));
324328
let executor_config = ExecutorConfig {
325329
command: "pytest tests/ --codspeed".into(),
326330
token: Some("change me".into()),
327-
profile_folder: Some(PathBuf::from(format!(
328-
"{}/src/uploader/samples/adrien-python-test",
329-
env!("CARGO_MANIFEST_DIR")
330-
))),
331331
..ExecutorConfig::test()
332332
};
333333
async_with_vars(
@@ -365,8 +365,7 @@ mod tests {
365365
Orchestrator::new(orchestrator_config, &codspeed_config, &api_client)
366366
.await
367367
.expect("Failed to create Orchestrator for test");
368-
let execution_context = ExecutionContext::new(executor_config)
369-
.expect("Failed to create ExecutionContext");
368+
let execution_context = ExecutionContext::new(executor_config, profile_folder);
370369
let run_part_suffix =
371370
BTreeMap::from([("executor".to_string(), Value::from("valgrind"))]);
372371
upload(

0 commit comments

Comments
 (0)