solverforge_solver/stats/
solver.rs1use std::cmp::Ordering;
2use std::collections::BTreeMap;
3use std::time::{Duration, Instant};
4
5use super::{
6 AppliedMoveTelemetry, CandidatePullTelemetry, CandidateTraceDisposition,
7 CandidateTracePhasePlan, CandidateTracePullToken, CandidateTraceRecordDecision,
8 CandidateTraceTelemetry, MoveTelemetry, SelectorTelemetry, SolverTelemetry, Throughput,
9};
10
11const APPLIED_MOVE_TRACE_LIMIT: usize = 8;
12
13#[derive(Debug, Default)]
14pub struct SolverStats {
15 start_time: Option<Instant>,
16 pause_started_at: Option<Instant>,
17 pub step_count: u64,
19 pub moves_generated: u64,
22 pub moves_evaluated: u64,
24 pub moves_accepted: u64,
26 pub moves_applied: u64,
28 pub moves_not_doable: u64,
29 pub moves_acceptor_rejected: u64,
30 pub moves_forager_ignored: u64,
31 pub moves_hard_improving: u64,
32 pub moves_hard_neutral: u64,
33 pub moves_hard_worse: u64,
34 pub conflict_repair_provider_generated: u64,
35 pub conflict_repair_duplicate_filtered: u64,
36 pub conflict_repair_illegal_filtered: u64,
37 pub conflict_repair_not_doable_filtered: u64,
38 pub conflict_repair_hard_improving: u64,
39 pub conflict_repair_exposed: u64,
40 pub score_calculations: u64,
42 pub construction_slots_assigned: u64,
43 pub construction_slots_kept: u64,
44 pub construction_slots_no_doable: u64,
45 pub scalar_assignment_required_remaining: u64,
46 scalar_assignment_required_remaining_by_group: BTreeMap<&'static str, u64>,
47 generation_time: Duration,
48 evaluation_time: Duration,
49 selector_stats: Vec<SelectorTelemetry>,
50 move_stats: BTreeMap<&'static str, MoveTelemetry>,
51 applied_move_trace: Vec<AppliedMoveTelemetry>,
52 candidate_trace: Option<CandidateTraceTelemetry>,
53}
54
55impl SolverStats {
56 pub fn start(&mut self) {
58 self.start_time = Some(Instant::now());
59 self.pause_started_at = None;
60 }
61
62 pub fn elapsed(&self) -> Duration {
63 match (self.start_time, self.pause_started_at) {
64 (Some(start), Some(paused_at)) => paused_at.duration_since(start),
65 (Some(start), None) => start.elapsed(),
66 _ => Duration::default(),
67 }
68 }
69
70 pub fn pause(&mut self) {
71 if self.start_time.is_some() && self.pause_started_at.is_none() {
72 self.pause_started_at = Some(Instant::now());
73 }
74 }
75
76 pub fn resume(&mut self) {
77 if let (Some(start), Some(paused_at)) = (self.start_time, self.pause_started_at.take()) {
78 self.start_time = Some(start + paused_at.elapsed());
79 }
80 }
81
82 pub fn record_generated_batch(&mut self, count: u64, duration: Duration) {
84 self.moves_generated += count;
85 self.generation_time += duration;
86 }
87
88 pub fn record_selector_generated(
89 &mut self,
90 selector_index: usize,
91 count: u64,
92 duration: Duration,
93 ) {
94 self.record_generated_batch(count, duration);
95 let selector = self.selector_stats_entry(selector_index);
96 selector.moves_generated += count;
97 selector.generation_time += duration;
98 }
99
100 pub fn record_generation_time(&mut self, duration: Duration) {
102 self.generation_time += duration;
103 }
104
105 pub fn record_generated_move(&mut self, duration: Duration) {
107 self.record_generated_batch(1, duration);
108 }
109
110 pub fn record_evaluated_move(&mut self, duration: Duration) {
112 self.moves_evaluated += 1;
113 self.evaluation_time += duration;
114 }
115
116 pub fn record_selector_evaluated(&mut self, selector_index: usize, duration: Duration) {
117 self.record_evaluated_move(duration);
118 let selector = self.selector_stats_entry(selector_index);
119 selector.moves_evaluated += 1;
120 selector.evaluation_time += duration;
121 }
122
123 pub fn record_move_accepted(&mut self) {
125 self.moves_accepted += 1;
126 }
127
128 pub fn record_selector_accepted(&mut self, selector_index: usize) {
129 self.record_move_accepted();
130 self.selector_stats_entry(selector_index).moves_accepted += 1;
131 }
132
133 pub fn record_move_applied(&mut self) {
134 self.moves_applied += 1;
135 }
136
137 pub fn record_selector_applied(&mut self, selector_index: usize) {
138 self.record_move_applied();
139 self.selector_stats_entry(selector_index).moves_applied += 1;
140 }
141
142 pub fn record_move_not_doable(&mut self) {
143 self.moves_not_doable += 1;
144 }
145
146 pub fn record_selector_not_doable(&mut self, selector_index: usize) {
147 self.record_move_not_doable();
148 self.selector_stats_entry(selector_index).moves_not_doable += 1;
149 }
150
151 pub fn record_move_acceptor_rejected(&mut self) {
152 self.moves_acceptor_rejected += 1;
153 }
154
155 pub fn record_selector_acceptor_rejected(&mut self, selector_index: usize) {
156 self.record_move_acceptor_rejected();
157 self.selector_stats_entry(selector_index)
158 .moves_acceptor_rejected += 1;
159 }
160
161 pub fn record_moves_forager_ignored(&mut self, count: u64) {
162 self.moves_forager_ignored += count;
163 }
164
165 pub fn record_move_hard_improving(&mut self) {
166 self.moves_hard_improving += 1;
167 }
168
169 pub fn record_move_hard_neutral(&mut self) {
170 self.moves_hard_neutral += 1;
171 }
172
173 pub fn record_move_hard_worse(&mut self) {
174 self.moves_hard_worse += 1;
175 }
176
177 pub fn record_conflict_repair_provider_generated(&mut self, count: u64) {
178 self.conflict_repair_provider_generated += count;
179 }
180
181 pub fn record_conflict_repair_duplicate_filtered(&mut self) {
182 self.conflict_repair_duplicate_filtered += 1;
183 }
184
185 pub fn record_conflict_repair_illegal_filtered(&mut self) {
186 self.conflict_repair_illegal_filtered += 1;
187 }
188
189 pub fn record_conflict_repair_not_doable_filtered(&mut self) {
190 self.conflict_repair_not_doable_filtered += 1;
191 }
192
193 pub fn record_conflict_repair_hard_improving(&mut self) {
194 self.conflict_repair_hard_improving += 1;
195 }
196
197 pub fn record_conflict_repair_exposed(&mut self) {
198 self.conflict_repair_exposed += 1;
199 }
200
201 pub fn record_step(&mut self) {
203 self.step_count += 1;
204 }
205
206 pub fn record_score_calculation(&mut self) {
208 self.score_calculations += 1;
209 }
210
211 pub fn record_construction_slot_assigned(&mut self) {
212 self.construction_slots_assigned += 1;
213 }
214
215 pub fn record_construction_slot_kept(&mut self) {
216 self.construction_slots_kept += 1;
217 }
218
219 pub fn record_construction_slot_no_doable(&mut self) {
220 self.construction_slots_no_doable += 1;
221 }
222
223 pub fn record_scalar_assignment_required_remaining(
224 &mut self,
225 group_name: &'static str,
226 count: u64,
227 ) {
228 self.scalar_assignment_required_remaining_by_group
229 .insert(group_name, count);
230 self.scalar_assignment_required_remaining = self
231 .scalar_assignment_required_remaining_by_group
232 .values()
233 .copied()
234 .sum();
235 }
236
237 pub fn generated_throughput(&self) -> Throughput {
238 Throughput {
239 count: self.moves_generated,
240 elapsed: self.generation_time,
241 }
242 }
243
244 pub fn evaluated_throughput(&self) -> Throughput {
245 Throughput {
246 count: self.moves_evaluated,
247 elapsed: self.evaluation_time,
248 }
249 }
250
251 pub fn acceptance_rate(&self) -> f64 {
252 if self.moves_evaluated == 0 {
253 0.0
254 } else {
255 self.moves_accepted as f64 / self.moves_evaluated as f64
256 }
257 }
258
259 pub fn generation_time(&self) -> Duration {
260 self.generation_time
261 }
262
263 pub fn evaluation_time(&self) -> Duration {
264 self.evaluation_time
265 }
266
267 pub fn snapshot(&self) -> SolverTelemetry {
268 self.snapshot_with_applied_move_trace(true)
269 }
270
271 pub fn snapshot_without_applied_move_trace(&self) -> SolverTelemetry {
272 self.snapshot_with_applied_move_trace(false)
273 }
274
275 fn snapshot_with_applied_move_trace(
276 &self,
277 include_applied_move_trace: bool,
278 ) -> SolverTelemetry {
279 SolverTelemetry {
280 elapsed: self.elapsed(),
281 step_count: self.step_count,
282 moves_generated: self.moves_generated,
283 moves_evaluated: self.moves_evaluated,
284 moves_accepted: self.moves_accepted,
285 moves_applied: self.moves_applied,
286 moves_score_improving: self.moves_score_improving(),
287 moves_applied_improving: self.moves_applied_improving(),
288 moves_not_doable: self.moves_not_doable,
289 moves_acceptor_rejected: self.moves_acceptor_rejected,
290 moves_forager_ignored: self.moves_forager_ignored,
291 moves_hard_improving: self.moves_hard_improving,
292 moves_hard_neutral: self.moves_hard_neutral,
293 moves_hard_worse: self.moves_hard_worse,
294 conflict_repair_provider_generated: self.conflict_repair_provider_generated,
295 conflict_repair_duplicate_filtered: self.conflict_repair_duplicate_filtered,
296 conflict_repair_illegal_filtered: self.conflict_repair_illegal_filtered,
297 conflict_repair_not_doable_filtered: self.conflict_repair_not_doable_filtered,
298 conflict_repair_hard_improving: self.conflict_repair_hard_improving,
299 conflict_repair_exposed: self.conflict_repair_exposed,
300 score_calculations: self.score_calculations,
301 construction_slots_assigned: self.construction_slots_assigned,
302 construction_slots_kept: self.construction_slots_kept,
303 construction_slots_no_doable: self.construction_slots_no_doable,
304 scalar_assignment_required_remaining: self.scalar_assignment_required_remaining,
305 generation_time: self.generation_time,
306 evaluation_time: self.evaluation_time,
307 phase: None,
308 selector_telemetry: self.selector_stats.clone(),
309 move_telemetry: self.move_stats.values().cloned().collect(),
310 applied_move_trace: if include_applied_move_trace {
311 self.applied_move_trace.to_vec()
312 } else {
313 Vec::new()
314 },
315 candidate_trace: if include_applied_move_trace {
316 self.candidate_trace
317 .as_ref()
318 .map(CandidateTraceTelemetry::snapshot)
319 } else {
320 None
321 },
322 }
323 }
324
325 pub fn record_move_kind_generated(&mut self, move_label: &'static str) {
326 self.move_stats_entry(move_label).moves_generated += 1;
327 }
328
329 pub fn moves_score_improving(&self) -> u64 {
330 self.move_stats
331 .values()
332 .map(|entry| entry.moves_score_improving)
333 .sum()
334 }
335
336 pub fn moves_applied_improving(&self) -> u64 {
337 self.move_stats
338 .values()
339 .map(|entry| entry.moves_applied_improving)
340 .sum()
341 }
342
343 pub fn record_move_kind_evaluated(
344 &mut self,
345 move_label: &'static str,
346 score_ordering: Ordering,
347 ) {
348 let entry = self.move_stats_entry(move_label);
349 entry.moves_evaluated += 1;
350 match score_ordering {
351 Ordering::Greater => entry.moves_score_improving += 1,
352 Ordering::Equal => entry.moves_score_equal += 1,
353 Ordering::Less => entry.moves_score_worse += 1,
354 }
355 }
356
357 pub fn record_move_kind_evaluated_unscored(&mut self, move_label: &'static str) {
358 self.move_stats_entry(move_label).moves_evaluated += 1;
359 }
360
361 pub fn record_move_kind_accepted(&mut self, move_label: &'static str) {
362 self.move_stats_entry(move_label).moves_accepted += 1;
363 }
364
365 pub fn record_move_kind_applied(&mut self, move_label: &'static str, score_improvement: f64) {
366 let entry = self.move_stats_entry(move_label);
367 entry.moves_applied += 1;
368 if score_improvement > 0.0 {
369 entry.moves_applied_improving += 1;
370 entry.applied_score_improvement += score_improvement;
371 }
372 }
373
374 pub fn record_move_kind_not_doable(&mut self, move_label: &'static str) {
375 self.move_stats_entry(move_label).moves_not_doable += 1;
376 }
377
378 pub fn record_move_kind_acceptor_rejected(
379 &mut self,
380 move_label: &'static str,
381 score_ordering: Ordering,
382 ) {
383 let entry = self.move_stats_entry(move_label);
384 entry.moves_acceptor_rejected += 1;
385 if score_ordering == Ordering::Greater {
386 entry.moves_rejected_improving += 1;
387 }
388 }
389
390 pub fn record_move_kind_forager_ignored(&mut self, move_label: &'static str, count: u64) {
391 if count == 0 {
392 return;
393 }
394 self.move_stats_entry(move_label).moves_forager_ignored += count;
395 }
396
397 pub fn record_applied_move_trace(&mut self, applied_move: AppliedMoveTelemetry) {
398 if self.applied_move_trace.len() < APPLIED_MOVE_TRACE_LIMIT {
399 self.applied_move_trace.push(applied_move);
400 }
401 }
402
403 pub(crate) fn enable_candidate_trace(&mut self, candidate_trace: CandidateTraceTelemetry) {
404 assert!(
405 self.candidate_trace.is_none(),
406 "candidate trace may be enabled only once per solve"
407 );
408 self.candidate_trace = Some(candidate_trace);
409 }
410
411 pub(crate) fn prepare_candidate_trace_pull(&mut self) -> CandidateTraceRecordDecision {
412 match self.candidate_trace.as_mut() {
413 Some(trace) => trace.prepare_pull(),
414 None => CandidateTraceRecordDecision::Disabled,
415 }
416 }
417
418 pub(crate) fn begin_candidate_trace_phase(&mut self) -> Option<usize> {
419 self.candidate_trace
420 .as_mut()
421 .map(CandidateTraceTelemetry::begin_phase)
422 }
423
424 pub(crate) fn finalize_candidate_trace_resolved_phase_plan(
430 &mut self,
431 resolved_phase_plan: CandidateTracePhasePlan,
432 ) {
433 if let Some(trace) = self.candidate_trace.as_mut() {
434 trace.finalize_resolved_phase_plan(resolved_phase_plan);
435 }
436 }
437
438 pub(crate) fn record_prepared_candidate_trace_pull(
439 &mut self,
440 pull: CandidatePullTelemetry,
441 ) -> CandidateTracePullToken {
442 let trace = self
443 .candidate_trace
444 .as_mut()
445 .expect("a prepared candidate trace pull requires an enabled trace");
446 trace.push_prepared(pull)
447 }
448
449 pub(crate) fn record_candidate_trace_disposition(
450 &mut self,
451 token: CandidateTracePullToken,
452 disposition: CandidateTraceDisposition,
453 ) {
454 let trace = self
455 .candidate_trace
456 .as_mut()
457 .expect("a candidate trace disposition requires an enabled trace");
458 trace.record_disposition(token, disposition);
459 }
460
461 pub fn record_selector_generated_with_label(
462 &mut self,
463 selector_index: usize,
464 selector_label: impl Into<String>,
465 count: u64,
466 duration: Duration,
467 ) {
468 self.record_generated_batch(count, duration);
469 let selector = self.selector_stats_entry_with_label(selector_index, selector_label);
470 selector.moves_generated += count;
471 selector.generation_time += duration;
472 }
473
474 fn selector_stats_entry(&mut self, selector_index: usize) -> &mut SelectorTelemetry {
475 self.selector_stats_entry_with_label(selector_index, format!("selector-{selector_index}"))
476 }
477
478 fn selector_stats_entry_with_label(
479 &mut self,
480 selector_index: usize,
481 selector_label: impl Into<String>,
482 ) -> &mut SelectorTelemetry {
483 let selector_label = selector_label.into();
484 if let Some(position) = self
485 .selector_stats
486 .iter()
487 .position(|entry| entry.selector_index == selector_index)
488 {
489 if self.selector_stats[position]
490 .selector_label
491 .starts_with("selector-")
492 && !selector_label.starts_with("selector-")
493 {
494 self.selector_stats[position].selector_label = selector_label;
495 }
496 return &mut self.selector_stats[position];
497 }
498 self.selector_stats.push(SelectorTelemetry {
499 selector_index,
500 selector_label,
501 ..SelectorTelemetry::default()
502 });
503 self.selector_stats
504 .last_mut()
505 .expect("selector stats entry was just inserted")
506 }
507
508 fn move_stats_entry(&mut self, move_label: &'static str) -> &mut MoveTelemetry {
509 self.move_stats
510 .entry(move_label)
511 .or_insert_with(|| MoveTelemetry {
512 move_label: move_label.to_string(),
513 ..MoveTelemetry::default()
514 })
515 }
516}
517
518