|
| 1 | +use super::ExecutionOptions; |
| 2 | +use super::config::RoundOrTime; |
| 3 | +use crate::prelude::*; |
| 4 | +use codspeed::instrument_hooks::InstrumentHooks; |
| 5 | +use std::process::Command; |
| 6 | +use std::time::Duration; |
| 7 | + |
| 8 | +pub fn run_rounds( |
| 9 | + bench_uri: String, |
| 10 | + command: Vec<String>, |
| 11 | + config: &ExecutionOptions, |
| 12 | +) -> Result<Vec<u128>> { |
| 13 | + let warmup_time_ns = config.warmup_time_ns; |
| 14 | + let hooks = InstrumentHooks::instance(); |
| 15 | + |
| 16 | + let do_one_round = |times_per_round_ns: &mut Vec<u128>| { |
| 17 | + let mut child = Command::new(&command[0]) |
| 18 | + .args(&command[1..]) |
| 19 | + .spawn() |
| 20 | + .context("Failed to execute command")?; |
| 21 | + let bench_round_start_ts_ns = InstrumentHooks::current_timestamp(); |
| 22 | + let status = child |
| 23 | + .wait() |
| 24 | + .context("Failed to wait for command to finish")?; |
| 25 | + |
| 26 | + let bench_round_end_ts_ns = InstrumentHooks::current_timestamp(); |
| 27 | + hooks.add_benchmark_timestamps(bench_round_start_ts_ns, bench_round_end_ts_ns); |
| 28 | + |
| 29 | + if !status.success() { |
| 30 | + bail!("Command exited with non-zero status: {status}"); |
| 31 | + } |
| 32 | + |
| 33 | + times_per_round_ns.push((bench_round_end_ts_ns - bench_round_start_ts_ns) as u128); |
| 34 | + |
| 35 | + Ok(()) |
| 36 | + }; |
| 37 | + |
| 38 | + // Compute the number of rounds to perform (potentially undefined if no warmup and only time constraints) |
| 39 | + let rounds_to_perform: Option<u64> = if warmup_time_ns > 0 { |
| 40 | + match compute_rounds_from_warmup(config, hooks, &bench_uri, do_one_round)? { |
| 41 | + WarmupResult::EarlyReturn(times) => return Ok(times), |
| 42 | + WarmupResult::Rounds(rounds) => Some(rounds), |
| 43 | + } |
| 44 | + } else { |
| 45 | + extract_rounds_from_config(config) |
| 46 | + }; |
| 47 | + |
| 48 | + let (min_time_ns, max_time_ns) = extract_time_constraints(config); |
| 49 | + |
| 50 | + // Validate that we have at least one constraint when warmup is disabled |
| 51 | + if warmup_time_ns == 0 |
| 52 | + && rounds_to_perform.is_none() |
| 53 | + && min_time_ns.is_none() |
| 54 | + && max_time_ns.is_none() |
| 55 | + { |
| 56 | + bail!( |
| 57 | + "When warmup is disabled, at least one constraint (min_rounds, max_rounds, min_time, or max_time) must be specified" |
| 58 | + ); |
| 59 | + } |
| 60 | + |
| 61 | + if let Some(rounds) = rounds_to_perform { |
| 62 | + info!("Warmup done, now performing {rounds} rounds"); |
| 63 | + } else { |
| 64 | + debug!( |
| 65 | + "Running in degraded mode (no warmup, time-based constraints only): min_time={}, max_time={}", |
| 66 | + min_time_ns |
| 67 | + .map(format_ns) |
| 68 | + .unwrap_or_else(|| "none".to_string()), |
| 69 | + max_time_ns |
| 70 | + .map(format_ns) |
| 71 | + .unwrap_or_else(|| "none".to_string()) |
| 72 | + ); |
| 73 | + } |
| 74 | + |
| 75 | + let mut times_per_round_ns = rounds_to_perform |
| 76 | + .map(|r| Vec::with_capacity(r as usize)) |
| 77 | + .unwrap_or_default(); |
| 78 | + let mut current_round: u64 = 0; |
| 79 | + |
| 80 | + hooks.start_benchmark().unwrap(); |
| 81 | + |
| 82 | + debug!( |
| 83 | + "Starting loop with ending conditions: \ |
| 84 | + rounds {rounds_to_perform:?}, \ |
| 85 | + min_time_ns {min_time_ns:?}, \ |
| 86 | + max_time_ns {max_time_ns:?}" |
| 87 | + ); |
| 88 | + let round_start_ts_ns = InstrumentHooks::current_timestamp(); |
| 89 | + loop { |
| 90 | + do_one_round(&mut times_per_round_ns)?; |
| 91 | + current_round += 1; |
| 92 | + |
| 93 | + let elapsed_ns = InstrumentHooks::current_timestamp() - round_start_ts_ns; |
| 94 | + |
| 95 | + // Check stop conditions |
| 96 | + let reached_max_rounds = rounds_to_perform.is_some_and(|r| current_round >= r); |
| 97 | + let reached_max_time = max_time_ns.is_some_and(|t| elapsed_ns >= t); |
| 98 | + let reached_min_time = min_time_ns.is_some_and(|t| elapsed_ns >= t); |
| 99 | + |
| 100 | + // Stop if we hit max_time |
| 101 | + if reached_max_time { |
| 102 | + debug!( |
| 103 | + "Reached maximum time limit after {current_round} rounds (elapsed: {}, max: {})", |
| 104 | + format_ns(elapsed_ns), |
| 105 | + format_ns(max_time_ns.unwrap()) |
| 106 | + ); |
| 107 | + break; |
| 108 | + } |
| 109 | + |
| 110 | + // Stop if we hit max_rounds |
| 111 | + if reached_max_rounds { |
| 112 | + break; |
| 113 | + } |
| 114 | + |
| 115 | + // If no rounds constraint, stop when min_time is reached |
| 116 | + if rounds_to_perform.is_none() && reached_min_time { |
| 117 | + debug!( |
| 118 | + "Reached minimum time after {current_round} rounds (elapsed: {}, min: {})", |
| 119 | + format_ns(elapsed_ns), |
| 120 | + format_ns(min_time_ns.unwrap()) |
| 121 | + ); |
| 122 | + break; |
| 123 | + } |
| 124 | + } |
| 125 | + hooks.stop_benchmark().unwrap(); |
| 126 | + hooks.set_executed_benchmark(&bench_uri).unwrap(); |
| 127 | + |
| 128 | + Ok(times_per_round_ns) |
| 129 | +} |
| 130 | + |
| 131 | +enum WarmupResult { |
| 132 | + /// Warmup satisfied max_time constraint, return early with these times |
| 133 | + EarlyReturn(Vec<u128>), |
| 134 | + /// Continue with this many rounds |
| 135 | + Rounds(u64), |
| 136 | +} |
| 137 | + |
| 138 | +/// Run warmup rounds and compute the number of benchmark rounds to perform |
| 139 | +fn compute_rounds_from_warmup<F>( |
| 140 | + config: &ExecutionOptions, |
| 141 | + hooks: &InstrumentHooks, |
| 142 | + bench_uri: &str, |
| 143 | + do_one_round: F, |
| 144 | +) -> Result<WarmupResult> |
| 145 | +where |
| 146 | + F: Fn(&mut Vec<u128>) -> Result<()>, |
| 147 | +{ |
| 148 | + let mut warmup_times_ns = Vec::new(); |
| 149 | + let warmup_start_ts_ns = InstrumentHooks::current_timestamp(); |
| 150 | + |
| 151 | + hooks.start_benchmark().unwrap(); |
| 152 | + while InstrumentHooks::current_timestamp() < warmup_start_ts_ns + config.warmup_time_ns { |
| 153 | + do_one_round(&mut warmup_times_ns)?; |
| 154 | + } |
| 155 | + hooks.stop_benchmark().unwrap(); |
| 156 | + let warmup_end_ts_ns = InstrumentHooks::current_timestamp(); |
| 157 | + |
| 158 | + // Check if single warmup round already exceeded max_time |
| 159 | + if let [single_warmup_round_duration_ns] = warmup_times_ns.as_slice() { |
| 160 | + match config.max { |
| 161 | + Some(RoundOrTime::TimeNs(time_ns)) | Some(RoundOrTime::Both { time_ns, .. }) => { |
| 162 | + if time_ns <= *single_warmup_round_duration_ns as u64 { |
| 163 | + info!( |
| 164 | + "Warmup duration ({}) exceeded or met max_time ({}). No more rounds will be performed.", |
| 165 | + format_ns(*single_warmup_round_duration_ns as u64), |
| 166 | + format_ns(time_ns) |
| 167 | + ); |
| 168 | + hooks.set_executed_benchmark(bench_uri).unwrap(); |
| 169 | + return Ok(WarmupResult::EarlyReturn(warmup_times_ns)); |
| 170 | + } |
| 171 | + } |
| 172 | + _ => { /* No max time constraint */ } |
| 173 | + } |
| 174 | + } |
| 175 | + |
| 176 | + info!("Completed {} warmup rounds", warmup_times_ns.len()); |
| 177 | + |
| 178 | + let average_time_per_round_ns = |
| 179 | + (warmup_end_ts_ns - warmup_start_ts_ns) / warmup_times_ns.len() as u64; |
| 180 | + |
| 181 | + let actual_min_rounds = compute_min_rounds(config, average_time_per_round_ns); |
| 182 | + let actual_max_rounds = compute_max_rounds(config, average_time_per_round_ns); |
| 183 | + |
| 184 | + let rounds = match (actual_min_rounds, actual_max_rounds) { |
| 185 | + (Some(min), Some(max)) if min > max => { |
| 186 | + warn!( |
| 187 | + "Computed min rounds ({min}) is greater than max rounds ({max}). Using max rounds.", |
| 188 | + ); |
| 189 | + max |
| 190 | + } |
| 191 | + (Some(min), Some(max)) => (min + max) / 2, |
| 192 | + (None, Some(max)) => max, |
| 193 | + (Some(min), None) => min, |
| 194 | + (None, None) => { |
| 195 | + bail!("Unable to determine number of rounds to perform"); |
| 196 | + } |
| 197 | + }; |
| 198 | + |
| 199 | + Ok(WarmupResult::Rounds(rounds)) |
| 200 | +} |
| 201 | + |
| 202 | +/// Compute the minimum number of rounds based on config and average round time |
| 203 | +fn compute_min_rounds(config: &ExecutionOptions, avg_time_per_round_ns: u64) -> Option<u64> { |
| 204 | + match &config.min { |
| 205 | + Some(RoundOrTime::Rounds(rounds)) => Some(*rounds), |
| 206 | + Some(RoundOrTime::TimeNs(time_ns)) => { |
| 207 | + Some(((time_ns + avg_time_per_round_ns) / avg_time_per_round_ns) + 1) |
| 208 | + } |
| 209 | + Some(RoundOrTime::Both { rounds, time_ns }) => { |
| 210 | + let rounds_from_time = ((time_ns + avg_time_per_round_ns) / avg_time_per_round_ns) + 1; |
| 211 | + Some((*rounds).max(rounds_from_time)) |
| 212 | + } |
| 213 | + None => None, |
| 214 | + } |
| 215 | +} |
| 216 | + |
| 217 | +/// Compute the maximum number of rounds based on config and average round time |
| 218 | +fn compute_max_rounds(config: &ExecutionOptions, avg_time_per_round_ns: u64) -> Option<u64> { |
| 219 | + match &config.max { |
| 220 | + Some(RoundOrTime::Rounds(rounds)) => Some(*rounds), |
| 221 | + Some(RoundOrTime::TimeNs(time_ns)) => { |
| 222 | + Some((time_ns + avg_time_per_round_ns) / avg_time_per_round_ns) |
| 223 | + } |
| 224 | + Some(RoundOrTime::Both { rounds, time_ns }) => { |
| 225 | + let rounds_from_time = (time_ns + avg_time_per_round_ns) / avg_time_per_round_ns; |
| 226 | + Some((*rounds).min(rounds_from_time)) |
| 227 | + } |
| 228 | + None => None, |
| 229 | + } |
| 230 | +} |
| 231 | + |
| 232 | +/// Extract rounds directly from config (used when warmup is disabled) |
| 233 | +fn extract_rounds_from_config(config: &ExecutionOptions) -> Option<u64> { |
| 234 | + match (&config.max, &config.min) { |
| 235 | + (Some(RoundOrTime::Rounds(rounds)), _) | (_, Some(RoundOrTime::Rounds(rounds))) => { |
| 236 | + Some(*rounds) |
| 237 | + } |
| 238 | + (Some(RoundOrTime::Both { rounds, .. }), _) |
| 239 | + | (_, Some(RoundOrTime::Both { rounds, .. })) => Some(*rounds), |
| 240 | + _ => None, |
| 241 | + } |
| 242 | +} |
| 243 | + |
| 244 | +/// Extract time constraints from config for stop conditions |
| 245 | +fn extract_time_constraints(config: &ExecutionOptions) -> (Option<u64>, Option<u64>) { |
| 246 | + let min_time_ns = match &config.min { |
| 247 | + Some(RoundOrTime::TimeNs(time_ns)) | Some(RoundOrTime::Both { time_ns, .. }) => { |
| 248 | + Some(*time_ns) |
| 249 | + } |
| 250 | + _ => None, |
| 251 | + }; |
| 252 | + let max_time_ns = match &config.max { |
| 253 | + Some(RoundOrTime::TimeNs(time_ns)) | Some(RoundOrTime::Both { time_ns, .. }) => { |
| 254 | + Some(*time_ns) |
| 255 | + } |
| 256 | + _ => None, |
| 257 | + }; |
| 258 | + (min_time_ns, max_time_ns) |
| 259 | +} |
| 260 | + |
| 261 | +fn format_ns(ns: u64) -> String { |
| 262 | + format!("{:?}", Duration::from_nanos(ns)) |
| 263 | +} |
0 commit comments