henad_explore/
progress.rs1use 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
15pub const PROGRESS_INTERVAL: Duration = Duration::from_secs(1);
17
18#[derive(Debug, Clone, Copy)]
20pub enum ProgressEvent<'a> {
21 Planned(&'a SweepOutline),
23 Warned(&'a SweepWarning),
25 RunCommitted(&'a RunOutcome),
27 Progressed(ProgressUpdate),
29 SearchBatchTold(&'a SearchUpdate),
31 Ended(&'a SweepReport),
33}
34
35pub trait Progress {
37 fn report(&mut self, event: &ProgressEvent<'_>);
39}
40
41#[derive(Debug, Clone, Copy, Default)]
43pub struct NoProgress;
44
45impl Progress for NoProgress {
46 fn report(&mut self, _event: &ProgressEvent<'_>) {}
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct ProgressUpdate {
52 pub done: u64,
54 pub total: u64,
56 pub failed: u64,
58 pub elapsed: Duration,
60 pub remaining: Option<Duration>,
64}
65
66impl ProgressUpdate {
67 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#[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 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 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 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}