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 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}