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