Skip to content

Commit 4262c75

Browse files
feat(exec-harness): make the config less strict about its config
1 parent 8a39cf5 commit 4262c75

File tree

4 files changed

+311
-210
lines changed

4 files changed

+311
-210
lines changed
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
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

Comments
 (0)