Skip to main content

henad_explore/
progress.rs

1//! Progress of a sweep or search, reported to the host as events.
2//!
3//! The library never prints. A host renders the events as it likes, such as a line on a terminal, JSON lines or a
4//! panel.
5
6use std::time::Duration;
7
8use web_time::Instant;
9
10use henad_core::explore::outcome::{RunOutcome, RunStatus};
11
12use crate::search_run::SearchUpdate;
13use crate::sweep::{SweepOutline, SweepReport, SweepWarning};
14
15/// Shortest time between two [`ProgressEvent::Progressed`] events.
16pub const PROGRESS_INTERVAL: Duration = Duration::from_secs(1);
17
18/// Event in the progress of a sweep.
19#[derive(Debug, Clone, Copy)]
20pub enum ProgressEvent<'a> {
21    /// The sweep is planned and probed. A dry run ends here.
22    Planned(&'a SweepOutline),
23    /// A warning. The sweep still runs, but likely not as intended.
24    Warned(&'a SweepWarning),
25    /// A run was written. Runs arrive in plan order, and a search's runs in the order that it requests them.
26    RunCommitted(&'a RunOutcome),
27    /// Runs have finished since the last update. Sent at most once per [`PROGRESS_INTERVAL`].
28    Progressed(ProgressUpdate),
29    /// A search was told the evaluations of one batch. Batches arrive in order, each after its runs.
30    SearchBatchTold(&'a SearchUpdate),
31    /// The sweep ended without an error.
32    Ended(&'a SweepReport),
33}
34
35/// Receiver of a sweep's progress.
36pub trait Progress {
37    /// Receives `event` as it happens.
38    fn report(&mut self, event: &ProgressEvent<'_>);
39}
40
41/// Progress that reports nowhere.
42#[derive(Debug, Clone, Copy, Default)]
43pub struct NoProgress;
44
45impl Progress for NoProgress {
46    fn report(&mut self, _event: &ProgressEvent<'_>) {}
47}
48
49/// Runs finished so far, and the time left at the pace so far.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct ProgressUpdate {
52    /// Number of runs finished so far. A run counts once it finishes, before it is written.
53    pub done: u64,
54    /// Number of runs this session executes.
55    pub total: u64,
56    /// Number of finished runs that ended on a fault or a timeout.
57    pub failed: u64,
58    /// Time since the [`ProgressMeter`] started. The meter starts before the first run is built.
59    pub elapsed: Duration,
60    /// Time left at the mean pace of the finished runs, `None` before the first run finishes.
61    ///
62    /// A time too long for a [`Duration`] is `None` as well.
63    pub remaining: Option<Duration>,
64}
65
66impl ProgressUpdate {
67    /// Returns the update after `done` of `total` runs in `elapsed`, `failed` of them on a fault or a timeout.
68    pub fn new(done: u64, total: u64, failed: u64, elapsed: Duration) -> Self {
69        let remaining = (done > 0)
70            .then(|| elapsed.as_secs_f64() / done as f64 * total.saturating_sub(done) as f64)
71            .and_then(|seconds| Duration::try_from_secs_f64(seconds).ok());
72        Self {
73            done,
74            total,
75            failed,
76            elapsed,
77            remaining,
78        }
79    }
80}
81
82/// Counter of finished runs that yields an update at most once per [`PROGRESS_INTERVAL`].
83#[derive(Debug, Clone)]
84pub struct ProgressMeter {
85    total: u64,
86    done: u64,
87    failed: u64,
88    started: Instant,
89    last_update: Instant,
90}
91
92impl ProgressMeter {
93    /// Returns a meter for `total` runs, started now.
94    pub fn new(total: u64) -> Self {
95        let now = Instant::now();
96        Self {
97            total,
98            done: 0,
99            failed: 0,
100            started: now,
101            last_update: now,
102        }
103    }
104
105    /// Counts a finished run with `status`. Returns an update once [`PROGRESS_INTERVAL`] has passed since the last
106    /// update.
107    pub fn record_finished_run(&mut self, status: RunStatus) -> Option<ProgressUpdate> {
108        self.tally(status);
109        let now = Instant::now();
110        if now.duration_since(self.last_update) < PROGRESS_INTERVAL {
111            return None;
112        }
113        self.last_update = now;
114        Some(self.update())
115    }
116
117    fn tally(&mut self, status: RunStatus) {
118        self.done += 1;
119        if status.is_failure() {
120            self.failed += 1;
121        }
122    }
123
124    /// Returns the update for the runs counted so far.
125    pub fn update(&self) -> ProgressUpdate {
126        ProgressUpdate::new(self.done, self.total, self.failed, self.started.elapsed())
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use std::time::Duration;
133
134    use henad_core::explore::outcome::RunStatus;
135
136    use super::{PROGRESS_INTERVAL, ProgressMeter, ProgressUpdate};
137
138    #[test]
139    fn the_remaining_time_scales_the_elapsed_time_by_the_runs_left() {
140        let update = ProgressUpdate::new(4, 10, 1, Duration::from_secs(8));
141        assert_eq!(update.remaining, Some(Duration::from_secs(12)));
142        assert_eq!(update.elapsed, Duration::from_secs(8));
143        assert_eq!(ProgressUpdate::new(0, 10, 0, Duration::from_secs(3)).remaining, None);
144        assert_eq!(
145            ProgressUpdate::new(10, 10, 0, Duration::from_secs(3)).remaining,
146            Some(Duration::ZERO)
147        );
148        assert_eq!(
149            ProgressUpdate::new(1, u64::MAX, 0, Duration::from_secs(2)).remaining,
150            None,
151            "a time too long for a duration reads as none"
152        );
153    }
154
155    #[test]
156    fn the_meter_updates_at_most_once_per_interval() {
157        let mut meter = ProgressMeter::new(3);
158        assert_eq!(
159            meter.record_finished_run(RunStatus::Ok),
160            None,
161            "the interval has not passed"
162        );
163        meter.last_update = meter
164            .last_update
165            .checked_sub(PROGRESS_INTERVAL)
166            .expect("the clock has run for longer than one interval");
167        let update = meter
168            .record_finished_run(RunStatus::Panicked)
169            .expect("the interval has passed");
170        assert_eq!((update.done, update.total, update.failed), (2, 3, 1));
171        assert_eq!(
172            meter.record_finished_run(RunStatus::NonFinite),
173            None,
174            "the interval starts again"
175        );
176        assert_eq!((meter.update().done, meter.update().failed), (3, 1));
177    }
178}