hashiverse_lib/tools/
pow_required_estimator.rs1use crate::tools::time::{DurationMillis, TimeMillis, MILLIS_IN_MILLISECOND, MILLIS_IN_SECOND};
19use crate::tools::types::Pow;
20
21pub struct PowRequiredEstimator {
39 description: String,
40 pow_required: Pow,
41 total_iterations: usize,
42 best_pow_so_far: Pow,
43 started_at_millis: TimeMillis,
44 next_report_millis: TimeMillis,
45}
46
47const REPORTING_PERIOD_MILLIS: DurationMillis = MILLIS_IN_SECOND.const_mul(1);
48
49impl PowRequiredEstimator {
50 pub fn new(started_at_millis: TimeMillis, description: &str, pow_required: Pow) -> Self {
51 Self {
52 description: description.to_string(),
53 pow_required,
54 total_iterations: 0,
55 best_pow_so_far: Pow(0),
56 started_at_millis,
57 next_report_millis: started_at_millis + REPORTING_PERIOD_MILLIS,
58 }
59 }
60
61 fn report_large_number(num: usize) -> String {
62 let num = num as f64;
63
64 if num > 1e9 {
65 return format!("{:.2}B", num / 1e9);
66 }
67 if num > 1e6 {
68 return format!("{:.2}M", num / 1e6);
69 }
70 if num > 1e3 {
71 return format!("{:.2}k", num / 1e3);
72 }
73
74 format!("{}", num)
75 }
76
77 pub fn record_batch_and_estimate(&mut self, current_time_millis: TimeMillis, iterations_in_batch: usize, best_pow_in_batch: Pow) -> Option<String> {
79 self.total_iterations += iterations_in_batch;
80 if best_pow_in_batch > self.best_pow_so_far {
81 self.best_pow_so_far = best_pow_in_batch;
82 }
83
84 if current_time_millis < self.next_report_millis {
86 return None;
87 }
88 self.next_report_millis = current_time_millis + REPORTING_PERIOD_MILLIS;
89
90 let elapsed_duration_millis = current_time_millis - self.started_at_millis;
91
92 if elapsed_duration_millis < MILLIS_IN_MILLISECOND || self.total_iterations == 0 {
93 return Some(format!(
94 "{}: PoW {}/{} bits | {} | {} iters | too early to estimate",
95 self.description,
96 self.best_pow_so_far.0,
97 self.pow_required.0,
98 elapsed_duration_millis,
99 Self::report_large_number(self.total_iterations)
100 ));
101 }
102
103 let iterations_per_second = 1000.0 * self.total_iterations as f64 / elapsed_duration_millis.0 as f64;
104 let expected_total_iterations = (2.0f64).powi(self.pow_required.0 as i32);
108 let expected_remaining_duration = DurationMillis((1000.0 * expected_total_iterations / iterations_per_second) as i64);
109 let eta_one_sigma = expected_remaining_duration;
111 let progress_pct = self.total_iterations as f64 / expected_total_iterations * 100.0;
112
113 Some(format!(
114 "{}: PoW {}/{} bits | {} | {} iters | {}/s | {:.1}% of expected | ETA ~{} \u{00b1}{}",
115 self.description,
116 self.best_pow_so_far.0,
117 self.pow_required.0,
118 elapsed_duration_millis,
119 Self::report_large_number(self.total_iterations),
120 Self::report_large_number(iterations_per_second as usize),
121 progress_pct,
122 expected_remaining_duration,
123 eta_one_sigma,
124 ))
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 fn make_estimator() -> PowRequiredEstimator {
133 PowRequiredEstimator::new(TimeMillis(0), "test", Pow(24))
134 }
135
136 #[test]
137 fn record_batch_accumulates_iterations_and_tracks_best_pow() {
138 let mut estimator = make_estimator();
139
140 estimator.record_batch_and_estimate(TimeMillis(100), 1024, Pow(10));
141 assert_eq!(estimator.total_iterations, 1024);
142 assert_eq!(estimator.best_pow_so_far, Pow(10));
143
144 estimator.record_batch_and_estimate(TimeMillis(200), 1024, Pow(8)); assert_eq!(estimator.total_iterations, 2048);
146 assert_eq!(estimator.best_pow_so_far, Pow(10));
147
148 estimator.record_batch_and_estimate(TimeMillis(300), 1024, Pow(15)); assert_eq!(estimator.total_iterations, 3072);
150 assert_eq!(estimator.best_pow_so_far, Pow(15));
151 }
152
153 #[test]
154 fn progress_string_contains_key_fields() {
155 let mut estimator = make_estimator();
156 let output = estimator.record_batch_and_estimate(TimeMillis(1000), 65536, Pow(18));
157 assert!(output.is_some());
158
159 let output = output.unwrap();
160 assert!(output.contains("test"), "should include description: {}", output);
161 assert!(output.contains("18/24"), "should show best/required bits: {}", output);
162 assert!(output.contains("1s"), "should show elapsed time: {}", output);
163 assert!(output.contains("65.54"), "should show iteration count: {}", output);
164 assert!(output.contains("ETA"), "should show ETA: {}", output);
165 }
166
167 #[test]
168 fn progress_string_before_any_elapsed_time() {
169 let mut estimator = make_estimator();
170 let output = estimator.record_batch_and_estimate(TimeMillis(100), 1024, Pow(5));
171 assert!(output.is_none());
172 }
173
174 #[test]
175 fn eta_stays_non_negative_past_expected_iterations() {
176 let mut estimator = PowRequiredEstimator::new(TimeMillis(0), "test", Pow(4));
179
180 let output = estimator
181 .record_batch_and_estimate(TimeMillis(1000), 100, Pow(3))
182 .expect("should emit a report once the reporting period has elapsed");
183
184 let after_eta = output.split("ETA ~").nth(1).expect("output should contain ETA marker");
185 let eta_str = after_eta.split(' ').next().expect("ETA value should be the first token after the marker");
186 let eta = DurationMillis::parse(eta_str).expect("ETA value should parse as a DurationMillis");
187
188 assert!(eta.0 >= 0, "ETA must be non-negative past expected iterations; got {:?} in {:?}", eta_str, output);
189 }
190}