1use std::sync::Arc;
19use std::sync::atomic::{AtomicBool, Ordering};
20use std::sync::mpsc::{Receiver, Sender, channel};
21use std::time::{Duration, Instant};
22
23use anyhow::Result;
24use futures::{Stream, StreamExt};
25
26use crate::artifacts::ArtifactStore;
27use crate::benchmark::BenchmarkDescriptor;
28use crate::coherence::CoherencePolicy;
29use crate::dynamic::DynBenchmark;
30use crate::hardware::policy;
31use crate::hardware::report::HardwareStateReport;
32use crate::hardware::state::HardwareState;
33use crate::params::ParamValues;
34use crate::plugin::{PluginEvent, PluginHandle, TargetEndpoint};
35use crate::result::{BenchmarkResult, LogLine, RunStatus};
36
37pub fn drive(bench: &mut dyn DynBenchmark) -> impl Stream<Item = Result<BenchmarkResult>> + '_ {
41 futures::stream::unfold(Some(bench), |state| async move {
42 let bench = state?;
43 match bench.next().await {
44 Ok(frame) => {
45 let finished = frame.status.is_terminal();
46 Some((Ok(frame), if finished { None } else { Some(bench) }))
47 }
48 Err(e) => Some((Err(e), None)),
50 }
51 })
52}
53
54pub enum ExecutorMessage {
57 Event(PluginEvent),
58 Frame(Box<BenchmarkResult>),
59}
60
61pub struct RunHandle {
63 events: Receiver<PluginEvent>,
64 frames: Receiver<BenchmarkResult>,
65 cancel: Arc<AtomicBool>,
66 finished: Arc<AtomicBool>,
67}
68
69impl RunHandle {
70 pub fn drain(&self) -> Vec<ExecutorMessage> {
73 let mut out: Vec<ExecutorMessage> =
74 self.events.try_iter().map(ExecutorMessage::Event).collect();
75 out.extend(
76 self.frames
77 .try_iter()
78 .map(|f| ExecutorMessage::Frame(Box::new(f))),
79 );
80 out
81 }
82
83 pub fn cancel(&self) {
87 self.cancel.store(true, Ordering::Relaxed);
88 }
89
90 pub fn is_cancelled(&self) -> bool {
91 self.cancel.load(Ordering::Relaxed)
92 }
93
94 pub fn is_finished(&self) -> bool {
96 self.finished.load(Ordering::Relaxed)
97 }
98}
99
100#[derive(Clone)]
102pub struct BenchmarkExecutor {
103 runtime: tokio::runtime::Handle,
104 artifacts: ArtifactStore,
105 next_run_id: Arc<std::sync::atomic::AtomicU64>,
109}
110
111impl BenchmarkExecutor {
112 pub fn new(runtime: tokio::runtime::Handle, artifacts: ArtifactStore) -> Self {
113 Self {
114 runtime,
115 artifacts,
116 next_run_id: Arc::new(std::sync::atomic::AtomicU64::new(1)),
117 }
118 }
119
120 pub fn runtime(&self) -> &tokio::runtime::Handle {
123 &self.runtime
124 }
125
126 pub fn artifacts(&self) -> &ArtifactStore {
127 &self.artifacts
128 }
129
130 pub fn start(
135 &self,
136 descriptor: &'static BenchmarkDescriptor,
137 values: ParamValues,
138 target: TargetEndpoint,
139 coherence: CoherencePolicy,
140 ceilings: Option<crate::hardware::policy::TempCeilings>,
141 ) -> RunHandle {
142 let (event_tx, event_rx) = channel();
143 let (frame_tx, frame_rx) = channel();
144 let cancel = Arc::new(AtomicBool::new(false));
145 let finished = Arc::new(AtomicBool::new(false));
146 let handle = PluginHandle::new(
147 self.next_run_id
148 .fetch_add(1, std::sync::atomic::Ordering::Relaxed),
149 target,
150 self.artifacts.clone(),
151 event_tx.clone(),
152 cancel.clone(),
153 );
154 let task = RunTask {
155 descriptor,
156 values,
157 coherence,
158 handle,
159 events: event_tx,
160 frames: frame_tx,
161 cancel: cancel.clone(),
162 finished: finished.clone(),
163 ceilings,
164 };
165 self.runtime.spawn(task.execute());
166 RunHandle {
167 events: event_rx,
168 frames: frame_rx,
169 cancel,
170 finished,
171 }
172 }
173}
174
175const COHERENCE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
178
179struct RunTask {
180 descriptor: &'static BenchmarkDescriptor,
181 values: ParamValues,
182 handle: PluginHandle,
183 events: Sender<PluginEvent>,
184 frames: Sender<BenchmarkResult>,
185 cancel: Arc<AtomicBool>,
186 finished: Arc<AtomicBool>,
187 coherence: CoherencePolicy,
188 ceilings: Option<crate::hardware::policy::TempCeilings>,
191}
192
193impl RunTask {
194 async fn probe_coherence(&self) {
197 if self.coherence == CoherencePolicy::Skip {
198 return;
199 }
200 self.handle.status("checking the endpoint".to_string());
201 let report = crate::coherence::probe_for(
202 self.handle.target(),
203 self.descriptor.intended_for,
204 COHERENCE_TIMEOUT,
205 )
206 .await;
207 match report.concern(self.handle.target()) {
208 Some(concern) => self.handle.warn(concern),
209 None => {
210 for a in &report.answers {
211 self.handle
212 .info(format!("endpoint check {}: {:?}", a.label, a.answer.trim()));
213 }
214 }
215 }
216 }
217
218 async fn open_hardware_report(&self) -> Option<HardwareStateReport> {
225 let sensitivity = self.descriptor.sensitivity;
226 let before = tokio::task::spawn_blocking(HardwareState::collect)
227 .await
228 .ok()?;
229 self.handle
230 .info(format!("box state: {}", before.one_line()));
231 let report = HardwareStateReport::opened(sensitivity, before, self.ceilings);
232 if policy::PolicyOptions::from_env().kill_switch {
233 self.handle.warn(format!(
236 "{}=1 — the hardware pre-check CANNOT refuse this run. Whatever it found is \
237 recorded below and travels with the record.",
238 policy::KILL_SWITCH_ENV
239 ));
240 }
241 for concern in &report.precheck.concerns {
242 self.handle.warn(format!("hardware: {concern}"));
243 }
244 Some(report)
245 }
246
247 async fn close_hardware(
251 &self,
252 mut frame: BenchmarkResult,
253 report: Option<HardwareStateReport>,
254 ) -> BenchmarkResult {
255 let Some(mut report) = report else {
256 return frame;
257 };
258 if let Ok(after) = tokio::task::spawn_blocking(HardwareState::collect).await {
259 report.close(after);
260 }
261 if let Some(post) = &report.postcheck {
262 for concern in &post.concerns {
263 self.handle.warn(format!("hardware: {concern}"));
264 }
265 }
266 if report.invalidated() && frame.status == RunStatus::Completed {
268 self.handle.warn(
269 "hardware: this run's SPEED numbers are marked INVALID — the box throttled \
270 while it was measuring. Re-run on a box that does not."
271 .to_string(),
272 );
273 }
274 frame.hardware_state = Some(report);
275 frame
276 }
277
278 async fn execute(self) {
279 let started = Instant::now();
280 self.handle.set_glow(true);
281 let mut bench = self.descriptor.build();
282
283 let hardware = self.open_hardware_report().await;
286 if let Some(report) = hardware.as_ref().filter(|r| r.refuses()) {
287 let frame = BenchmarkResult::failed(
288 "hardware precheck",
289 format!(
290 "this box is not in a state to produce a comparable SPEED number: {}. \
291 Set {}=1 to measure anyway.",
292 report.precheck.concerns.join("; "),
293 policy::KILL_SWITCH_ENV
294 ),
295 started.elapsed(),
296 );
297 let _ = self.frames.send(frame.with_hardware_state(report.clone()));
300 self.teardown(bench.as_mut()).await;
301 return;
302 }
303
304 self.probe_coherence().await;
309 let setup = async {
310 bench.load(self.handle.clone()).await?;
311 bench.configure(&self.values)
312 }
313 .await;
314 if let Err(e) = setup {
315 self.emit_failed("setup", e, started.elapsed(), hardware)
316 .await;
317 self.teardown(bench.as_mut()).await;
318 return;
319 }
320
321 let mut terminal: Option<BenchmarkResult> = None;
324 {
325 let stream = drive(bench.as_mut());
326 futures::pin_mut!(stream);
327 while let Some(item) = stream.next().await {
328 match item {
329 Ok(frame) => {
330 if frame.status.is_terminal() {
331 terminal = Some(frame);
332 break;
333 }
334 if self.frames.send(frame).is_err() {
335 break;
336 }
337 }
338 Err(e) => {
340 terminal = Some(BenchmarkResult::failed(
341 "run",
342 format!("{e:#}"),
343 started.elapsed(),
344 ));
345 break;
346 }
347 }
348 if self.cancel.load(Ordering::Relaxed) {
349 terminal = Some(BenchmarkResult::failed(
350 "cancelled",
351 "cancelled by user",
352 started.elapsed(),
353 ));
354 break;
355 }
356 }
357 }
358 if let Some(frame) = terminal {
359 let frame = self.close_hardware(frame, hardware).await;
360 let _ = self.frames.send(frame);
361 }
362
363 self.teardown(bench.as_mut()).await;
364 }
365
366 async fn emit_failed(
367 &self,
368 phase: &str,
369 error: anyhow::Error,
370 elapsed: Duration,
371 hardware: Option<HardwareStateReport>,
372 ) {
373 let frame = BenchmarkResult::failed(phase, format!("{error:#}"), elapsed);
374 let frame = self.close_hardware(frame, hardware).await;
375 let _ = self.frames.send(frame);
376 }
377
378 async fn teardown(&self, bench: &mut dyn DynBenchmark) {
379 if let Err(e) = bench.cleanup().await {
380 let _ = self
381 .events
382 .send(PluginEvent::Log(LogLine::warn(format!("cleanup: {e:#}"))));
383 }
384 let _ = self.events.send(PluginEvent::Glow(false));
385 self.finished.store(true, Ordering::Relaxed);
386 }
387}
388
389pub fn is_final(frame: &BenchmarkResult) -> bool {
392 frame.status.is_terminal()
393}
394
395pub fn terminal_status(ok: bool) -> RunStatus {
397 if ok {
398 RunStatus::Completed
399 } else {
400 RunStatus::Failed
401 }
402}
403
404#[cfg(test)]
405#[path = "executor_tests.rs"]
406mod tests;