Skip to main content

vtcode_core/metrics/
skill_metrics.rs

1use chrono::{DateTime, Utc};
2use hashbrown::HashMap;
3use serde::{Deserialize, Serialize};
4use std::collections::VecDeque;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct SkillMetrics {
8    pub total_skills: u64,
9    pub active_skills: u64,
10    pub total_executions: u64,
11    pub skill_stats: HashMap<String, SkillStats>,
12    pub recent_skill_usage: VecDeque<SkillUsageRecord>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct SkillStats {
17    pub name: String,
18    pub language: String,
19    pub execution_count: u64,
20    pub success_count: u64,
21    pub total_duration_ms: u64,
22    pub created_at: DateTime<Utc>,
23    pub last_used: DateTime<Utc>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct SkillUsageRecord {
28    pub skill_name: String,
29    pub success: bool,
30    pub duration_ms: u64,
31    pub timestamp: DateTime<Utc>,
32}
33
34impl SkillMetrics {
35    pub fn new() -> Self {
36        Self {
37            total_skills: 0,
38            active_skills: 0,
39            total_executions: 0,
40            skill_stats: HashMap::new(),
41            recent_skill_usage: VecDeque::with_capacity(100),
42        }
43    }
44
45    pub fn record_created(&mut self, skill_name: String, language: String) {
46        self.total_skills += 1;
47        self.active_skills += 1;
48
49        self.skill_stats.insert(
50            skill_name.clone(),
51            SkillStats {
52                name: skill_name,
53                language,
54                execution_count: 0,
55                success_count: 0,
56                total_duration_ms: 0,
57                created_at: Utc::now(),
58                last_used: Utc::now(),
59            },
60        );
61    }
62
63    pub fn record_deleted(&mut self, skill_name: String) {
64        // Use remove directly - returns Option<V>, avoids redundant contains_key check
65        if self.skill_stats.remove(&skill_name).is_some() {
66            self.active_skills = self.active_skills.saturating_sub(1);
67        }
68    }
69
70    pub fn record_execution(&mut self, skill_name: String, duration_ms: u64, success: bool) {
71        self.total_executions += 1;
72
73        if let Some(stats) = self.skill_stats.get_mut(&skill_name) {
74            stats.execution_count += 1;
75            if success {
76                stats.success_count += 1;
77            }
78            stats.total_duration_ms += duration_ms;
79            stats.last_used = Utc::now();
80        }
81
82        let record = SkillUsageRecord {
83            skill_name,
84            success,
85            duration_ms,
86            timestamp: Utc::now(),
87        };
88
89        if self.recent_skill_usage.len() >= 100 {
90            self.recent_skill_usage.pop_front();
91        }
92        self.recent_skill_usage.push_back(record);
93    }
94
95    pub fn reuse_ratio(&self) -> f64 {
96        if self.active_skills > 0 && self.total_executions > 0 {
97            self.total_executions as f64 / self.active_skills as f64
98        } else {
99            0.0
100        }
101    }
102
103    pub fn get_skill_success_rate(&self, skill_name: &str) -> f64 {
104        if let Some(stats) = self.skill_stats.get(skill_name) {
105            if stats.execution_count > 0 {
106                stats.success_count as f64 / stats.execution_count as f64
107            } else {
108                0.0
109            }
110        } else {
111            0.0
112        }
113    }
114
115    pub fn get_skill_avg_duration(&self, skill_name: &str) -> u64 {
116        if let Some(stats) = self.skill_stats.get(skill_name) {
117            if stats.execution_count > 0 {
118                stats.total_duration_ms / stats.execution_count
119            } else {
120                0
121            }
122        } else {
123            0
124        }
125    }
126
127    pub fn get_underutilized_skills(&self, threshold: f64) -> Vec<String> {
128        let avg_usage = if self.active_skills > 0 {
129            self.total_executions as f64 / self.active_skills as f64
130        } else {
131            0.0
132        };
133
134        self.skill_stats
135            .iter()
136            .filter(|(_, stats)| (stats.execution_count as f64) < (avg_usage * threshold))
137            .map(|(name, _)| name.clone())
138            .collect()
139    }
140}
141
142impl Default for SkillMetrics {
143    fn default() -> Self {
144        Self::new()
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_record_created() {
154        let mut metrics = SkillMetrics::new();
155        metrics.record_created("filter_test_files".to_owned(), "python3".to_owned());
156
157        assert_eq!(metrics.total_skills, 1);
158        assert_eq!(metrics.active_skills, 1);
159        assert!(metrics.skill_stats.contains_key("filter_test_files"));
160    }
161
162    #[test]
163    fn test_record_deleted() {
164        let mut metrics = SkillMetrics::new();
165        metrics.record_created("skill1".to_owned(), "python3".to_owned());
166        metrics.record_deleted("skill1".to_owned());
167
168        assert_eq!(metrics.active_skills, 0);
169        assert!(!metrics.skill_stats.contains_key("skill1"));
170    }
171
172    #[test]
173    fn test_record_execution() {
174        let mut metrics = SkillMetrics::new();
175        metrics.record_created("analyze".to_owned(), "python3".to_owned());
176        metrics.record_execution("analyze".to_owned(), 1000, true);
177        metrics.record_execution("analyze".to_owned(), 950, true);
178
179        assert_eq!(metrics.total_executions, 2);
180        let stats = metrics.skill_stats.get("analyze").unwrap();
181        assert_eq!(stats.execution_count, 2);
182        assert_eq!(stats.success_count, 2);
183        assert_eq!(stats.total_duration_ms, 1950);
184    }
185
186    #[test]
187    fn test_reuse_ratio() {
188        let mut metrics = SkillMetrics::new();
189        metrics.record_created("skill1".to_owned(), "python3".to_owned());
190        metrics.record_created("skill2".to_owned(), "javascript".to_owned());
191        metrics.record_execution("skill1".to_owned(), 100, true);
192        metrics.record_execution("skill1".to_owned(), 100, true);
193        metrics.record_execution("skill2".to_owned(), 200, true);
194
195        assert_eq!(metrics.reuse_ratio(), 3.0 / 2.0);
196    }
197
198    #[test]
199    fn test_success_rate() {
200        let mut metrics = SkillMetrics::new();
201        metrics.record_created("test".to_owned(), "python3".to_owned());
202        metrics.record_execution("test".to_owned(), 100, true);
203        metrics.record_execution("test".to_owned(), 100, true);
204        metrics.record_execution("test".to_owned(), 100, false);
205
206        let success_rate = metrics.get_skill_success_rate("test");
207        assert!((success_rate - 2.0 / 3.0).abs() < 0.01);
208    }
209
210    #[test]
211    fn test_avg_duration() {
212        let mut metrics = SkillMetrics::new();
213        metrics.record_created("perf".to_owned(), "javascript".to_owned());
214        metrics.record_execution("perf".to_owned(), 500, true);
215        metrics.record_execution("perf".to_owned(), 600, true);
216        metrics.record_execution("perf".to_owned(), 400, true);
217
218        let avg = metrics.get_skill_avg_duration("perf");
219        assert_eq!(avg, 500);
220    }
221
222    #[test]
223    fn test_underutilized_skills() {
224        let mut metrics = SkillMetrics::new();
225        metrics.record_created("popular".to_owned(), "python3".to_owned());
226        metrics.record_created("unpopular".to_owned(), "python3".to_owned());
227
228        for _ in 0..10 {
229            metrics.record_execution("popular".to_owned(), 100, true);
230        }
231        metrics.record_execution("unpopular".to_owned(), 100, true);
232
233        let underutilized = metrics.get_underutilized_skills(0.5);
234        assert!(underutilized.contains(&"unpopular".to_owned()));
235    }
236}