1use std::collections::HashMap;
8
9#[derive(Debug, Clone)]
13pub struct RuleProfile {
14 pub rule_id: u64,
16 pub rule_name: String,
18 pub invocations: u64,
20 pub successes: u64,
22 pub failures: u64,
24 pub total_time_us: u64,
26 pub min_time_us: u64,
28 pub max_time_us: u64,
30}
31
32impl RuleProfile {
33 #[inline]
37 pub fn success_rate(&self) -> f64 {
38 self.successes as f64 / self.invocations.max(1) as f64
39 }
40
41 #[inline]
45 pub fn avg_time_us(&self) -> f64 {
46 self.total_time_us as f64 / self.invocations.max(1) as f64
47 }
48
49 #[inline]
51 pub fn is_hotspot(&self, threshold_us: u64) -> bool {
52 self.avg_time_us() > threshold_us as f64
53 }
54}
55
56#[derive(Debug, Clone)]
60pub struct ProfilerStats {
61 pub total_rules: usize,
63 pub total_invocations: u64,
65 pub total_time_us: u64,
67 pub hotspot_count: usize,
69}
70
71impl ProfilerStats {
72 pub fn avg_success_rate(&self, profiles: &[RuleProfile]) -> f64 {
76 if profiles.is_empty() {
77 return 0.0;
78 }
79 let sum: f64 = profiles.iter().map(|p| p.success_rate()).sum();
80 sum / profiles.len() as f64
81 }
82}
83
84pub struct RuleExecutionProfiler {
105 pub profiles: HashMap<u64, RuleProfile>,
107 pub hotspot_threshold_us: u64,
109}
110
111impl RuleExecutionProfiler {
112 pub fn new(hotspot_threshold_us: u64) -> Self {
117 Self {
118 profiles: HashMap::new(),
119 hotspot_threshold_us,
120 }
121 }
122
123 pub fn record_invocation(
132 &mut self,
133 rule_id: u64,
134 rule_name: &str,
135 time_us: u64,
136 success: bool,
137 ) {
138 let profile = self.profiles.entry(rule_id).or_insert_with(|| RuleProfile {
139 rule_id,
140 rule_name: rule_name.to_owned(),
141 invocations: 0,
142 successes: 0,
143 failures: 0,
144 total_time_us: 0,
145 min_time_us: u64::MAX,
147 max_time_us: 0,
148 });
149
150 profile.invocations += 1;
151 if success {
152 profile.successes += 1;
153 } else {
154 profile.failures += 1;
155 }
156 profile.total_time_us += time_us;
157 profile.min_time_us = profile.min_time_us.min(time_us);
158 profile.max_time_us = profile.max_time_us.max(time_us);
159 }
160
161 pub fn hotspots(&self) -> Vec<&RuleProfile> {
164 let threshold = self.hotspot_threshold_us;
165 let mut result: Vec<&RuleProfile> = self
166 .profiles
167 .values()
168 .filter(|p| p.is_hotspot(threshold))
169 .collect();
170 result.sort_by(|a, b| {
171 b.avg_time_us()
172 .partial_cmp(&a.avg_time_us())
173 .unwrap_or(std::cmp::Ordering::Equal)
174 });
175 result
176 }
177
178 pub fn top_rules_by_invocations(&self, n: usize) -> Vec<&RuleProfile> {
180 let mut result: Vec<&RuleProfile> = self.profiles.values().collect();
181 result.sort_by_key(|b| std::cmp::Reverse(b.invocations));
182 result.truncate(n);
183 result
184 }
185
186 pub fn slowest_rules(&self, n: usize) -> Vec<&RuleProfile> {
188 let mut result: Vec<&RuleProfile> = self.profiles.values().collect();
189 result.sort_by(|a, b| {
190 b.avg_time_us()
191 .partial_cmp(&a.avg_time_us())
192 .unwrap_or(std::cmp::Ordering::Equal)
193 });
194 result.truncate(n);
195 result
196 }
197
198 pub fn reset_rule(&mut self, rule_id: u64) -> bool {
203 self.profiles.remove(&rule_id).is_some()
204 }
205
206 pub fn stats(&self) -> ProfilerStats {
208 let threshold = self.hotspot_threshold_us;
209 let total_invocations = self.profiles.values().map(|p| p.invocations).sum();
210 let total_time_us = self.profiles.values().map(|p| p.total_time_us).sum();
211 let hotspot_count = self
212 .profiles
213 .values()
214 .filter(|p| p.is_hotspot(threshold))
215 .count();
216
217 ProfilerStats {
218 total_rules: self.profiles.len(),
219 total_invocations,
220 total_time_us,
221 hotspot_count,
222 }
223 }
224}
225
226#[cfg(test)]
229mod tests {
230 use super::*;
231
232 fn make_profiler() -> RuleExecutionProfiler {
235 RuleExecutionProfiler::new(1_000)
236 }
237
238 #[test]
240 fn test_record_creates_profile() {
241 let mut p = make_profiler();
242 p.record_invocation(42, "ancestor", 200, true);
243 assert!(p.profiles.contains_key(&42));
244 let prof = &p.profiles[&42];
245 assert_eq!(prof.rule_id, 42);
246 assert_eq!(prof.rule_name, "ancestor");
247 }
248
249 #[test]
251 fn test_invocation_count() {
252 let mut p = make_profiler();
253 p.record_invocation(1, "rule_a", 100, true);
254 p.record_invocation(1, "rule_a", 200, false);
255 p.record_invocation(1, "rule_a", 300, true);
256 assert_eq!(p.profiles[&1].invocations, 3);
257 }
258
259 #[test]
261 fn test_success_tracking() {
262 let mut p = make_profiler();
263 p.record_invocation(2, "rule_b", 100, true);
264 p.record_invocation(2, "rule_b", 100, true);
265 p.record_invocation(2, "rule_b", 100, false);
266 assert_eq!(p.profiles[&2].successes, 2);
267 }
268
269 #[test]
271 fn test_failure_tracking() {
272 let mut p = make_profiler();
273 p.record_invocation(3, "rule_c", 50, false);
274 p.record_invocation(3, "rule_c", 50, false);
275 assert_eq!(p.profiles[&3].failures, 2);
276 }
277
278 #[test]
280 fn test_min_time_update() {
281 let mut p = make_profiler();
282 p.record_invocation(4, "rule_d", 500, true);
283 p.record_invocation(4, "rule_d", 100, true);
284 p.record_invocation(4, "rule_d", 300, false);
285 assert_eq!(p.profiles[&4].min_time_us, 100);
286 }
287
288 #[test]
290 fn test_max_time_update() {
291 let mut p = make_profiler();
292 p.record_invocation(5, "rule_e", 200, true);
293 p.record_invocation(5, "rule_e", 800, false);
294 p.record_invocation(5, "rule_e", 50, true);
295 assert_eq!(p.profiles[&5].max_time_us, 800);
296 }
297
298 #[test]
300 fn test_min_time_initialized_correctly() {
301 let mut p = make_profiler();
302 p.record_invocation(6, "rule_f", 777, true);
304 assert_eq!(p.profiles[&6].min_time_us, 777);
305 }
306
307 #[test]
309 fn test_avg_time_us() {
310 let mut p = make_profiler();
311 p.record_invocation(7, "rule_g", 200, true);
312 p.record_invocation(7, "rule_g", 400, false);
313 let avg = p.profiles[&7].avg_time_us();
315 assert!((avg - 300.0).abs() < f64::EPSILON);
316 }
317
318 #[test]
320 fn test_avg_time_us_zero_invocations() {
321 let prof = RuleProfile {
322 rule_id: 99,
323 rule_name: "ghost".to_owned(),
324 invocations: 0,
325 successes: 0,
326 failures: 0,
327 total_time_us: 0,
328 min_time_us: u64::MAX,
329 max_time_us: 0,
330 };
331 assert_eq!(prof.avg_time_us(), 0.0);
332 }
333
334 #[test]
336 fn test_success_rate() {
337 let mut p = make_profiler();
338 p.record_invocation(8, "rule_h", 100, true);
339 p.record_invocation(8, "rule_h", 100, false);
340 p.record_invocation(8, "rule_h", 100, false);
341 p.record_invocation(8, "rule_h", 100, true);
342 let rate = p.profiles[&8].success_rate();
344 assert!((rate - 0.5).abs() < f64::EPSILON);
345 }
346
347 #[test]
349 fn test_is_hotspot() {
350 let mut p = make_profiler(); p.record_invocation(9, "slow_rule", 2_000, true);
353 assert!(p.profiles[&9].is_hotspot(1_000));
354 let mut p2 = make_profiler();
356 p2.record_invocation(10, "fast_rule", 500, true);
357 assert!(!p2.profiles[&10].is_hotspot(1_000));
358 }
359
360 #[test]
362 fn test_hotspots_sorted_desc() {
363 let mut p = RuleExecutionProfiler::new(500);
364 p.record_invocation(1, "hot_a", 1_500, true);
366 p.record_invocation(2, "hot_b", 2_500, true);
368 p.record_invocation(3, "cold", 300, false);
370
371 let hs = p.hotspots();
372 assert_eq!(hs.len(), 2);
373 assert_eq!(hs[0].rule_id, 2);
375 assert_eq!(hs[1].rule_id, 1);
376 }
377
378 #[test]
380 fn test_top_rules_by_invocations() {
381 let mut p = make_profiler();
382 for _ in 0..5 {
383 p.record_invocation(1, "frequent", 10, true);
384 }
385 for _ in 0..2 {
386 p.record_invocation(2, "infrequent", 10, true);
387 }
388 for _ in 0..8 {
389 p.record_invocation(3, "most_frequent", 10, true);
390 }
391
392 let top = p.top_rules_by_invocations(2);
393 assert_eq!(top.len(), 2);
394 assert_eq!(top[0].rule_id, 3); assert_eq!(top[1].rule_id, 1); }
397
398 #[test]
400 fn test_slowest_rules() {
401 let mut p = make_profiler();
402 p.record_invocation(1, "fast", 100, true);
403 p.record_invocation(2, "medium", 500, true);
404 p.record_invocation(3, "slow", 3_000, true);
405
406 let slowest = p.slowest_rules(2);
407 assert_eq!(slowest.len(), 2);
408 assert_eq!(slowest[0].rule_id, 3);
409 assert_eq!(slowest[1].rule_id, 2);
410 }
411
412 #[test]
414 fn test_reset_rule_existing() {
415 let mut p = make_profiler();
416 p.record_invocation(11, "rule_to_reset", 200, true);
417 let removed = p.reset_rule(11);
418 assert!(removed);
419 assert!(!p.profiles.contains_key(&11));
420 }
421
422 #[test]
424 fn test_reset_rule_absent() {
425 let mut p = make_profiler();
426 assert!(!p.reset_rule(999));
427 }
428
429 #[test]
431 fn test_stats_total_invocations() {
432 let mut p = make_profiler();
433 p.record_invocation(1, "r1", 100, true);
434 p.record_invocation(1, "r1", 200, false);
435 p.record_invocation(2, "r2", 300, true);
436 let stats = p.stats();
437 assert_eq!(stats.total_invocations, 3);
438 assert_eq!(stats.total_rules, 2);
439 }
440
441 #[test]
443 fn test_stats_total_time_us() {
444 let mut p = make_profiler();
445 p.record_invocation(1, "r1", 400, true);
446 p.record_invocation(2, "r2", 600, false);
447 let stats = p.stats();
448 assert_eq!(stats.total_time_us, 1_000);
449 }
450
451 #[test]
453 fn test_stats_hotspot_count() {
454 let mut p = RuleExecutionProfiler::new(1_000);
455 p.record_invocation(1, "slow", 2_000, true); p.record_invocation(2, "fast", 500, true); let stats = p.stats();
458 assert_eq!(stats.hotspot_count, 1);
459 }
460
461 #[test]
463 fn test_avg_success_rate() {
464 let mut p = make_profiler();
465 p.record_invocation(1, "r1", 100, true);
467 p.record_invocation(1, "r1", 100, false);
468 p.record_invocation(2, "r2", 100, true);
470 p.record_invocation(2, "r2", 100, true);
471
472 let profiles: Vec<RuleProfile> = p.profiles.values().cloned().collect();
473 let stats = p.stats();
474 let avg_rate = stats.avg_success_rate(&profiles);
476 assert!((avg_rate - 0.75).abs() < 1e-10);
477 }
478
479 #[test]
481 fn test_avg_success_rate_empty() {
482 let p = make_profiler();
483 let stats = p.stats();
484 assert_eq!(stats.avg_success_rate(&[]), 0.0);
485 }
486
487 #[test]
489 fn test_multiple_rules_isolated() {
490 let mut p = make_profiler();
491 p.record_invocation(10, "rule_x", 1_000, true);
492 p.record_invocation(20, "rule_y", 2_000, false);
493
494 assert_eq!(p.profiles[&10].invocations, 1);
495 assert_eq!(p.profiles[&20].invocations, 1);
496 assert_eq!(p.profiles[&10].failures, 0);
497 assert_eq!(p.profiles[&20].successes, 0);
498 }
499
500 #[test]
502 fn test_total_time_us_accumulation() {
503 let mut p = make_profiler();
504 p.record_invocation(5, "acc_rule", 100, true);
505 p.record_invocation(5, "acc_rule", 200, false);
506 p.record_invocation(5, "acc_rule", 700, true);
507 assert_eq!(p.profiles[&5].total_time_us, 1_000);
508 }
509}