1use std::time::{Duration, Instant};
7
8#[derive(Debug, Clone, Copy, Default, PartialEq)]
33pub struct SolverTelemetry {
34 pub elapsed: Duration,
35 pub step_count: u64,
36 pub moves_generated: u64,
37 pub moves_evaluated: u64,
38 pub moves_accepted: u64,
39 pub score_calculations: u64,
40 pub generation_time: Duration,
41 pub evaluation_time: Duration,
42}
43
44#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
45pub struct Throughput {
46 pub count: u64,
47 pub elapsed: Duration,
48}
49
50pub(crate) fn whole_units_per_second(count: u64, elapsed: Duration) -> u128 {
51 let nanos = elapsed.as_nanos();
52 if nanos == 0 {
53 0
54 } else {
55 u128::from(count)
56 .saturating_mul(1_000_000_000)
57 .checked_div(nanos)
58 .unwrap_or(0)
59 }
60}
61
62pub(crate) fn format_duration(duration: Duration) -> String {
63 let secs = duration.as_secs();
64 let nanos = duration.subsec_nanos();
65
66 if secs >= 60 {
67 let mins = secs / 60;
68 let rem_secs = secs % 60;
69 return format!("{mins}m {rem_secs}s");
70 }
71
72 if secs > 0 {
73 let millis = nanos / 1_000_000;
74 if millis == 0 {
75 return format!("{secs}s");
76 }
77 return format!("{secs}s {millis}ms");
78 }
79
80 let millis = nanos / 1_000_000;
81 if millis > 0 {
82 return format!("{millis}ms");
83 }
84
85 let micros = nanos / 1_000;
86 if micros > 0 {
87 return format!("{micros}us");
88 }
89
90 format!("{nanos}ns")
91}
92
93#[derive(Debug, Default)]
94pub struct SolverStats {
95 start_time: Option<Instant>,
96 pause_started_at: Option<Instant>,
97 pub step_count: u64,
99 pub moves_generated: u64,
101 pub moves_evaluated: u64,
103 pub moves_accepted: u64,
105 pub score_calculations: u64,
107 generation_time: Duration,
108 evaluation_time: Duration,
109}
110
111impl SolverStats {
112 pub fn start(&mut self) {
114 self.start_time = Some(Instant::now());
115 self.pause_started_at = None;
116 }
117
118 pub fn elapsed(&self) -> Duration {
119 match (self.start_time, self.pause_started_at) {
120 (Some(start), Some(paused_at)) => paused_at.duration_since(start),
121 (Some(start), None) => start.elapsed(),
122 _ => Duration::default(),
123 }
124 }
125
126 pub fn pause(&mut self) {
127 if self.start_time.is_some() && self.pause_started_at.is_none() {
128 self.pause_started_at = Some(Instant::now());
129 }
130 }
131
132 pub fn resume(&mut self) {
133 if let (Some(start), Some(paused_at)) = (self.start_time, self.pause_started_at.take()) {
134 self.start_time = Some(start + paused_at.elapsed());
135 }
136 }
137
138 pub fn record_generated_batch(&mut self, count: u64, duration: Duration) {
140 self.moves_generated += count;
141 self.generation_time += duration;
142 }
143
144 pub fn record_generation_time(&mut self, duration: Duration) {
146 self.generation_time += duration;
147 }
148
149 pub fn record_generated_move(&mut self, duration: Duration) {
151 self.record_generated_batch(1, duration);
152 }
153
154 pub fn record_evaluated_move(&mut self, duration: Duration) {
156 self.moves_evaluated += 1;
157 self.evaluation_time += duration;
158 }
159
160 pub fn record_move_accepted(&mut self) {
162 self.moves_accepted += 1;
163 }
164
165 pub fn record_step(&mut self) {
167 self.step_count += 1;
168 }
169
170 pub fn record_score_calculation(&mut self) {
172 self.score_calculations += 1;
173 }
174
175 pub fn generated_throughput(&self) -> Throughput {
176 Throughput {
177 count: self.moves_generated,
178 elapsed: self.generation_time,
179 }
180 }
181
182 pub fn evaluated_throughput(&self) -> Throughput {
183 Throughput {
184 count: self.moves_evaluated,
185 elapsed: self.evaluation_time,
186 }
187 }
188
189 pub fn acceptance_rate(&self) -> f64 {
190 if self.moves_evaluated == 0 {
191 0.0
192 } else {
193 self.moves_accepted as f64 / self.moves_evaluated as f64
194 }
195 }
196
197 pub fn generation_time(&self) -> Duration {
198 self.generation_time
199 }
200
201 pub fn evaluation_time(&self) -> Duration {
202 self.evaluation_time
203 }
204
205 pub fn snapshot(&self) -> SolverTelemetry {
206 SolverTelemetry {
207 elapsed: self.elapsed(),
208 step_count: self.step_count,
209 moves_generated: self.moves_generated,
210 moves_evaluated: self.moves_evaluated,
211 moves_accepted: self.moves_accepted,
212 score_calculations: self.score_calculations,
213 generation_time: self.generation_time,
214 evaluation_time: self.evaluation_time,
215 }
216 }
217}
218
219#[derive(Debug)]
242pub struct PhaseStats {
243 pub phase_index: usize,
245 pub phase_type: &'static str,
247 start_time: Instant,
248 pub step_count: u64,
250 pub moves_generated: u64,
252 pub moves_evaluated: u64,
254 pub moves_accepted: u64,
256 pub score_calculations: u64,
258 generation_time: Duration,
259 evaluation_time: Duration,
260}
261
262impl PhaseStats {
263 pub fn new(phase_index: usize, phase_type: &'static str) -> Self {
265 Self {
266 phase_index,
267 phase_type,
268 start_time: Instant::now(),
269 step_count: 0,
270 moves_generated: 0,
271 moves_evaluated: 0,
272 moves_accepted: 0,
273 score_calculations: 0,
274 generation_time: Duration::default(),
275 evaluation_time: Duration::default(),
276 }
277 }
278
279 pub fn elapsed(&self) -> Duration {
280 self.start_time.elapsed()
281 }
282
283 pub fn record_step(&mut self) {
285 self.step_count += 1;
286 }
287
288 pub fn record_generated_batch(&mut self, count: u64, duration: Duration) {
290 self.moves_generated += count;
291 self.generation_time += duration;
292 }
293
294 pub fn record_generation_time(&mut self, duration: Duration) {
296 self.generation_time += duration;
297 }
298
299 pub fn record_generated_move(&mut self, duration: Duration) {
301 self.record_generated_batch(1, duration);
302 }
303
304 pub fn record_evaluated_move(&mut self, duration: Duration) {
306 self.moves_evaluated += 1;
307 self.evaluation_time += duration;
308 }
309
310 pub fn record_move_accepted(&mut self) {
312 self.moves_accepted += 1;
313 }
314
315 pub fn record_score_calculation(&mut self) {
317 self.score_calculations += 1;
318 }
319
320 pub fn generated_throughput(&self) -> Throughput {
321 Throughput {
322 count: self.moves_generated,
323 elapsed: self.generation_time,
324 }
325 }
326
327 pub fn evaluated_throughput(&self) -> Throughput {
328 Throughput {
329 count: self.moves_evaluated,
330 elapsed: self.evaluation_time,
331 }
332 }
333
334 pub fn acceptance_rate(&self) -> f64 {
335 if self.moves_evaluated == 0 {
336 0.0
337 } else {
338 self.moves_accepted as f64 / self.moves_evaluated as f64
339 }
340 }
341
342 pub fn generation_time(&self) -> Duration {
343 self.generation_time
344 }
345
346 pub fn evaluation_time(&self) -> Duration {
347 self.evaluation_time
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354
355 #[test]
356 fn solver_snapshot_preserves_exact_counts_and_durations() {
357 let mut stats = SolverStats::default();
358 stats.start();
359 stats.record_step();
360 stats.record_generated_batch(3, Duration::from_millis(4));
361 stats.record_evaluated_move(Duration::from_millis(5));
362 stats.record_move_accepted();
363 stats.record_score_calculation();
364
365 let snapshot = stats.snapshot();
366
367 assert_eq!(snapshot.step_count, 1);
368 assert_eq!(snapshot.moves_generated, 3);
369 assert_eq!(snapshot.moves_evaluated, 1);
370 assert_eq!(snapshot.moves_accepted, 1);
371 assert_eq!(snapshot.score_calculations, 1);
372 assert_eq!(snapshot.generation_time, Duration::from_millis(4));
373 assert_eq!(snapshot.evaluation_time, Duration::from_millis(5));
374 }
375
376 #[test]
377 fn phase_stats_track_generation_and_evaluation_separately() {
378 let mut stats = PhaseStats::new(2, "LocalSearch");
379 stats.record_step();
380 stats.record_generated_batch(7, Duration::from_millis(8));
381 stats.record_evaluated_move(Duration::from_millis(9));
382 stats.record_move_accepted();
383 stats.record_score_calculation();
384
385 assert_eq!(stats.phase_index, 2);
386 assert_eq!(stats.phase_type, "LocalSearch");
387 assert_eq!(stats.step_count, 1);
388 assert_eq!(stats.moves_generated, 7);
389 assert_eq!(stats.moves_evaluated, 1);
390 assert_eq!(stats.moves_accepted, 1);
391 assert_eq!(stats.score_calculations, 1);
392 assert_eq!(stats.generation_time(), Duration::from_millis(8));
393 assert_eq!(stats.evaluation_time(), Duration::from_millis(9));
394 }
395
396 #[test]
397 fn throughput_helpers_use_stage_specific_durations() {
398 let mut solver_stats = SolverStats::default();
399 solver_stats.start();
400 solver_stats.record_generated_batch(5, Duration::from_millis(7));
401 solver_stats.record_evaluated_move(Duration::from_millis(11));
402
403 let mut phase_stats = PhaseStats::new(1, "LocalSearch");
404 phase_stats.record_generated_batch(3, Duration::from_millis(13));
405 phase_stats.record_evaluated_move(Duration::from_millis(17));
406
407 assert_eq!(
408 solver_stats.generated_throughput(),
409 Throughput {
410 count: 5,
411 elapsed: Duration::from_millis(7),
412 }
413 );
414 assert_eq!(
415 solver_stats.evaluated_throughput(),
416 Throughput {
417 count: 1,
418 elapsed: Duration::from_millis(11),
419 }
420 );
421 assert_eq!(
422 phase_stats.generated_throughput(),
423 Throughput {
424 count: 3,
425 elapsed: Duration::from_millis(13),
426 }
427 );
428 assert_eq!(
429 phase_stats.evaluated_throughput(),
430 Throughput {
431 count: 1,
432 elapsed: Duration::from_millis(17),
433 }
434 );
435 }
436
437 #[test]
438 fn whole_units_per_second_uses_integer_rate_math() {
439 assert_eq!(whole_units_per_second(3, Duration::from_millis(2_000)), 1);
440 assert_eq!(whole_units_per_second(9, Duration::from_secs(2)), 4);
441 assert_eq!(whole_units_per_second(5, Duration::ZERO), 0);
442 }
443
444 #[test]
445 fn format_duration_uses_exact_integer_units() {
446 assert_eq!(format_duration(Duration::from_millis(750)), "750ms");
447 assert_eq!(format_duration(Duration::from_millis(2_500)), "2s 500ms");
448 assert_eq!(format_duration(Duration::from_secs(125)), "2m 5s");
449 assert_eq!(format_duration(Duration::from_micros(42)), "42us");
450 }
451}