Skip to main content

solverforge_solver/stats/
telemetry.rs

1/* Solver statistics (zero-erasure).
2
3Stack-allocated statistics for solver and phase performance tracking.
4*/
5
6use std::time::Duration;
7
8/* Solver-level statistics.
9
10Tracks aggregate metrics across all phases of a solve run.
11
12# Example
13
14```
15use solverforge_solver::stats::SolverStats;
16use std::time::Duration;
17
18let mut stats = SolverStats::default();
19stats.start();
20stats.record_step();
21stats.record_generated_move(Duration::from_millis(1));
22stats.record_evaluated_move(Duration::from_millis(2));
23stats.record_move_accepted();
24stats.record_generated_move(Duration::from_millis(1));
25stats.record_evaluated_move(Duration::from_millis(2));
26
27assert_eq!(stats.step_count, 1);
28assert_eq!(stats.moves_evaluated, 2);
29assert_eq!(stats.moves_accepted, 1);
30```
31*/
32#[derive(Debug, Clone, Default, PartialEq)]
33pub struct SelectorTelemetry {
34    pub selector_index: usize,
35    pub selector_label: String,
36    pub moves_generated: u64,
37    pub moves_evaluated: u64,
38    pub moves_accepted: u64,
39    pub moves_applied: u64,
40    pub moves_not_doable: u64,
41    pub moves_acceptor_rejected: u64,
42    pub moves_forager_ignored: u64,
43    pub moves_hard_improving: u64,
44    pub moves_hard_neutral: u64,
45    pub moves_hard_worse: u64,
46    pub conflict_repair_provider_generated: u64,
47    pub conflict_repair_duplicate_filtered: u64,
48    pub conflict_repair_illegal_filtered: u64,
49    pub conflict_repair_not_doable_filtered: u64,
50    pub conflict_repair_hard_improving: u64,
51    pub conflict_repair_exposed: u64,
52    pub generation_time: Duration,
53    pub evaluation_time: Duration,
54}
55
56#[derive(Debug, Clone, Default, PartialEq)]
57pub struct MoveTelemetry {
58    pub move_label: String,
59    pub moves_generated: u64,
60    pub moves_evaluated: u64,
61    pub moves_accepted: u64,
62    pub moves_applied: u64,
63    pub moves_not_doable: u64,
64    pub moves_acceptor_rejected: u64,
65    pub moves_forager_ignored: u64,
66    pub moves_score_improving: u64,
67    pub moves_score_equal: u64,
68    pub moves_score_worse: u64,
69    pub moves_rejected_improving: u64,
70    pub applied_score_improvement: f64,
71}
72
73#[derive(Debug, Clone, Copy, Default, PartialEq)]
74pub struct AppliedMoveTelemetry {
75    pub step_index: u64,
76    pub move_label: &'static str,
77    pub selected_candidate_index: usize,
78    pub moves_generated_this_step: u64,
79    pub moves_evaluated_this_step: u64,
80    pub moves_accepted_this_step: u64,
81    pub moves_forager_ignored_this_step: u64,
82    pub score_before: f64,
83    pub score_after: f64,
84    pub score_delta: f64,
85    pub hard_feasible_before: bool,
86    pub hard_feasible_after: bool,
87}
88
89#[derive(Debug, Clone, Default, PartialEq)]
90pub struct SolverTelemetry {
91    pub elapsed: Duration,
92    pub step_count: u64,
93    pub moves_generated: u64,
94    pub moves_evaluated: u64,
95    pub moves_accepted: u64,
96    pub moves_applied: u64,
97    pub moves_not_doable: u64,
98    pub moves_acceptor_rejected: u64,
99    pub moves_forager_ignored: u64,
100    pub moves_hard_improving: u64,
101    pub moves_hard_neutral: u64,
102    pub moves_hard_worse: u64,
103    pub conflict_repair_provider_generated: u64,
104    pub conflict_repair_duplicate_filtered: u64,
105    pub conflict_repair_illegal_filtered: u64,
106    pub conflict_repair_not_doable_filtered: u64,
107    pub conflict_repair_hard_improving: u64,
108    pub conflict_repair_exposed: u64,
109    pub score_calculations: u64,
110    pub construction_slots_assigned: u64,
111    pub construction_slots_kept: u64,
112    pub construction_slots_no_doable: u64,
113    pub scalar_assignment_required_remaining: u64,
114    pub generation_time: Duration,
115    pub evaluation_time: Duration,
116    pub selector_telemetry: Vec<SelectorTelemetry>,
117    pub move_telemetry: Vec<MoveTelemetry>,
118    pub applied_move_trace: Vec<AppliedMoveTelemetry>,
119}
120
121impl SolverTelemetry {
122    pub const fn new_const() -> Self {
123        Self {
124            elapsed: Duration::ZERO,
125            step_count: 0,
126            moves_generated: 0,
127            moves_evaluated: 0,
128            moves_accepted: 0,
129            moves_applied: 0,
130            moves_not_doable: 0,
131            moves_acceptor_rejected: 0,
132            moves_forager_ignored: 0,
133            moves_hard_improving: 0,
134            moves_hard_neutral: 0,
135            moves_hard_worse: 0,
136            conflict_repair_provider_generated: 0,
137            conflict_repair_duplicate_filtered: 0,
138            conflict_repair_illegal_filtered: 0,
139            conflict_repair_not_doable_filtered: 0,
140            conflict_repair_hard_improving: 0,
141            conflict_repair_exposed: 0,
142            score_calculations: 0,
143            construction_slots_assigned: 0,
144            construction_slots_kept: 0,
145            construction_slots_no_doable: 0,
146            scalar_assignment_required_remaining: 0,
147            generation_time: Duration::ZERO,
148            evaluation_time: Duration::ZERO,
149            selector_telemetry: Vec::new(),
150            move_telemetry: Vec::new(),
151            applied_move_trace: Vec::new(),
152        }
153    }
154}
155
156#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
157pub struct Throughput {
158    pub count: u64,
159    pub elapsed: Duration,
160}
161
162pub(crate) fn whole_units_per_second(count: u64, elapsed: Duration) -> u128 {
163    let nanos = elapsed.as_nanos();
164    if nanos == 0 {
165        0
166    } else {
167        u128::from(count)
168            .saturating_mul(1_000_000_000)
169            .checked_div(nanos)
170            .unwrap_or(0)
171    }
172}
173
174pub(crate) fn format_duration(duration: Duration) -> String {
175    let secs = duration.as_secs();
176    let nanos = duration.subsec_nanos();
177
178    if secs >= 60 {
179        let mins = secs / 60;
180        let rem_secs = secs % 60;
181        return format!("{mins}m {rem_secs}s");
182    }
183
184    if secs > 0 {
185        let millis = nanos / 1_000_000;
186        if millis == 0 {
187            return format!("{secs}s");
188        }
189        return format!("{secs}s {millis}ms");
190    }
191
192    let millis = nanos / 1_000_000;
193    if millis > 0 {
194        return format!("{millis}ms");
195    }
196
197    let micros = nanos / 1_000;
198    if micros > 0 {
199        return format!("{micros}us");
200    }
201
202    format!("{nanos}ns")
203}