@@ -6,6 +6,7 @@ use crate::cli::run::logger::Logger;
66use crate :: config:: CodSpeedConfig ;
77use crate :: executor:: config:: BenchmarkTarget ;
88use crate :: executor:: config:: OrchestratorConfig ;
9+ use crate :: executor:: helpers:: profile_folder:: create_profile_folder;
910use crate :: local_logger:: rolling_buffer:: { activate_rolling_buffer, deactivate_rolling_buffer} ;
1011use crate :: prelude:: * ;
1112use crate :: run_environment:: { self , RunEnvironment , RunEnvironmentProvider } ;
@@ -14,7 +15,7 @@ use crate::system::{self, SystemInfo};
1415use crate :: upload:: { UploadResult , upload} ;
1516use serde_json:: Value ;
1617use 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.
0 commit comments