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 stats.total_duration_ms.checked_div(stats.execution_count).unwrap_or(0)
118 } else {
119 0
120 }
121 }
122
123 pub fn get_underutilized_skills(&self, threshold: f64) -> Vec<String> {
124 let avg_usage = if self.active_skills > 0 {
125 self.total_executions as f64 / self.active_skills as f64
126 } else {
127 0.0
128 };
129
130 self.skill_stats
131 .iter()
132 .filter(|(_, stats)| (stats.execution_count as f64) < (avg_usage * threshold))
133 .map(|(name, _)| name.clone())
134 .collect()
135 }
136}
137
138impl Default for SkillMetrics {
139 fn default() -> Self {
140 Self::new()
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn test_record_created() {
150 let mut metrics = SkillMetrics::new();
151 metrics.record_created("filter_test_files".to_owned(), "python3".to_owned());
152
153 assert_eq!(metrics.total_skills, 1);
154 assert_eq!(metrics.active_skills, 1);
155 assert!(metrics.skill_stats.contains_key("filter_test_files"));
156 }
157
158 #[test]
159 fn test_record_deleted() {
160 let mut metrics = SkillMetrics::new();
161 metrics.record_created("skill1".to_owned(), "python3".to_owned());
162 metrics.record_deleted("skill1".to_owned());
163
164 assert_eq!(metrics.active_skills, 0);
165 assert!(!metrics.skill_stats.contains_key("skill1"));
166 }
167
168 #[test]
169 fn test_record_execution() {
170 let mut metrics = SkillMetrics::new();
171 metrics.record_created("analyze".to_owned(), "python3".to_owned());
172 metrics.record_execution("analyze".to_owned(), 1000, true);
173 metrics.record_execution("analyze".to_owned(), 950, true);
174
175 assert_eq!(metrics.total_executions, 2);
176 let stats = metrics.skill_stats.get("analyze").unwrap();
177 assert_eq!(stats.execution_count, 2);
178 assert_eq!(stats.success_count, 2);
179 assert_eq!(stats.total_duration_ms, 1950);
180 }
181
182 #[test]
183 fn test_reuse_ratio() {
184 let mut metrics = SkillMetrics::new();
185 metrics.record_created("skill1".to_owned(), "python3".to_owned());
186 metrics.record_created("skill2".to_owned(), "javascript".to_owned());
187 metrics.record_execution("skill1".to_owned(), 100, true);
188 metrics.record_execution("skill1".to_owned(), 100, true);
189 metrics.record_execution("skill2".to_owned(), 200, true);
190
191 assert!((metrics.reuse_ratio() - 3.0 / 2.0).abs() < f64::EPSILON);
192 }
193
194 #[test]
195 fn test_success_rate() {
196 let mut metrics = SkillMetrics::new();
197 metrics.record_created("test".to_owned(), "python3".to_owned());
198 metrics.record_execution("test".to_owned(), 100, true);
199 metrics.record_execution("test".to_owned(), 100, true);
200 metrics.record_execution("test".to_owned(), 100, false);
201
202 let success_rate = metrics.get_skill_success_rate("test");
203 assert!((success_rate - 2.0 / 3.0).abs() < 0.01);
204 }
205
206 #[test]
207 fn test_avg_duration() {
208 let mut metrics = SkillMetrics::new();
209 metrics.record_created("perf".to_owned(), "javascript".to_owned());
210 metrics.record_execution("perf".to_owned(), 500, true);
211 metrics.record_execution("perf".to_owned(), 600, true);
212 metrics.record_execution("perf".to_owned(), 400, true);
213
214 let avg = metrics.get_skill_avg_duration("perf");
215 assert_eq!(avg, 500);
216 }
217
218 #[test]
219 fn test_underutilized_skills() {
220 let mut metrics = SkillMetrics::new();
221 metrics.record_created("popular".to_owned(), "python3".to_owned());
222 metrics.record_created("unpopular".to_owned(), "python3".to_owned());
223
224 for _ in 0..10 {
225 metrics.record_execution("popular".to_owned(), 100, true);
226 }
227 metrics.record_execution("unpopular".to_owned(), 100, true);
228
229 let underutilized = metrics.get_underutilized_skills(0.5);
230 assert!(underutilized.contains(&"unpopular".to_owned()));
231 }
232}