ringkernel_procint/analytics/
kpi_tracking.rs1use std::collections::VecDeque;
6use std::time::{Duration, Instant};
7
8#[derive(Debug)]
10pub struct KPITracker {
11 throughput_samples: VecDeque<(Instant, u64)>,
13 duration_samples: VecDeque<f32>,
15 fitness_samples: VecDeque<f32>,
17 window_size: usize,
19 last_update: Instant,
21 pub current: ProcessKPIs,
23}
24
25impl Default for KPITracker {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl KPITracker {
32 pub fn new() -> Self {
34 Self {
35 throughput_samples: VecDeque::with_capacity(100),
36 duration_samples: VecDeque::with_capacity(100),
37 fitness_samples: VecDeque::with_capacity(100),
38 window_size: 100,
39 last_update: Instant::now(),
40 current: ProcessKPIs::default(),
41 }
42 }
43
44 pub fn with_window_size(mut self, size: usize) -> Self {
46 self.window_size = size;
47 self
48 }
49
50 pub fn record_events(&mut self, count: u64) {
52 let now = Instant::now();
53 self.throughput_samples.push_back((now, count));
54
55 while let Some(&(time, _)) = self.throughput_samples.front() {
57 if now.duration_since(time) > Duration::from_secs(10) {
58 self.throughput_samples.pop_front();
59 } else {
60 break;
61 }
62 }
63
64 self.update_throughput();
65 self.last_update = now;
66 }
67
68 pub fn record_duration(&mut self, duration_ms: f32) {
70 self.duration_samples.push_back(duration_ms);
71 if self.duration_samples.len() > self.window_size {
72 self.duration_samples.pop_front();
73 }
74 self.update_duration();
75 }
76
77 pub fn record_fitness(&mut self, fitness: f32) {
79 self.fitness_samples.push_back(fitness);
80 if self.fitness_samples.len() > self.window_size {
81 self.fitness_samples.pop_front();
82 }
83 self.update_fitness();
84 }
85
86 pub fn update(&mut self, events: u64, duration_ms: f32, fitness: f32) {
88 self.record_events(events);
89 self.record_duration(duration_ms);
90 self.record_fitness(fitness);
91 }
92
93 fn update_throughput(&mut self) {
94 if self.throughput_samples.len() < 2 {
95 return;
96 }
97
98 let (first_time, _) = self.throughput_samples.front().unwrap();
99 let (last_time, _) = self.throughput_samples.back().unwrap();
100 let elapsed = last_time.duration_since(*first_time).as_secs_f64();
101
102 if elapsed > 0.0 {
103 let total_events: u64 = self.throughput_samples.iter().map(|(_, c)| c).sum();
104 self.current.events_per_second = total_events as f64 / elapsed;
105 }
106 }
107
108 fn update_duration(&mut self) {
109 if self.duration_samples.is_empty() {
110 return;
111 }
112
113 let sum: f32 = self.duration_samples.iter().sum();
114 self.current.avg_case_duration_ms = sum / self.duration_samples.len() as f32;
115
116 self.current.min_duration_ms = self
118 .duration_samples
119 .iter()
120 .copied()
121 .fold(f32::MAX, f32::min);
122 self.current.max_duration_ms = self
123 .duration_samples
124 .iter()
125 .copied()
126 .fold(f32::MIN, f32::max);
127 }
128
129 fn update_fitness(&mut self) {
130 if self.fitness_samples.is_empty() {
131 return;
132 }
133
134 let sum: f32 = self.fitness_samples.iter().sum();
135 self.current.avg_fitness = sum / self.fitness_samples.len() as f32;
136 }
137
138 pub fn set_pattern_count(&mut self, count: u64) {
140 self.current.pattern_count = count;
141 }
142
143 pub fn set_cases_completed(&mut self, count: u64) {
145 self.current.cases_completed = count;
146 }
147
148 pub fn set_conformance_rate(&mut self, rate: f32) {
150 self.current.conformance_rate = rate;
151 }
152
153 pub fn kpis(&self) -> &ProcessKPIs {
155 &self.current
156 }
157
158 pub fn reset(&mut self) {
160 self.throughput_samples.clear();
161 self.duration_samples.clear();
162 self.fitness_samples.clear();
163 self.current = ProcessKPIs::default();
164 }
165}
166
167#[derive(Debug, Clone, Default)]
169pub struct ProcessKPIs {
170 pub events_per_second: f64,
172 pub avg_case_duration_ms: f32,
174 pub min_duration_ms: f32,
176 pub max_duration_ms: f32,
178 pub avg_fitness: f32,
180 pub pattern_count: u64,
182 pub cases_completed: u64,
184 pub conformance_rate: f32,
186}
187
188impl ProcessKPIs {
189 pub fn format_throughput(&self) -> String {
191 if self.events_per_second >= 1_000_000.0 {
192 format!("{:.2}M/s", self.events_per_second / 1_000_000.0)
193 } else if self.events_per_second >= 1_000.0 {
194 format!("{:.2}K/s", self.events_per_second / 1_000.0)
195 } else {
196 format!("{:.0}/s", self.events_per_second)
197 }
198 }
199
200 pub fn format_duration(&self) -> String {
202 if self.avg_case_duration_ms >= 60000.0 {
203 format!("{:.1}m", self.avg_case_duration_ms / 60000.0)
204 } else if self.avg_case_duration_ms >= 1000.0 {
205 format!("{:.1}s", self.avg_case_duration_ms / 1000.0)
206 } else {
207 format!("{:.0}ms", self.avg_case_duration_ms)
208 }
209 }
210
211 pub fn format_fitness(&self) -> String {
213 format!("{:.1}%", self.avg_fitness * 100.0)
214 }
215
216 pub fn health_status(&self) -> HealthStatus {
218 if self.avg_fitness >= 0.95 && self.conformance_rate >= 0.90 {
219 HealthStatus::Excellent
220 } else if self.avg_fitness >= 0.80 && self.conformance_rate >= 0.70 {
221 HealthStatus::Good
222 } else if self.avg_fitness >= 0.50 {
223 HealthStatus::Warning
224 } else {
225 HealthStatus::Critical
226 }
227 }
228}
229
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
232pub enum HealthStatus {
233 Excellent,
235 Good,
237 Warning,
239 Critical,
241}
242
243impl HealthStatus {
244 pub fn color(&self) -> [u8; 3] {
246 match self {
247 HealthStatus::Excellent => [40, 167, 69], HealthStatus::Good => [23, 162, 184], HealthStatus::Warning => [255, 193, 7], HealthStatus::Critical => [220, 53, 69], }
252 }
253
254 pub fn name(&self) -> &'static str {
256 match self {
257 HealthStatus::Excellent => "Excellent",
258 HealthStatus::Good => "Good",
259 HealthStatus::Warning => "Warning",
260 HealthStatus::Critical => "Critical",
261 }
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn test_kpi_tracking() {
271 let mut tracker = KPITracker::new();
272
273 tracker.record_events(1000);
274 tracker.record_duration(5000.0);
275 tracker.record_fitness(0.95);
276
277 let kpis = tracker.kpis();
278 assert_eq!(kpis.avg_fitness, 0.95);
279 assert_eq!(kpis.avg_case_duration_ms, 5000.0);
280 }
281
282 #[test]
283 fn test_throughput_formatting() {
284 let mut kpis = ProcessKPIs {
285 events_per_second: 1_500_000.0,
286 ..Default::default()
287 };
288 assert!(kpis.format_throughput().contains("M/s"));
289
290 kpis.events_per_second = 42_000.0;
291 assert!(kpis.format_throughput().contains("K/s"));
292 }
293
294 #[test]
295 fn test_health_status() {
296 let mut kpis = ProcessKPIs {
297 avg_fitness: 0.98,
298 conformance_rate: 0.95,
299 ..Default::default()
300 };
301 assert_eq!(kpis.health_status(), HealthStatus::Excellent);
302
303 kpis.avg_fitness = 0.40;
304 assert_eq!(kpis.health_status(), HealthStatus::Critical);
305 }
306}