solverforge_solver/stats/
solver.rs1use std::cmp::Ordering;
2use std::collections::BTreeMap;
3use std::time::{Duration, Instant};
4
5use super::{AppliedMoveTelemetry, MoveTelemetry, SelectorTelemetry, SolverTelemetry, Throughput};
6
7const APPLIED_MOVE_TRACE_LIMIT: usize = 8;
8
9#[derive(Debug, Default)]
10pub struct SolverStats {
11 start_time: Option<Instant>,
12 pause_started_at: Option<Instant>,
13 pub step_count: u64,
15 pub moves_generated: u64,
17 pub moves_evaluated: u64,
19 pub moves_accepted: u64,
21 pub moves_applied: u64,
23 pub moves_not_doable: u64,
24 pub moves_acceptor_rejected: u64,
25 pub moves_forager_ignored: u64,
26 pub moves_hard_improving: u64,
27 pub moves_hard_neutral: u64,
28 pub moves_hard_worse: u64,
29 pub conflict_repair_provider_generated: u64,
30 pub conflict_repair_duplicate_filtered: u64,
31 pub conflict_repair_illegal_filtered: u64,
32 pub conflict_repair_not_doable_filtered: u64,
33 pub conflict_repair_hard_improving: u64,
34 pub conflict_repair_exposed: u64,
35 pub score_calculations: u64,
37 pub construction_slots_assigned: u64,
38 pub construction_slots_kept: u64,
39 pub construction_slots_no_doable: u64,
40 pub scalar_assignment_required_remaining: u64,
41 scalar_assignment_required_remaining_by_group: BTreeMap<&'static str, u64>,
42 generation_time: Duration,
43 evaluation_time: Duration,
44 selector_stats: Vec<SelectorTelemetry>,
45 move_stats: BTreeMap<&'static str, MoveTelemetry>,
46 applied_move_trace: Vec<AppliedMoveTelemetry>,
47}
48
49impl SolverStats {
50 pub fn start(&mut self) {
52 self.start_time = Some(Instant::now());
53 self.pause_started_at = None;
54 }
55
56 pub fn elapsed(&self) -> Duration {
57 match (self.start_time, self.pause_started_at) {
58 (Some(start), Some(paused_at)) => paused_at.duration_since(start),
59 (Some(start), None) => start.elapsed(),
60 _ => Duration::default(),
61 }
62 }
63
64 pub fn pause(&mut self) {
65 if self.start_time.is_some() && self.pause_started_at.is_none() {
66 self.pause_started_at = Some(Instant::now());
67 }
68 }
69
70 pub fn resume(&mut self) {
71 if let (Some(start), Some(paused_at)) = (self.start_time, self.pause_started_at.take()) {
72 self.start_time = Some(start + paused_at.elapsed());
73 }
74 }
75
76 pub fn record_generated_batch(&mut self, count: u64, duration: Duration) {
78 self.moves_generated += count;
79 self.generation_time += duration;
80 }
81
82 pub fn record_selector_generated(
83 &mut self,
84 selector_index: usize,
85 count: u64,
86 duration: Duration,
87 ) {
88 self.record_generated_batch(count, duration);
89 let selector = self.selector_stats_entry(selector_index);
90 selector.moves_generated += count;
91 selector.generation_time += duration;
92 }
93
94 pub fn record_generation_time(&mut self, duration: Duration) {
96 self.generation_time += duration;
97 }
98
99 pub fn record_generated_move(&mut self, duration: Duration) {
101 self.record_generated_batch(1, duration);
102 }
103
104 pub fn record_evaluated_move(&mut self, duration: Duration) {
106 self.moves_evaluated += 1;
107 self.evaluation_time += duration;
108 }
109
110 pub fn record_selector_evaluated(&mut self, selector_index: usize, duration: Duration) {
111 self.record_evaluated_move(duration);
112 let selector = self.selector_stats_entry(selector_index);
113 selector.moves_evaluated += 1;
114 selector.evaluation_time += duration;
115 }
116
117 pub fn record_move_accepted(&mut self) {
119 self.moves_accepted += 1;
120 }
121
122 pub fn record_selector_accepted(&mut self, selector_index: usize) {
123 self.record_move_accepted();
124 self.selector_stats_entry(selector_index).moves_accepted += 1;
125 }
126
127 pub fn record_move_applied(&mut self) {
128 self.moves_applied += 1;
129 }
130
131 pub fn record_selector_applied(&mut self, selector_index: usize) {
132 self.record_move_applied();
133 self.selector_stats_entry(selector_index).moves_applied += 1;
134 }
135
136 pub fn record_move_not_doable(&mut self) {
137 self.moves_not_doable += 1;
138 }
139
140 pub fn record_selector_not_doable(&mut self, selector_index: usize) {
141 self.record_move_not_doable();
142 self.selector_stats_entry(selector_index).moves_not_doable += 1;
143 }
144
145 pub fn record_move_acceptor_rejected(&mut self) {
146 self.moves_acceptor_rejected += 1;
147 }
148
149 pub fn record_selector_acceptor_rejected(&mut self, selector_index: usize) {
150 self.record_move_acceptor_rejected();
151 self.selector_stats_entry(selector_index)
152 .moves_acceptor_rejected += 1;
153 }
154
155 pub fn record_moves_forager_ignored(&mut self, count: u64) {
156 self.moves_forager_ignored += count;
157 }
158
159 pub fn record_move_hard_improving(&mut self) {
160 self.moves_hard_improving += 1;
161 }
162
163 pub fn record_move_hard_neutral(&mut self) {
164 self.moves_hard_neutral += 1;
165 }
166
167 pub fn record_move_hard_worse(&mut self) {
168 self.moves_hard_worse += 1;
169 }
170
171 pub fn record_conflict_repair_provider_generated(&mut self, count: u64) {
172 self.conflict_repair_provider_generated += count;
173 }
174
175 pub fn record_conflict_repair_duplicate_filtered(&mut self) {
176 self.conflict_repair_duplicate_filtered += 1;
177 }
178
179 pub fn record_conflict_repair_illegal_filtered(&mut self) {
180 self.conflict_repair_illegal_filtered += 1;
181 }
182
183 pub fn record_conflict_repair_not_doable_filtered(&mut self) {
184 self.conflict_repair_not_doable_filtered += 1;
185 }
186
187 pub fn record_conflict_repair_hard_improving(&mut self) {
188 self.conflict_repair_hard_improving += 1;
189 }
190
191 pub fn record_conflict_repair_exposed(&mut self) {
192 self.conflict_repair_exposed += 1;
193 }
194
195 pub fn record_step(&mut self) {
197 self.step_count += 1;
198 }
199
200 pub fn record_score_calculation(&mut self) {
202 self.score_calculations += 1;
203 }
204
205 pub fn record_construction_slot_assigned(&mut self) {
206 self.construction_slots_assigned += 1;
207 }
208
209 pub fn record_construction_slot_kept(&mut self) {
210 self.construction_slots_kept += 1;
211 }
212
213 pub fn record_construction_slot_no_doable(&mut self) {
214 self.construction_slots_no_doable += 1;
215 }
216
217 pub fn record_scalar_assignment_required_remaining(
218 &mut self,
219 group_name: &'static str,
220 count: u64,
221 ) {
222 self.scalar_assignment_required_remaining_by_group
223 .insert(group_name, count);
224 self.scalar_assignment_required_remaining = self
225 .scalar_assignment_required_remaining_by_group
226 .values()
227 .copied()
228 .sum();
229 }
230
231 pub fn generated_throughput(&self) -> Throughput {
232 Throughput {
233 count: self.moves_generated,
234 elapsed: self.generation_time,
235 }
236 }
237
238 pub fn evaluated_throughput(&self) -> Throughput {
239 Throughput {
240 count: self.moves_evaluated,
241 elapsed: self.evaluation_time,
242 }
243 }
244
245 pub fn acceptance_rate(&self) -> f64 {
246 if self.moves_evaluated == 0 {
247 0.0
248 } else {
249 self.moves_accepted as f64 / self.moves_evaluated as f64
250 }
251 }
252
253 pub fn generation_time(&self) -> Duration {
254 self.generation_time
255 }
256
257 pub fn evaluation_time(&self) -> Duration {
258 self.evaluation_time
259 }
260
261 pub fn snapshot(&self) -> SolverTelemetry {
262 self.snapshot_with_applied_move_trace(true)
263 }
264
265 pub fn snapshot_without_applied_move_trace(&self) -> SolverTelemetry {
266 self.snapshot_with_applied_move_trace(false)
267 }
268
269 fn snapshot_with_applied_move_trace(
270 &self,
271 include_applied_move_trace: bool,
272 ) -> SolverTelemetry {
273 SolverTelemetry {
274 elapsed: self.elapsed(),
275 step_count: self.step_count,
276 moves_generated: self.moves_generated,
277 moves_evaluated: self.moves_evaluated,
278 moves_accepted: self.moves_accepted,
279 moves_applied: self.moves_applied,
280 moves_not_doable: self.moves_not_doable,
281 moves_acceptor_rejected: self.moves_acceptor_rejected,
282 moves_forager_ignored: self.moves_forager_ignored,
283 moves_hard_improving: self.moves_hard_improving,
284 moves_hard_neutral: self.moves_hard_neutral,
285 moves_hard_worse: self.moves_hard_worse,
286 conflict_repair_provider_generated: self.conflict_repair_provider_generated,
287 conflict_repair_duplicate_filtered: self.conflict_repair_duplicate_filtered,
288 conflict_repair_illegal_filtered: self.conflict_repair_illegal_filtered,
289 conflict_repair_not_doable_filtered: self.conflict_repair_not_doable_filtered,
290 conflict_repair_hard_improving: self.conflict_repair_hard_improving,
291 conflict_repair_exposed: self.conflict_repair_exposed,
292 score_calculations: self.score_calculations,
293 construction_slots_assigned: self.construction_slots_assigned,
294 construction_slots_kept: self.construction_slots_kept,
295 construction_slots_no_doable: self.construction_slots_no_doable,
296 scalar_assignment_required_remaining: self.scalar_assignment_required_remaining,
297 generation_time: self.generation_time,
298 evaluation_time: self.evaluation_time,
299 selector_telemetry: self.selector_stats.clone(),
300 move_telemetry: self.move_stats.values().cloned().collect(),
301 applied_move_trace: if include_applied_move_trace {
302 self.applied_move_trace.to_vec()
303 } else {
304 Vec::new()
305 },
306 }
307 }
308
309 pub fn record_move_kind_generated(&mut self, move_label: &'static str) {
310 self.move_stats_entry(move_label).moves_generated += 1;
311 }
312
313 pub fn record_move_kind_evaluated(
314 &mut self,
315 move_label: &'static str,
316 score_ordering: Ordering,
317 ) {
318 let entry = self.move_stats_entry(move_label);
319 entry.moves_evaluated += 1;
320 match score_ordering {
321 Ordering::Greater => entry.moves_score_improving += 1,
322 Ordering::Equal => entry.moves_score_equal += 1,
323 Ordering::Less => entry.moves_score_worse += 1,
324 }
325 }
326
327 pub fn record_move_kind_evaluated_unscored(&mut self, move_label: &'static str) {
328 self.move_stats_entry(move_label).moves_evaluated += 1;
329 }
330
331 pub fn record_move_kind_accepted(&mut self, move_label: &'static str) {
332 self.move_stats_entry(move_label).moves_accepted += 1;
333 }
334
335 pub fn record_move_kind_applied(&mut self, move_label: &'static str, score_improvement: f64) {
336 let entry = self.move_stats_entry(move_label);
337 entry.moves_applied += 1;
338 if score_improvement > 0.0 {
339 entry.applied_score_improvement += score_improvement;
340 }
341 }
342
343 pub fn record_move_kind_not_doable(&mut self, move_label: &'static str) {
344 self.move_stats_entry(move_label).moves_not_doable += 1;
345 }
346
347 pub fn record_move_kind_acceptor_rejected(
348 &mut self,
349 move_label: &'static str,
350 score_ordering: Ordering,
351 ) {
352 let entry = self.move_stats_entry(move_label);
353 entry.moves_acceptor_rejected += 1;
354 if score_ordering == Ordering::Greater {
355 entry.moves_rejected_improving += 1;
356 }
357 }
358
359 pub fn record_move_kind_forager_ignored(&mut self, move_label: &'static str, count: u64) {
360 if count == 0 {
361 return;
362 }
363 self.move_stats_entry(move_label).moves_forager_ignored += count;
364 }
365
366 pub fn record_applied_move_trace(&mut self, applied_move: AppliedMoveTelemetry) {
367 if self.applied_move_trace.len() < APPLIED_MOVE_TRACE_LIMIT {
368 self.applied_move_trace.push(applied_move);
369 }
370 }
371
372 pub fn record_selector_generated_with_label(
373 &mut self,
374 selector_index: usize,
375 selector_label: impl Into<String>,
376 count: u64,
377 duration: Duration,
378 ) {
379 self.record_generated_batch(count, duration);
380 let selector = self.selector_stats_entry_with_label(selector_index, selector_label);
381 selector.moves_generated += count;
382 selector.generation_time += duration;
383 }
384
385 fn selector_stats_entry(&mut self, selector_index: usize) -> &mut SelectorTelemetry {
386 self.selector_stats_entry_with_label(selector_index, format!("selector-{selector_index}"))
387 }
388
389 fn selector_stats_entry_with_label(
390 &mut self,
391 selector_index: usize,
392 selector_label: impl Into<String>,
393 ) -> &mut SelectorTelemetry {
394 let selector_label = selector_label.into();
395 if let Some(position) = self
396 .selector_stats
397 .iter()
398 .position(|entry| entry.selector_index == selector_index)
399 {
400 if self.selector_stats[position]
401 .selector_label
402 .starts_with("selector-")
403 && !selector_label.starts_with("selector-")
404 {
405 self.selector_stats[position].selector_label = selector_label;
406 }
407 return &mut self.selector_stats[position];
408 }
409 self.selector_stats.push(SelectorTelemetry {
410 selector_index,
411 selector_label,
412 ..SelectorTelemetry::default()
413 });
414 self.selector_stats
415 .last_mut()
416 .expect("selector stats entry was just inserted")
417 }
418
419 fn move_stats_entry(&mut self, move_label: &'static str) -> &mut MoveTelemetry {
420 self.move_stats
421 .entry(move_label)
422 .or_insert_with(|| MoveTelemetry {
423 move_label: move_label.to_string(),
424 ..MoveTelemetry::default()
425 })
426 }
427}
428
429