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 SolverTelemetry {
58    pub elapsed: Duration,
59    pub step_count: u64,
60    pub moves_generated: u64,
61    pub moves_evaluated: u64,
62    pub moves_accepted: u64,
63    pub moves_applied: u64,
64    pub moves_not_doable: u64,
65    pub moves_acceptor_rejected: u64,
66    pub moves_forager_ignored: u64,
67    pub moves_hard_improving: u64,
68    pub moves_hard_neutral: u64,
69    pub moves_hard_worse: u64,
70    pub conflict_repair_provider_generated: u64,
71    pub conflict_repair_duplicate_filtered: u64,
72    pub conflict_repair_illegal_filtered: u64,
73    pub conflict_repair_not_doable_filtered: u64,
74    pub conflict_repair_hard_improving: u64,
75    pub conflict_repair_exposed: u64,
76    pub score_calculations: u64,
77    pub construction_slots_assigned: u64,
78    pub construction_slots_kept: u64,
79    pub construction_slots_no_doable: u64,
80    pub generation_time: Duration,
81    pub evaluation_time: Duration,
82    pub selector_telemetry: Vec<SelectorTelemetry>,
83}
84
85impl SolverTelemetry {
86    pub const fn new_const() -> Self {
87        Self {
88            elapsed: Duration::ZERO,
89            step_count: 0,
90            moves_generated: 0,
91            moves_evaluated: 0,
92            moves_accepted: 0,
93            moves_applied: 0,
94            moves_not_doable: 0,
95            moves_acceptor_rejected: 0,
96            moves_forager_ignored: 0,
97            moves_hard_improving: 0,
98            moves_hard_neutral: 0,
99            moves_hard_worse: 0,
100            conflict_repair_provider_generated: 0,
101            conflict_repair_duplicate_filtered: 0,
102            conflict_repair_illegal_filtered: 0,
103            conflict_repair_not_doable_filtered: 0,
104            conflict_repair_hard_improving: 0,
105            conflict_repair_exposed: 0,
106            score_calculations: 0,
107            construction_slots_assigned: 0,
108            construction_slots_kept: 0,
109            construction_slots_no_doable: 0,
110            generation_time: Duration::ZERO,
111            evaluation_time: Duration::ZERO,
112            selector_telemetry: Vec::new(),
113        }
114    }
115}
116
117#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
118pub struct Throughput {
119    pub count: u64,
120    pub elapsed: Duration,
121}
122
123pub(crate) fn whole_units_per_second(count: u64, elapsed: Duration) -> u128 {
124    let nanos = elapsed.as_nanos();
125    if nanos == 0 {
126        0
127    } else {
128        u128::from(count)
129            .saturating_mul(1_000_000_000)
130            .checked_div(nanos)
131            .unwrap_or(0)
132    }
133}
134
135pub(crate) fn format_duration(duration: Duration) -> String {
136    let secs = duration.as_secs();
137    let nanos = duration.subsec_nanos();
138
139    if secs >= 60 {
140        let mins = secs / 60;
141        let rem_secs = secs % 60;
142        return format!("{mins}m {rem_secs}s");
143    }
144
145    if secs > 0 {
146        let millis = nanos / 1_000_000;
147        if millis == 0 {
148            return format!("{secs}s");
149        }
150        return format!("{secs}s {millis}ms");
151    }
152
153    let millis = nanos / 1_000_000;
154    if millis > 0 {
155        return format!("{millis}ms");
156    }
157
158    let micros = nanos / 1_000;
159    if micros > 0 {
160        return format!("{micros}us");
161    }
162
163    format!("{nanos}ns")
164}