use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct RuleStats {
pub rule_id: String,
pub activation_count: u64,
pub total_duration_us: u64,
pub inferences_produced: u64,
pub last_activation_us: u64,
}
impl RuleStats {
pub fn new(rule_id: impl Into<String>) -> Self {
Self {
rule_id: rule_id.into(),
..Default::default()
}
}
pub fn avg_duration_us(&self) -> f64 {
if self.activation_count == 0 {
0.0
} else {
self.total_duration_us as f64 / self.activation_count as f64
}
}
pub fn throughput_per_sec(&self) -> f64 {
if self.total_duration_us == 0 {
0.0
} else {
let secs = self.total_duration_us as f64 / 1_000_000.0;
self.inferences_produced as f64 / secs
}
}
pub fn record_activation(&mut self, duration_us: u64, inferences: u64, timestamp_us: u64) {
self.activation_count += 1;
self.total_duration_us += duration_us;
self.inferences_produced += inferences;
self.last_activation_us = timestamp_us;
}
}
pub struct RuleStatisticsCollector {
stats: HashMap<String, RuleStats>,
total_cycles: u64,
}
impl RuleStatisticsCollector {
pub fn new() -> Self {
Self {
stats: HashMap::new(),
total_cycles: 0,
}
}
pub fn record(&mut self, rule_id: &str, duration_us: u64, inferences: u64) {
let entry = self
.stats
.entry(rule_id.to_string())
.or_insert_with(|| RuleStats::new(rule_id));
let ts = entry.activation_count;
entry.record_activation(duration_us, inferences, ts);
}
pub fn increment_cycle(&mut self) {
self.total_cycles += 1;
}
pub fn get(&self, rule_id: &str) -> Option<&RuleStats> {
self.stats.get(rule_id)
}
pub fn all(&self) -> Vec<&RuleStats> {
self.stats.values().collect()
}
pub fn top_by_activations(&self, n: usize) -> Vec<&RuleStats> {
let mut sorted: Vec<&RuleStats> = self.stats.values().collect();
sorted.sort_by_key(|b| std::cmp::Reverse(b.activation_count));
sorted.truncate(n);
sorted
}
pub fn top_by_duration(&self, n: usize) -> Vec<&RuleStats> {
let mut sorted: Vec<&RuleStats> = self.stats.values().collect();
sorted.sort_by_key(|b| std::cmp::Reverse(b.total_duration_us));
sorted.truncate(n);
sorted
}
pub fn total_inferences(&self) -> u64 {
self.stats.values().map(|s| s.inferences_produced).sum()
}
pub fn total_cycles(&self) -> u64 {
self.total_cycles
}
pub fn reset(&mut self) {
self.stats.clear();
self.total_cycles = 0;
}
pub fn rule_count(&self) -> usize {
self.stats.len()
}
pub fn is_empty(&self) -> bool {
self.stats.is_empty()
}
pub fn hottest_rule(&self) -> Option<&RuleStats> {
self.stats.values().max_by_key(|s| s.activation_count)
}
pub fn total_duration_us(&self) -> u64 {
self.stats.values().map(|s| s.total_duration_us).sum()
}
}
impl Default for RuleStatisticsCollector {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_collector() -> RuleStatisticsCollector {
RuleStatisticsCollector::new()
}
#[test]
fn test_rule_stats_default() {
let s = RuleStats::default();
assert_eq!(s.activation_count, 0);
assert_eq!(s.total_duration_us, 0);
assert_eq!(s.inferences_produced, 0);
}
#[test]
fn test_rule_stats_new() {
let s = RuleStats::new("rule-1");
assert_eq!(s.rule_id, "rule-1");
assert_eq!(s.activation_count, 0);
}
#[test]
fn test_avg_duration_zero_activations() {
let s = RuleStats::new("r");
assert_eq!(s.avg_duration_us(), 0.0);
}
#[test]
fn test_avg_duration_single_activation() {
let mut s = RuleStats::new("r");
s.record_activation(100, 5, 0);
assert!((s.avg_duration_us() - 100.0).abs() < f64::EPSILON);
}
#[test]
fn test_avg_duration_multiple_activations() {
let mut s = RuleStats::new("r");
s.record_activation(200, 1, 0);
s.record_activation(400, 1, 1);
assert!((s.avg_duration_us() - 300.0).abs() < f64::EPSILON);
}
#[test]
fn test_throughput_zero_duration() {
let mut s = RuleStats::new("r");
s.activation_count = 1;
s.inferences_produced = 100;
assert_eq!(s.throughput_per_sec(), 0.0);
}
#[test]
fn test_throughput_non_zero() {
let mut s = RuleStats::new("r");
s.record_activation(1_000_000, 50, 0); assert!((s.throughput_per_sec() - 50.0).abs() < 1e-6);
}
#[test]
fn test_throughput_half_second() {
let mut s = RuleStats::new("r");
s.record_activation(500_000, 100, 0); assert!((s.throughput_per_sec() - 200.0).abs() < 1e-4);
}
#[test]
fn test_record_activation_increments_all_fields() {
let mut s = RuleStats::new("r");
s.record_activation(50, 3, 999);
assert_eq!(s.activation_count, 1);
assert_eq!(s.total_duration_us, 50);
assert_eq!(s.inferences_produced, 3);
assert_eq!(s.last_activation_us, 999);
}
#[test]
fn test_new_is_empty() {
let c = make_collector();
assert!(c.is_empty());
assert_eq!(c.rule_count(), 0);
assert_eq!(c.total_inferences(), 0);
assert_eq!(c.total_cycles(), 0);
}
#[test]
fn test_record_single_rule() {
let mut c = make_collector();
c.record("rule-a", 100, 5);
let s = c.get("rule-a").expect("should exist");
assert_eq!(s.activation_count, 1);
assert_eq!(s.total_duration_us, 100);
assert_eq!(s.inferences_produced, 5);
}
#[test]
fn test_record_same_rule_twice() {
let mut c = make_collector();
c.record("rule-a", 100, 5);
c.record("rule-a", 200, 10);
let s = c.get("rule-a").expect("should exist");
assert_eq!(s.activation_count, 2);
assert_eq!(s.total_duration_us, 300);
assert_eq!(s.inferences_produced, 15);
}
#[test]
fn test_record_multiple_rules() {
let mut c = make_collector();
c.record("rule-a", 100, 5);
c.record("rule-b", 200, 10);
c.record("rule-c", 300, 15);
assert_eq!(c.rule_count(), 3);
}
#[test]
fn test_get_nonexistent_rule() {
let c = make_collector();
assert!(c.get("no-such-rule").is_none());
}
#[test]
fn test_all_returns_all_rules() {
let mut c = make_collector();
c.record("r1", 10, 1);
c.record("r2", 20, 2);
let all = c.all();
assert_eq!(all.len(), 2);
}
#[test]
fn test_all_empty() {
let c = make_collector();
assert!(c.all().is_empty());
}
#[test]
fn test_top_by_activations_ordering() {
let mut c = make_collector();
c.record("slow", 100, 1);
c.record("slow", 100, 1); c.record("fast", 50, 1); let top = c.top_by_activations(2);
assert_eq!(top[0].rule_id, "slow");
}
#[test]
fn test_top_by_activations_limit() {
let mut c = make_collector();
for i in 0..10u64 {
c.record(&format!("rule-{}", i), 10 * i, i);
}
let top = c.top_by_activations(3);
assert_eq!(top.len(), 3);
}
#[test]
fn test_top_by_activations_n_larger_than_count() {
let mut c = make_collector();
c.record("only", 10, 1);
let top = c.top_by_activations(100);
assert_eq!(top.len(), 1);
}
#[test]
fn test_top_by_duration_ordering() {
let mut c = make_collector();
c.record("cheap", 10, 1);
c.record("expensive", 9999, 1);
let top = c.top_by_duration(2);
assert_eq!(top[0].rule_id, "expensive");
}
#[test]
fn test_top_by_duration_limit() {
let mut c = make_collector();
c.record("r1", 100, 1);
c.record("r2", 200, 1);
c.record("r3", 300, 1);
let top = c.top_by_duration(2);
assert_eq!(top.len(), 2);
assert_eq!(top[0].rule_id, "r3");
}
#[test]
fn test_total_inferences_sum() {
let mut c = make_collector();
c.record("r1", 10, 3);
c.record("r2", 20, 7);
c.record("r1", 10, 5); assert_eq!(c.total_inferences(), 15);
}
#[test]
fn test_total_inferences_empty() {
let c = make_collector();
assert_eq!(c.total_inferences(), 0);
}
#[test]
fn test_increment_cycle() {
let mut c = make_collector();
c.increment_cycle();
c.increment_cycle();
assert_eq!(c.total_cycles(), 2);
}
#[test]
fn test_increment_cycle_many() {
let mut c = make_collector();
for _ in 0..100 {
c.increment_cycle();
}
assert_eq!(c.total_cycles(), 100);
}
#[test]
fn test_reset_clears_all() {
let mut c = make_collector();
c.record("r1", 100, 5);
c.record("r2", 200, 10);
c.increment_cycle();
c.increment_cycle();
c.reset();
assert!(c.is_empty());
assert_eq!(c.total_cycles(), 0);
assert_eq!(c.total_inferences(), 0);
}
#[test]
fn test_reset_then_reuse() {
let mut c = make_collector();
c.record("r1", 100, 5);
c.reset();
c.record("r2", 50, 3);
assert_eq!(c.rule_count(), 1);
assert_eq!(c.total_inferences(), 3);
}
#[test]
fn test_hottest_rule_empty() {
let c = make_collector();
assert!(c.hottest_rule().is_none());
}
#[test]
fn test_hottest_rule_single() {
let mut c = make_collector();
c.record("only", 10, 1);
let h = c.hottest_rule().expect("should have hottest");
assert_eq!(h.rule_id, "only");
}
#[test]
fn test_hottest_rule_multiple() {
let mut c = make_collector();
c.record("cold", 10, 1);
c.record("hot", 100, 1);
c.record("hot", 100, 1);
c.record("hot", 100, 1);
let h = c.hottest_rule().expect("must exist");
assert_eq!(h.rule_id, "hot");
}
#[test]
fn test_total_duration_us() {
let mut c = make_collector();
c.record("r1", 150, 1);
c.record("r2", 250, 1);
assert_eq!(c.total_duration_us(), 400);
}
#[test]
fn test_default_constructor() {
let c = RuleStatisticsCollector::default();
assert!(c.is_empty());
}
#[test]
fn test_top_by_activations_empty() {
let c = make_collector();
assert!(c.top_by_activations(5).is_empty());
}
#[test]
fn test_top_by_duration_empty() {
let c = make_collector();
assert!(c.top_by_duration(5).is_empty());
}
#[test]
fn test_avg_duration_three_activations() {
let mut s = RuleStats::new("r");
s.record_activation(100, 0, 0);
s.record_activation(200, 0, 1);
s.record_activation(300, 0, 2);
assert!((s.avg_duration_us() - 200.0).abs() < f64::EPSILON);
}
#[test]
fn test_throughput_multiple_activations() {
let mut s = RuleStats::new("r");
s.record_activation(1_000_000, 30, 0); s.record_activation(1_000_000, 70, 1); assert!((s.throughput_per_sec() - 50.0).abs() < 1e-6);
}
#[test]
fn test_rule_count_after_multiple_records() {
let mut c = make_collector();
c.record("a", 10, 1);
c.record("b", 20, 2);
c.record("a", 30, 3); assert_eq!(c.rule_count(), 2);
}
#[test]
fn test_record_zero_inferences() {
let mut c = make_collector();
c.record("r", 100, 0);
let s = c.get("r").expect("must exist");
assert_eq!(s.inferences_produced, 0);
assert_eq!(s.activation_count, 1);
}
#[test]
fn test_record_zero_duration() {
let mut c = make_collector();
c.record("r", 0, 5);
let s = c.get("r").expect("must exist");
assert_eq!(s.total_duration_us, 0);
assert_eq!(s.avg_duration_us(), 0.0);
}
#[test]
fn test_all_returns_correct_rule_ids() {
let mut c = make_collector();
c.record("rule-alpha", 10, 1);
c.record("rule-beta", 20, 2);
let ids: std::collections::HashSet<&str> =
c.all().iter().map(|s| s.rule_id.as_str()).collect();
assert!(ids.contains("rule-alpha"));
assert!(ids.contains("rule-beta"));
}
#[test]
fn test_top_by_activations_ties() {
let mut c = make_collector();
c.record("r1", 10, 1);
c.record("r2", 10, 1);
let top = c.top_by_activations(1);
assert_eq!(top.len(), 1);
}
#[test]
fn test_top_by_duration_zero_elements_requested() {
let mut c = make_collector();
c.record("r1", 100, 1);
assert!(c.top_by_duration(0).is_empty());
}
#[test]
fn test_total_duration_empty() {
let c = make_collector();
assert_eq!(c.total_duration_us(), 0);
}
#[test]
fn test_hottest_rule_after_reset() {
let mut c = make_collector();
c.record("hot", 10, 1);
c.record("hot", 10, 1);
c.reset();
assert!(c.hottest_rule().is_none());
}
#[test]
fn test_cycles_reset_to_zero() {
let mut c = make_collector();
c.increment_cycle();
c.increment_cycle();
c.reset();
assert_eq!(c.total_cycles(), 0);
}
}