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