1use std::cmp::Ordering;
2use std::collections::BTreeMap;
3use std::time::{Duration, Instant};
4
5use smallvec::SmallVec;
6
7use super::{
8 AppliedMoveTelemetry, CandidatePullTelemetry, CandidateTraceDisposition,
9 CandidateTracePhasePlan, CandidateTracePullToken, CandidateTraceRecordDecision,
10 CandidateTraceTelemetry, PhaseStats, PhaseTelemetry, SolverTelemetry, Throughput,
11};
12
13#[derive(Debug)]
24pub struct SolverStats {
25 start_time: Option<Instant>,
26 pause_started_at: Option<Instant>,
27 folded: PhaseStats,
29 finished_phases: SmallVec<[FinishedPhase; INLINE_PHASES]>,
30 current_phase: Option<RunningPhase>,
31 scalar_assignment_required_remaining_by_group: BTreeMap<&'static str, u64>,
32 candidate_trace: Option<CandidateTraceTelemetry>,
33}
34
35const INLINE_PHASES: usize = 4;
38
39#[derive(Debug)]
40struct FinishedPhase {
41 stats: PhaseStats,
42 elapsed: Duration,
43}
44
45#[derive(Debug)]
46struct RunningPhase {
47 stats: PhaseStats,
48 started_at: Option<Duration>,
50}
51
52impl Default for SolverStats {
53 fn default() -> Self {
54 Self {
55 start_time: None,
56 pause_started_at: None,
57 folded: PhaseStats::new(0, "Solve"),
58 finished_phases: SmallVec::new(),
59 current_phase: None,
60 scalar_assignment_required_remaining_by_group: BTreeMap::new(),
61 candidate_trace: None,
62 }
63 }
64}
65
66macro_rules! run_totals {
67 ($($(#[$doc:meta])* $name:ident),* $(,)?) => {
68 $(
69 $(#[$doc])*
70 pub fn $name(&self) -> u64 {
71 self.folded.$name
72 + self
73 .current_phase
74 .as_ref()
75 .map_or(0, |phase| phase.stats.$name)
76 }
77 )*
78 };
79}
80
81macro_rules! record_into_active {
82 ($($(#[$doc:meta])* $name:ident($($arg:ident: $ty:ty),*)),* $(,)?) => {
83 $(
84 $(#[$doc])*
85 pub fn $name(&mut self, $($arg: $ty),*) {
86 self.active().$name($($arg),*);
87 }
88 )*
89 };
90}
91
92impl SolverStats {
93 pub fn start(&mut self) {
95 self.start_time = Some(Instant::now());
96 self.pause_started_at = None;
97 }
98
99 pub fn elapsed(&self) -> Duration {
101 self.active_elapsed().unwrap_or_default()
102 }
103
104 pub fn active_elapsed(&self) -> Option<Duration> {
106 match (self.start_time, self.pause_started_at) {
107 (Some(start), Some(paused_at)) => Some(paused_at.duration_since(start)),
108 (Some(start), None) => Some(start.elapsed()),
109 _ => None,
110 }
111 }
112
113 pub fn started_at(&self) -> Option<Instant> {
115 self.start_time
116 }
117
118 pub fn is_paused(&self) -> bool {
119 self.pause_started_at.is_some()
120 }
121
122 pub fn pause(&mut self) {
123 if self.start_time.is_some() && self.pause_started_at.is_none() {
124 self.pause_started_at = Some(Instant::now());
125 }
126 }
127
128 pub fn resume(&mut self) {
129 if let (Some(start), Some(paused_at)) = (self.start_time, self.pause_started_at.take()) {
130 self.start_time = Some(start + paused_at.elapsed());
131 }
132 }
133
134 pub fn begin_phase(&mut self, phase_type: &'static str) {
137 self.end_phase();
138 let phase_index = self.finished_phases.len();
139 self.current_phase = Some(RunningPhase {
140 stats: PhaseStats::new(phase_index, phase_type),
141 started_at: self.active_elapsed(),
142 });
143 }
144
145 pub fn end_phase(&mut self) {
147 if let Some(phase) = self.current_phase.take() {
148 let elapsed = self.running_elapsed(&phase);
149 self.folded.absorb(&phase.stats);
150 self.finished_phases.push(FinishedPhase {
151 stats: phase.stats,
152 elapsed,
153 });
154 }
155 }
156
157 pub fn phase_elapsed(&self) -> Option<Duration> {
159 self.current_phase
160 .as_ref()
161 .map(|phase| self.running_elapsed(phase))
162 }
163
164 pub fn phase(&self) -> Option<&PhaseStats> {
166 self.current_phase.as_ref().map(|phase| &phase.stats)
167 }
168
169 pub fn phase_mut(&mut self) -> Option<&mut PhaseStats> {
170 self.current_phase.as_mut().map(|phase| &mut phase.stats)
171 }
172
173 fn active(&mut self) -> &mut PhaseStats {
174 match self.current_phase.as_mut() {
175 Some(phase) => &mut phase.stats,
176 None => &mut self.folded,
177 }
178 }
179
180 fn running_elapsed(&self, phase: &RunningPhase) -> Duration {
181 match (phase.started_at, self.active_elapsed()) {
182 (Some(started_at), Some(now)) => now.saturating_sub(started_at),
183 _ => phase.stats.elapsed(),
184 }
185 }
186
187 run_totals!(
188 step_count,
190 moves_generated,
193 moves_evaluated,
194 moves_speculative,
197 moves_accepted,
198 moves_applied,
199 moves_not_doable,
200 moves_acceptor_rejected,
201 moves_forager_ignored,
202 moves_hard_improving,
203 moves_hard_neutral,
204 moves_hard_worse,
205 conflict_repair_provider_generated,
206 conflict_repair_duplicate_filtered,
207 conflict_repair_illegal_filtered,
208 conflict_repair_not_doable_filtered,
209 conflict_repair_hard_improving,
210 conflict_repair_exposed,
211 score_calculations,
212 construction_slots_assigned,
213 construction_slots_kept,
214 construction_slots_no_doable,
215 );
216
217 pub fn scalar_assignment_required_remaining(&self) -> u64 {
219 self.scalar_assignment_required_remaining_by_group
220 .values()
221 .copied()
222 .sum()
223 }
224
225 record_into_active!(
226 record_generated_batch(count: u64, duration: Duration),
228 record_selector_generated(selector_index: usize, count: u64, duration: Duration),
229 record_generation_time(duration: Duration),
231 record_generated_move(duration: Duration),
233 record_evaluated_move(duration: Duration),
235 record_selector_evaluated(selector_index: usize, duration: Duration),
236 record_move_accepted(),
237 record_selector_accepted(selector_index: usize),
238 record_move_applied(),
239 record_selector_applied(selector_index: usize),
240 record_move_not_doable(),
241 record_selector_not_doable(selector_index: usize),
242 record_move_acceptor_rejected(),
243 record_selector_acceptor_rejected(selector_index: usize),
244 record_moves_forager_ignored(count: u64),
245 record_move_hard_improving(),
246 record_move_hard_neutral(),
247 record_move_hard_worse(),
248 record_conflict_repair_provider_generated(count: u64),
249 record_conflict_repair_duplicate_filtered(),
250 record_conflict_repair_illegal_filtered(),
251 record_conflict_repair_not_doable_filtered(),
252 record_conflict_repair_hard_improving(),
253 record_conflict_repair_exposed(),
254 record_step(),
256 record_score_calculation(),
258 record_move_speculative(),
261 record_construction_slot_assigned(),
262 record_construction_slot_kept(),
263 record_construction_slot_no_doable(),
264 record_move_kind_generated(move_label: &'static str),
265 record_move_kind_evaluated(move_label: &'static str, score_ordering: Ordering),
266 record_move_kind_evaluated_unscored(move_label: &'static str),
267 record_move_kind_accepted(move_label: &'static str),
268 record_move_kind_applied(move_label: &'static str, score_improvement: f64),
269 record_move_kind_not_doable(move_label: &'static str),
270 record_move_kind_acceptor_rejected(move_label: &'static str, score_ordering: Ordering),
271 record_move_kind_forager_ignored(move_label: &'static str, count: u64),
272 record_applied_move_trace(applied_move: AppliedMoveTelemetry),
273 );
274
275 pub fn record_selector_generated_with_label(
276 &mut self,
277 selector_index: usize,
278 selector_label: impl Into<String>,
279 count: u64,
280 duration: Duration,
281 ) {
282 self.active().record_selector_generated_with_label(
283 selector_index,
284 selector_label,
285 count,
286 duration,
287 );
288 }
289
290 pub fn record_scalar_assignment_required_remaining(
294 &mut self,
295 group_name: &'static str,
296 count: u64,
297 ) {
298 self.scalar_assignment_required_remaining_by_group
299 .insert(group_name, count);
300 self.active()
301 .record_scalar_assignment_required_remaining(count);
302 }
303
304 pub fn generation_time(&self) -> Duration {
305 self.folded.generation_time()
306 + self
307 .phase()
308 .map_or(Duration::ZERO, PhaseStats::generation_time)
309 }
310
311 pub fn evaluation_time(&self) -> Duration {
312 self.folded.evaluation_time()
313 + self
314 .phase()
315 .map_or(Duration::ZERO, PhaseStats::evaluation_time)
316 }
317
318 pub fn generated_throughput(&self) -> Throughput {
319 Throughput {
320 count: self.moves_generated(),
321 elapsed: self.generation_time(),
322 }
323 }
324
325 pub fn evaluated_throughput(&self) -> Throughput {
326 Throughput {
327 count: self.moves_evaluated(),
328 elapsed: self.evaluation_time(),
329 }
330 }
331
332 pub fn acceptance_rate(&self) -> f64 {
333 let evaluated = self.moves_evaluated();
334 if evaluated == 0 {
335 0.0
336 } else {
337 self.moves_accepted() as f64 / evaluated as f64
338 }
339 }
340
341 pub fn snapshot(&self) -> SolverTelemetry {
342 self.snapshot_with_applied_move_trace(true)
343 }
344
345 pub fn snapshot_without_applied_move_trace(&self) -> SolverTelemetry {
346 self.snapshot_with_applied_move_trace(false)
347 }
348
349 fn snapshot_with_applied_move_trace(
350 &self,
351 include_applied_move_trace: bool,
352 ) -> SolverTelemetry {
353 let mut totals = self.folded.clone();
354 let mut phases: Vec<PhaseTelemetry> = self
355 .finished_phases
356 .iter()
357 .map(|phase| phase.stats.snapshot_with_elapsed(phase.elapsed))
358 .collect();
359 if let Some(phase) = &self.current_phase {
360 totals.absorb(&phase.stats);
361 phases.push(
362 phase
363 .stats
364 .snapshot_with_elapsed(self.running_elapsed(phase)),
365 );
366 }
367 SolverTelemetry {
368 elapsed: self.elapsed(),
369 step_count: totals.step_count,
370 moves_generated: totals.moves_generated,
371 moves_evaluated: totals.moves_evaluated,
372 moves_speculative: totals.moves_speculative,
373 moves_accepted: totals.moves_accepted,
374 moves_applied: totals.moves_applied,
375 moves_score_improving: totals.moves_score_improving(),
376 moves_applied_improving: totals.moves_applied_improving(),
377 moves_not_doable: totals.moves_not_doable,
378 moves_acceptor_rejected: totals.moves_acceptor_rejected,
379 moves_forager_ignored: totals.moves_forager_ignored,
380 moves_hard_improving: totals.moves_hard_improving,
381 moves_hard_neutral: totals.moves_hard_neutral,
382 moves_hard_worse: totals.moves_hard_worse,
383 conflict_repair_provider_generated: totals.conflict_repair_provider_generated,
384 conflict_repair_duplicate_filtered: totals.conflict_repair_duplicate_filtered,
385 conflict_repair_illegal_filtered: totals.conflict_repair_illegal_filtered,
386 conflict_repair_not_doable_filtered: totals.conflict_repair_not_doable_filtered,
387 conflict_repair_hard_improving: totals.conflict_repair_hard_improving,
388 conflict_repair_exposed: totals.conflict_repair_exposed,
389 score_calculations: totals.score_calculations,
390 construction_slots_assigned: totals.construction_slots_assigned,
391 construction_slots_kept: totals.construction_slots_kept,
392 construction_slots_no_doable: totals.construction_slots_no_doable,
393 scalar_assignment_required_remaining: self.scalar_assignment_required_remaining(),
394 generation_time: totals.generation_time(),
395 evaluation_time: totals.evaluation_time(),
396 phases,
397 selector_telemetry: totals.selector_telemetry().to_vec(),
398 move_telemetry: totals.move_telemetry(),
399 applied_move_trace: if include_applied_move_trace {
400 totals.applied_move_trace().to_vec()
401 } else {
402 Vec::new()
403 },
404 candidate_trace: if include_applied_move_trace {
405 self.candidate_trace
406 .as_ref()
407 .map(CandidateTraceTelemetry::snapshot)
408 } else {
409 None
410 },
411 }
412 }
413
414 pub fn moves_score_improving(&self) -> u64 {
415 self.folded.moves_score_improving()
416 + self.phase().map_or(0, PhaseStats::moves_score_improving)
417 }
418
419 pub fn moves_applied_improving(&self) -> u64 {
420 self.folded.moves_applied_improving()
421 + self.phase().map_or(0, PhaseStats::moves_applied_improving)
422 }
423
424 pub(crate) fn enable_candidate_trace(&mut self, candidate_trace: CandidateTraceTelemetry) {
425 assert!(
426 self.candidate_trace.is_none(),
427 "candidate trace may be enabled only once per solve"
428 );
429 self.candidate_trace = Some(candidate_trace);
430 }
431
432 pub(crate) fn prepare_candidate_trace_pull(&mut self) -> CandidateTraceRecordDecision {
433 match self.candidate_trace.as_mut() {
434 Some(trace) => trace.prepare_pull(),
435 None => CandidateTraceRecordDecision::Disabled,
436 }
437 }
438
439 pub(crate) fn begin_candidate_trace_phase(&mut self) -> Option<usize> {
440 self.candidate_trace
441 .as_mut()
442 .map(CandidateTraceTelemetry::begin_phase)
443 }
444
445 pub(crate) fn finalize_candidate_trace_resolved_phase_plan(
451 &mut self,
452 resolved_phase_plan: CandidateTracePhasePlan,
453 ) {
454 if let Some(trace) = self.candidate_trace.as_mut() {
455 trace.finalize_resolved_phase_plan(resolved_phase_plan);
456 }
457 }
458
459 pub(crate) fn record_prepared_candidate_trace_pull(
460 &mut self,
461 pull: CandidatePullTelemetry,
462 ) -> CandidateTracePullToken {
463 let trace = self
464 .candidate_trace
465 .as_mut()
466 .expect("a prepared candidate trace pull requires an enabled trace");
467 trace.push_prepared(pull)
468 }
469
470 pub(crate) fn record_candidate_trace_disposition(
471 &mut self,
472 token: CandidateTracePullToken,
473 disposition: CandidateTraceDisposition,
474 ) {
475 let trace = self
476 .candidate_trace
477 .as_mut()
478 .expect("a candidate trace disposition requires an enabled trace");
479 trace.record_disposition(token, disposition);
480 }
481}
482
483