ruvector_sparse_inference/precision/
telemetry.rs1use super::lanes::PrecisionLane;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::time::{Duration, Instant};
9
10#[derive(Debug, Clone, Default, Serialize, Deserialize)]
12pub struct LaneStats {
13 pub operations: u64,
15
16 pub total_time_ns: u64,
18
19 pub avg_time_ns: u64,
21
22 pub peak_time_ns: u64,
24
25 pub bytes_processed: u64,
27
28 pub avg_active_set_size: f32,
30
31 pub errors: u64,
33
34 pub escalations: u64,
36
37 pub demotions: u64,
39}
40
41impl LaneStats {
42 pub fn record_operation(&mut self, duration_ns: u64, bytes: u64, active_set_size: usize) {
44 self.operations += 1;
45 self.total_time_ns += duration_ns;
46 self.bytes_processed += bytes;
47
48 let ops = self.operations as f32;
50 self.avg_time_ns = (self.total_time_ns / self.operations) as u64;
51 self.avg_active_set_size =
52 (self.avg_active_set_size * (ops - 1.0) + active_set_size as f32) / ops;
53
54 if duration_ns > self.peak_time_ns {
56 self.peak_time_ns = duration_ns;
57 }
58 }
59
60 pub fn record_error(&mut self) {
62 self.errors += 1;
63 }
64
65 pub fn record_escalation(&mut self) {
67 self.escalations += 1;
68 }
69
70 pub fn record_demotion(&mut self) {
72 self.demotions += 1;
73 }
74
75 pub fn throughput_bps(&self) -> f64 {
77 if self.total_time_ns == 0 {
78 return 0.0;
79 }
80 (self.bytes_processed as f64 * 1_000_000_000.0) / self.total_time_ns as f64
81 }
82}
83
84#[derive(Debug, Clone)]
86pub struct LaneTelemetry {
87 pub lane_stats: HashMap<PrecisionLane, LaneStats>,
89
90 pub current_lane: PrecisionLane,
92
93 pub transitions: u64,
95
96 transition_history: Vec<LaneTransition>,
98
99 start_time: Option<Instant>,
101
102 pub session_duration_secs: f64,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct LaneTransition {
109 pub from: PrecisionLane,
111
112 pub to: PrecisionLane,
114
115 pub reason: TransitionReason,
117
118 pub timestamp_secs: f64,
120}
121
122#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
124pub enum TransitionReason {
125 Novelty,
127 DriftPersistence,
129 StabilityReturned,
131 VelocityStalled,
133 ActiveSetShrunk,
135 Manual,
137 Init,
139}
140
141impl LaneTelemetry {
142 pub fn new(initial_lane: PrecisionLane) -> Self {
144 let mut lane_stats = HashMap::new();
145 lane_stats.insert(PrecisionLane::Bit3, LaneStats::default());
146 lane_stats.insert(PrecisionLane::Bit5, LaneStats::default());
147 lane_stats.insert(PrecisionLane::Bit7, LaneStats::default());
148 lane_stats.insert(PrecisionLane::Float32, LaneStats::default());
149
150 Self {
151 lane_stats,
152 current_lane: initial_lane,
153 transitions: 0,
154 transition_history: Vec::with_capacity(100),
155 start_time: Some(Instant::now()),
156 session_duration_secs: 0.0,
157 }
158 }
159
160 pub fn start_session(&mut self) {
162 self.start_time = Some(Instant::now());
163 }
164
165 pub fn record_operation(&mut self, duration: Duration, bytes: u64, active_set_size: usize) {
167 let duration_ns = duration.as_nanos() as u64;
168
169 if let Some(stats) = self.lane_stats.get_mut(&self.current_lane) {
170 stats.record_operation(duration_ns, bytes, active_set_size);
171 }
172
173 if let Some(start) = self.start_time {
175 self.session_duration_secs = start.elapsed().as_secs_f64();
176 }
177 }
178
179 pub fn record_transition(&mut self, from: PrecisionLane, to: PrecisionLane, reason: TransitionReason) {
181 self.transitions += 1;
182 self.current_lane = to;
183
184 if to.bits() > from.bits() {
186 if let Some(stats) = self.lane_stats.get_mut(&from) {
187 stats.record_escalation();
188 }
189 } else {
190 if let Some(stats) = self.lane_stats.get_mut(&to) {
191 stats.record_demotion();
192 }
193 }
194
195 let timestamp_secs = self.start_time
197 .map(|s| s.elapsed().as_secs_f64())
198 .unwrap_or(0.0);
199
200 let transition = LaneTransition {
201 from,
202 to,
203 reason,
204 timestamp_secs,
205 };
206
207 if self.transition_history.len() >= 100 {
208 self.transition_history.remove(0);
209 }
210 self.transition_history.push(transition);
211 }
212
213 pub fn record_error(&mut self) {
215 if let Some(stats) = self.lane_stats.get_mut(&self.current_lane) {
216 stats.record_error();
217 }
218 }
219
220 pub fn get_lane_stats(&self, lane: PrecisionLane) -> Option<&LaneStats> {
222 self.lane_stats.get(&lane)
223 }
224
225 pub fn total_operations(&self) -> u64 {
227 self.lane_stats.values().map(|s| s.operations).sum()
228 }
229
230 pub fn total_errors(&self) -> u64 {
232 self.lane_stats.values().map(|s| s.errors).sum()
233 }
234
235 pub fn lane_distribution(&self) -> HashMap<PrecisionLane, f32> {
237 let total = self.total_operations() as f32;
238 if total == 0.0 {
239 return HashMap::new();
240 }
241
242 self.lane_stats
243 .iter()
244 .map(|(lane, stats)| (*lane, (stats.operations as f32 / total) * 100.0))
245 .collect()
246 }
247
248 pub fn transition_history(&self) -> &[LaneTransition] {
250 &self.transition_history
251 }
252
253 pub fn summary_report(&self) -> TelemetrySummary {
255 TelemetrySummary {
256 session_duration_secs: self.session_duration_secs,
257 total_operations: self.total_operations(),
258 total_transitions: self.transitions,
259 total_errors: self.total_errors(),
260 lane_distribution: self.lane_distribution(),
261 avg_operations_per_sec: if self.session_duration_secs > 0.0 {
262 self.total_operations() as f64 / self.session_duration_secs
263 } else {
264 0.0
265 },
266 current_lane: self.current_lane,
267 }
268 }
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct TelemetrySummary {
274 pub session_duration_secs: f64,
275 pub total_operations: u64,
276 pub total_transitions: u64,
277 pub total_errors: u64,
278 pub lane_distribution: HashMap<PrecisionLane, f32>,
279 pub avg_operations_per_sec: f64,
280 pub current_lane: PrecisionLane,
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn test_lane_stats_recording() {
289 let mut stats = LaneStats::default();
290
291 stats.record_operation(1000, 64, 100);
292 stats.record_operation(2000, 64, 100);
293
294 assert_eq!(stats.operations, 2);
295 assert_eq!(stats.total_time_ns, 3000);
296 assert_eq!(stats.avg_time_ns, 1500);
297 assert_eq!(stats.bytes_processed, 128);
298 }
299
300 #[test]
301 fn test_telemetry_transitions() {
302 let mut telemetry = LaneTelemetry::new(PrecisionLane::Bit5);
303
304 telemetry.record_transition(
305 PrecisionLane::Bit5,
306 PrecisionLane::Bit7,
307 TransitionReason::Novelty,
308 );
309
310 assert_eq!(telemetry.transitions, 1);
311 assert_eq!(telemetry.current_lane, PrecisionLane::Bit7);
312 assert_eq!(telemetry.transition_history.len(), 1);
313 }
314
315 #[test]
316 fn test_lane_distribution() {
317 let mut telemetry = LaneTelemetry::new(PrecisionLane::Bit5);
318
319 for _ in 0..30 {
321 telemetry.current_lane = PrecisionLane::Bit3;
322 telemetry.record_operation(Duration::from_nanos(100), 8, 10);
323 }
324 for _ in 0..50 {
325 telemetry.current_lane = PrecisionLane::Bit5;
326 telemetry.record_operation(Duration::from_nanos(200), 16, 50);
327 }
328 for _ in 0..20 {
329 telemetry.current_lane = PrecisionLane::Bit7;
330 telemetry.record_operation(Duration::from_nanos(500), 32, 100);
331 }
332
333 let distribution = telemetry.lane_distribution();
334
335 assert!((distribution[&PrecisionLane::Bit3] - 30.0).abs() < 0.1);
336 assert!((distribution[&PrecisionLane::Bit5] - 50.0).abs() < 0.1);
337 assert!((distribution[&PrecisionLane::Bit7] - 20.0).abs() < 0.1);
338 }
339}