1use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
11use std::sync::{Arc, Mutex};
12
13#[derive(Debug)]
27pub struct LatencyHistogram {
28 buckets: [AtomicU64; 7],
31 total_count: AtomicU64,
32 total_sum_ms: AtomicU64,
33}
34
35impl Default for LatencyHistogram {
36 fn default() -> Self {
37 Self {
38 buckets: [
39 AtomicU64::new(0),
40 AtomicU64::new(0),
41 AtomicU64::new(0),
42 AtomicU64::new(0),
43 AtomicU64::new(0),
44 AtomicU64::new(0),
45 AtomicU64::new(0),
46 ],
47 total_count: AtomicU64::new(0),
48 total_sum_ms: AtomicU64::new(0),
49 }
50 }
51}
52
53impl LatencyHistogram {
54 const BOUNDS: [u64; 7] = [1, 5, 10, 50, 100, 500, u64::MAX];
69
70 pub fn record(&self, ms: u64) {
72 self.total_count.fetch_add(1, Ordering::Relaxed);
73 self.total_sum_ms.fetch_add(ms, Ordering::Relaxed);
74 for (i, &bound) in Self::BOUNDS.iter().enumerate() {
75 if ms <= bound {
76 self.buckets[i].fetch_add(1, Ordering::Relaxed);
77 return;
78 }
79 }
80 }
81
82 pub fn mean_ms(&self) -> f64 {
84 let count = self.total_count.load(Ordering::Relaxed);
85 if count == 0 {
86 return 0.0;
87 }
88 self.total_sum_ms.load(Ordering::Relaxed) as f64 / count as f64
89 }
90
91 pub fn std_dev_ms(&self) -> f64 {
101 let count = self.total_count.load(Ordering::Relaxed);
102 if count < 2 {
103 return 0.0;
104 }
105 const MIDS: [f64; 7] = [0.5, 3.0, 7.5, 30.0, 75.0, 300.0, 500.0];
106 let (sum, sum_sq): (f64, f64) = self
107 .buckets
108 .iter()
109 .zip(MIDS.iter())
110 .map(|(b, &m)| {
111 let c = b.load(Ordering::Relaxed) as f64;
112 (c * m, c * m * m)
113 })
114 .fold((0.0, 0.0), |(s, ss), (v, v2)| (s + v, ss + v2));
115 let n = count as f64;
116 let variance = sum_sq / n - (sum / n) * (sum / n);
117 variance.max(0.0).sqrt()
118 }
119
120 pub fn count(&self) -> u64 {
122 self.total_count.load(Ordering::Relaxed)
123 }
124
125 pub fn has_data(&self) -> bool {
127 self.count() > 0
128 }
129
130 pub fn is_empty(&self) -> bool {
132 self.count() == 0
133 }
134
135 pub fn percentile(&self, p: f64) -> u64 {
146 let total = self.total_count.load(Ordering::Relaxed);
147 if total == 0 {
148 return 0;
149 }
150 let target = (p.clamp(0.0, 1.0) * total as f64).ceil() as u64;
151 let mut cumulative = 0u64;
152 for (i, bucket) in self.buckets.iter().enumerate() {
153 cumulative += bucket.load(Ordering::Relaxed);
154 if cumulative >= target {
155 return Self::BOUNDS[i];
156 }
157 }
158 *Self::BOUNDS.last().unwrap_or(&u64::MAX)
160 }
161
162 pub fn mode_bucket_ms(&self) -> Option<u64> {
167 if self.count() == 0 {
168 return None;
169 }
170 let (idx, _) = self
171 .buckets
172 .iter()
173 .enumerate()
174 .max_by_key(|(_, a)| a.load(Ordering::Relaxed))?;
175 Some(Self::BOUNDS[idx])
176 }
177
178 pub fn buckets(&self) -> Vec<(u64, u64)> {
180 Self::BOUNDS
181 .iter()
182 .zip(self.buckets.iter())
183 .map(|(&b, a)| (b, a.load(Ordering::Relaxed)))
184 .collect()
185 }
186
187 pub fn min_ms(&self) -> Option<u64> {
189 let total = self.total_count.load(Ordering::Relaxed);
190 if total == 0 {
191 return None;
192 }
193 for (i, bucket) in self.buckets.iter().enumerate() {
197 if bucket.load(Ordering::Relaxed) > 0 {
198 return Some(if i == 0 { 0 } else { Self::BOUNDS[i - 1] + 1 });
199 }
200 }
201 None
202 }
203
204 pub fn max_ms(&self) -> Option<u64> {
206 let total = self.total_count.load(Ordering::Relaxed);
207 if total == 0 {
208 return None;
209 }
210 for (i, bucket) in self.buckets.iter().enumerate().rev() {
212 if bucket.load(Ordering::Relaxed) > 0 {
213 return Some(Self::BOUNDS[i]);
214 }
215 }
216 None
217 }
218
219 pub fn range_ms(&self) -> Option<u64> {
224 Some(self.max_ms()?.saturating_sub(self.min_ms()?))
225 }
226
227 pub fn interquartile_range_ms(&self) -> u64 {
235 self.p75().saturating_sub(self.p25())
236 }
237
238 pub fn p50(&self) -> u64 {
240 self.percentile(0.50)
241 }
242
243 pub fn p95(&self) -> u64 {
245 self.percentile(0.95)
246 }
247
248 pub fn p99(&self) -> u64 {
250 self.percentile(0.99)
251 }
252
253 pub fn p25(&self) -> u64 {
255 self.percentile(0.25)
256 }
257
258 pub fn p75(&self) -> u64 {
260 self.percentile(0.75)
261 }
262
263 pub fn p90(&self) -> u64 {
265 self.percentile(0.90)
266 }
267
268 pub fn p10(&self) -> u64 {
272 self.percentile(0.10)
273 }
274
275 pub fn median_ms(&self) -> u64 {
280 self.p50()
281 }
282
283 pub fn reset(&self) {
285 self.total_count.store(0, Ordering::Relaxed);
286 self.total_sum_ms.store(0, Ordering::Relaxed);
287 for bucket in &self.buckets {
288 bucket.store(0, Ordering::Relaxed);
289 }
290 }
291
292 pub fn sum_ms(&self) -> u64 {
296 self.total_sum_ms.load(Ordering::Relaxed)
297 }
298
299 pub fn coefficient_of_variation(&self) -> f64 {
305 let mean = self.mean_ms();
306 if mean == 0.0 {
307 return 0.0;
308 }
309 self.std_dev_ms() / mean
310 }
311
312 pub fn sample_count(&self) -> u64 {
314 self.total_count.load(std::sync::atomic::Ordering::Relaxed)
315 }
316
317 pub fn percentile_spread(&self) -> u64 {
323 self.p99().saturating_sub(self.p50())
324 }
325
326 pub fn bucket_counts(&self) -> [u64; 7] {
329 let mut out = [0u64; 7];
330 for (i, b) in self.buckets.iter().enumerate() {
331 out[i] = b.load(std::sync::atomic::Ordering::Relaxed);
332 }
333 out
334 }
335
336 pub fn min_occupied_ms(&self) -> Option<u64> {
339 Self::BOUNDS
340 .iter()
341 .zip(self.buckets.iter())
342 .find(|(_, b)| b.load(std::sync::atomic::Ordering::Relaxed) > 0)
343 .map(|(&bound, _)| bound)
344 }
345
346 pub fn max_occupied_ms(&self) -> Option<u64> {
350 Self::BOUNDS
351 .iter()
352 .zip(self.buckets.iter())
353 .rev()
354 .find(|(_, b)| b.load(std::sync::atomic::Ordering::Relaxed) > 0)
355 .map(|(&bound, _)| bound)
356 }
357
358 pub fn occupied_bucket_count(&self) -> usize {
360 self.buckets
361 .iter()
362 .filter(|b| b.load(std::sync::atomic::Ordering::Relaxed) > 0)
363 .count()
364 }
365
366 pub fn is_skewed(&self) -> bool {
370 let p50 = self.p50();
371 if p50 == 0 {
372 return false;
373 }
374 self.p99() > 2 * p50
375 }
376
377 pub fn is_uniform(&self) -> bool {
381 let non_empty = self
382 .buckets
383 .iter()
384 .filter(|b| b.load(std::sync::atomic::Ordering::Relaxed) > 0)
385 .count();
386 non_empty <= 1
387 }
388
389 pub fn clear(&self) {
395 self.reset();
396 }
397
398 pub fn is_above_p99(&self, latency_ms: u64) -> bool {
403 latency_ms > self.p99()
404 }
405
406 pub fn is_below_p99(&self, threshold_ms: u64) -> bool {
411 self.p99() < threshold_ms
412 }
413
414}
415
416impl MetricsSnapshot {
417 pub fn delta(after: &Self, before: &Self) -> Self {
431 Self {
432 active_sessions: after.active_sessions.saturating_sub(before.active_sessions),
433 total_sessions: after.total_sessions.saturating_sub(before.total_sessions),
434 total_steps: after.total_steps.saturating_sub(before.total_steps),
435 total_tool_calls: after.total_tool_calls.saturating_sub(before.total_tool_calls),
436 failed_tool_calls: after.failed_tool_calls.saturating_sub(before.failed_tool_calls),
437 backpressure_shed_count: after
438 .backpressure_shed_count
439 .saturating_sub(before.backpressure_shed_count),
440 memory_recall_count: after
441 .memory_recall_count
442 .saturating_sub(before.memory_recall_count),
443 checkpoint_errors: after
444 .checkpoint_errors
445 .saturating_sub(before.checkpoint_errors),
446 per_tool_calls: {
447 let mut m = after.per_tool_calls.clone();
448 for (k, v) in &before.per_tool_calls {
449 let entry = m.entry(k.clone()).or_default();
450 *entry = entry.saturating_sub(*v);
451 }
452 m
453 },
454 per_tool_failures: {
455 let mut m = after.per_tool_failures.clone();
456 for (k, v) in &before.per_tool_failures {
457 let entry = m.entry(k.clone()).or_default();
458 *entry = entry.saturating_sub(*v);
459 }
460 m
461 },
462 step_latency_buckets: after
463 .step_latency_buckets
464 .iter()
465 .zip(before.step_latency_buckets.iter())
466 .map(|((bound, a), (_, b))| (*bound, a.saturating_sub(*b)))
467 .collect(),
468 step_latency_mean_ms: after.step_latency_mean_ms - before.step_latency_mean_ms,
469 per_agent_tool_calls: after.per_agent_tool_calls.clone(),
470 per_agent_tool_failures: after.per_agent_tool_failures.clone(),
471 }
472 }
473
474 pub fn to_json(&self) -> serde_json::Value {
476 serde_json::json!({
477 "active_sessions": self.active_sessions,
478 "total_sessions": self.total_sessions,
479 "total_steps": self.total_steps,
480 "total_tool_calls": self.total_tool_calls,
481 "failed_tool_calls": self.failed_tool_calls,
482 "backpressure_shed_count": self.backpressure_shed_count,
483 "memory_recall_count": self.memory_recall_count,
484 "step_latency_mean_ms": self.step_latency_mean_ms,
485 "per_tool_calls": self.per_tool_calls,
486 "per_tool_failures": self.per_tool_failures,
487 })
488 }
489
490 pub fn tool_call_count(&self, name: &str) -> u64 {
494 self.per_tool_calls.get(name).copied().unwrap_or(0)
495 }
496
497 pub fn tool_failure_count(&self, name: &str) -> u64 {
501 self.per_tool_failures.get(name).copied().unwrap_or(0)
502 }
503
504 pub fn tool_names(&self) -> Vec<&str> {
506 let mut names: Vec<&str> = self.per_tool_calls.keys().map(|s| s.as_str()).collect();
507 names.sort_unstable();
508 names
509 }
510
511 pub fn failure_rate(&self) -> f64 {
515 if self.total_tool_calls == 0 {
516 return 0.0;
517 }
518 self.failed_tool_calls as f64 / self.total_tool_calls as f64
519 }
520
521 pub fn success_rate(&self) -> f64 {
525 1.0 - self.failure_rate()
526 }
527
528 pub fn tool_success_count(&self, name: &str) -> u64 {
532 self.tool_call_count(name)
533 .saturating_sub(self.tool_failure_count(name))
534 }
535
536 pub fn tool_failure_rate(&self, name: &str) -> f64 {
540 let calls = self.tool_call_count(name);
541 if calls == 0 {
542 return 0.0;
543 }
544 self.tool_failure_count(name) as f64 / calls as f64
545 }
546
547 pub fn total_successful_tool_calls(&self) -> u64 {
552 self.total_tool_calls.saturating_sub(self.failed_tool_calls)
553 }
554
555 pub fn is_zero(&self) -> bool {
557 self.active_sessions == 0
558 && self.total_sessions == 0
559 && self.total_steps == 0
560 && self.total_tool_calls == 0
561 && self.failed_tool_calls == 0
562 && self.backpressure_shed_count == 0
563 && self.memory_recall_count == 0
564 && self.checkpoint_errors == 0
565 }
566
567 pub fn avg_steps_per_session(&self) -> f64 {
572 if self.total_sessions == 0 {
573 0.0
574 } else {
575 self.total_steps as f64 / self.total_sessions as f64
576 }
577 }
578
579 pub fn error_rate(&self) -> f64 {
583 if self.total_tool_calls == 0 {
584 return 0.0;
585 }
586 self.failed_tool_calls as f64 / self.total_tool_calls as f64
587 }
588
589 pub fn memory_recall_rate(&self) -> f64 {
593 if self.total_sessions == 0 {
594 return 0.0;
595 }
596 self.memory_recall_count as f64 / self.total_sessions as f64
597 }
598
599 pub fn steps_per_session(&self) -> f64 {
604 if self.total_sessions == 0 {
605 return 0.0;
606 }
607 self.total_steps as f64 / self.total_sessions as f64
608 }
609
610 pub fn has_errors(&self) -> bool {
616 self.failed_tool_calls > 0 || self.checkpoint_errors > 0
617 }
618
619 pub fn is_healthy(&self) -> bool {
625 self.failed_tool_calls == 0
626 && self.backpressure_shed_count == 0
627 && self.checkpoint_errors == 0
628 }
629
630 pub fn tool_call_rate(&self) -> f64 {
634 if self.total_sessions == 0 {
635 return 0.0;
636 }
637 self.total_tool_calls as f64 / self.total_sessions as f64
638 }
639
640 pub fn backpressure_rate(&self) -> f64 {
644 if self.total_sessions == 0 {
645 return 0.0;
646 }
647 self.backpressure_shed_count as f64 / self.total_sessions as f64
648 }
649
650 pub fn memory_efficiency(&self) -> f64 {
654 if self.total_steps == 0 {
655 return 0.0;
656 }
657 self.memory_recall_count as f64 / self.total_steps as f64
658 }
659
660 pub fn active_session_ratio(&self) -> f64 {
664 if self.total_sessions == 0 {
665 return 0.0;
666 }
667 self.active_sessions as f64 / self.total_sessions as f64
668 }
669
670 pub fn step_to_tool_ratio(&self) -> f64 {
674 if self.total_steps == 0 {
675 return 0.0;
676 }
677 self.total_tool_calls as f64 / self.total_steps as f64
678 }
679
680 pub fn has_failures(&self) -> bool {
682 self.failed_tool_calls > 0
683 }
684
685 pub fn tool_diversity(&self) -> usize {
687 self.per_tool_calls.len()
688 }
689
690 pub fn avg_failures_per_session(&self) -> f64 {
694 if self.total_sessions == 0 {
695 return 0.0;
696 }
697 self.failed_tool_calls as f64 / self.total_sessions as f64
698 }
699
700 pub fn most_called_tool(&self) -> Option<String> {
704 self.per_tool_calls
705 .iter()
706 .max_by_key(|(_, &v)| v)
707 .map(|(k, _)| k.clone())
708 }
709
710 pub fn tool_names_with_failures(&self) -> Vec<String> {
712 let mut names: Vec<String> = self
713 .per_tool_failures
714 .iter()
715 .filter(|(_, &v)| v > 0)
716 .map(|(k, _)| k.clone())
717 .collect();
718 names.sort_unstable();
719 names
720 }
721
722 pub fn failed_tool_ratio_for(&self, name: &str) -> f64 {
726 let calls = self.tool_call_count(name);
727 if calls == 0 {
728 return 0.0;
729 }
730 self.tool_failure_count(name) as f64 / calls as f64
731 }
732
733 pub fn backpressure_shed_rate(&self) -> f64 {
737 if self.total_tool_calls == 0 {
738 return 0.0;
739 }
740 self.backpressure_shed_count as f64 / self.total_tool_calls as f64
741 }
742
743 pub fn total_agent_count(&self) -> usize {
745 self.per_agent_tool_calls.len()
746 }
747
748 pub fn steps_per_tool_call(&self) -> f64 {
752 if self.total_tool_calls == 0 {
753 return 0.0;
754 }
755 self.total_steps as f64 / self.total_tool_calls as f64
756 }
757
758 pub fn agent_with_most_calls(&self) -> Option<String> {
762 self.per_agent_tool_calls
763 .iter()
764 .map(|(agent, tools)| (agent, tools.values().sum::<u64>()))
765 .max_by_key(|(_, total)| *total)
766 .map(|(agent, _)| agent.clone())
767 }
768}
769
770impl std::fmt::Display for MetricsSnapshot {
771 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
772 write!(
773 f,
774 "MetricsSnapshot {{ sessions: active={} total={}, steps={}, \
775 tool_calls={} (failed={}), backpressure_shed={}, \
776 memory_recalls={}, checkpoint_errors={}, latency_mean={:.1}ms }}",
777 self.active_sessions,
778 self.total_sessions,
779 self.total_steps,
780 self.total_tool_calls,
781 self.failed_tool_calls,
782 self.backpressure_shed_count,
783 self.memory_recall_count,
784 self.checkpoint_errors,
785 self.step_latency_mean_ms,
786 )
787 }
788}
789
790#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
810pub struct MetricsSnapshot {
811 pub active_sessions: usize,
813 pub total_sessions: u64,
815 pub total_steps: u64,
817 pub total_tool_calls: u64,
819 pub failed_tool_calls: u64,
821 pub backpressure_shed_count: u64,
823 pub memory_recall_count: u64,
825 pub checkpoint_errors: u64,
827 pub per_tool_calls: HashMap<String, u64>,
829 pub per_tool_failures: HashMap<String, u64>,
831 pub step_latency_buckets: Vec<(u64, u64)>,
833 pub step_latency_mean_ms: f64,
835 pub per_agent_tool_calls: HashMap<String, HashMap<String, u64>>,
837 pub per_agent_tool_failures: HashMap<String, HashMap<String, u64>>,
839}
840
841#[derive(Debug, Default)]
847struct PerToolMaps {
848 calls: HashMap<String, u64>,
850 failures: HashMap<String, u64>,
852 agent_calls: HashMap<String, HashMap<String, u64>>,
854 agent_failures: HashMap<String, HashMap<String, u64>>,
856}
857
858#[derive(Debug)]
860pub struct RuntimeMetrics {
861 pub active_sessions: AtomicUsize,
863 pub total_sessions: AtomicU64,
865 pub total_steps: AtomicU64,
867 pub total_tool_calls: AtomicU64,
869 pub failed_tool_calls: AtomicU64,
871 pub backpressure_shed_count: AtomicU64,
873 pub memory_recall_count: AtomicU64,
875 pub checkpoint_errors: AtomicU64,
877 per_tool: Mutex<PerToolMaps>,
879 pub step_latency: LatencyHistogram,
881}
882
883impl Default for RuntimeMetrics {
884 fn default() -> Self {
885 Self {
886 active_sessions: AtomicUsize::new(0),
887 total_sessions: AtomicU64::new(0),
888 total_steps: AtomicU64::new(0),
889 total_tool_calls: AtomicU64::new(0),
890 failed_tool_calls: AtomicU64::new(0),
891 backpressure_shed_count: AtomicU64::new(0),
892 memory_recall_count: AtomicU64::new(0),
893 checkpoint_errors: AtomicU64::new(0),
894 per_tool: Mutex::new(PerToolMaps::default()),
895 step_latency: LatencyHistogram::default(),
896 }
897 }
898}
899
900impl RuntimeMetrics {
901 pub fn new() -> Arc<Self> {
903 Arc::new(Self::default())
904 }
905
906 pub fn active_sessions(&self) -> usize {
908 self.active_sessions.load(Ordering::Relaxed)
909 }
910
911 pub fn total_sessions(&self) -> u64 {
913 self.total_sessions.load(Ordering::Relaxed)
914 }
915
916 pub fn avg_tool_calls_per_session(&self) -> f64 {
920 let sessions = self.total_sessions();
921 if sessions == 0 {
922 return 0.0;
923 }
924 self.total_tool_calls() as f64 / sessions as f64
925 }
926
927 pub fn total_steps(&self) -> u64 {
929 self.total_steps.load(Ordering::Relaxed)
930 }
931
932 pub fn avg_steps_per_session(&self) -> f64 {
936 let sessions = self.total_sessions();
937 if sessions == 0 {
938 return 0.0;
939 }
940 self.total_steps() as f64 / sessions as f64
941 }
942
943 pub fn total_tool_calls(&self) -> u64 {
945 self.total_tool_calls.load(Ordering::Relaxed)
946 }
947
948 pub fn failed_tool_calls(&self) -> u64 {
950 self.failed_tool_calls.load(Ordering::Relaxed)
951 }
952
953 pub fn tool_success_rate(&self) -> f64 {
958 let total = self.total_tool_calls();
959 if total == 0 {
960 return 1.0;
961 }
962 let failed = self.failed_tool_calls();
963 1.0 - (failed as f64 / total as f64)
964 }
965
966 pub fn backpressure_shed_count(&self) -> u64 {
968 self.backpressure_shed_count.load(Ordering::Relaxed)
969 }
970
971 pub fn memory_recall_count(&self) -> u64 {
973 self.memory_recall_count.load(Ordering::Relaxed)
974 }
975
976 pub fn checkpoint_errors(&self) -> u64 {
978 self.checkpoint_errors.load(Ordering::Relaxed)
979 }
980
981 pub fn checkpoint_error_rate(&self) -> f64 {
985 let sessions = self.total_sessions();
986 if sessions == 0 {
987 return 0.0;
988 }
989 self.checkpoint_errors() as f64 / sessions as f64
990 }
991
992 pub fn p50_latency_ms(&self) -> u64 {
997 self.step_latency.p50()
998 }
999
1000 pub fn record_tool_call(&self, tool_name: &str) {
1004 self.total_tool_calls.fetch_add(1, Ordering::Relaxed);
1005 if let Ok(mut maps) = self.per_tool.lock() {
1006 *maps.calls.entry(tool_name.to_owned()).or_insert(0) += 1;
1007 }
1008 }
1009
1010 pub fn record_tool_failure(&self, tool_name: &str) {
1014 self.failed_tool_calls.fetch_add(1, Ordering::Relaxed);
1015 if let Ok(mut maps) = self.per_tool.lock() {
1016 *maps.failures.entry(tool_name.to_owned()).or_insert(0) += 1;
1017 }
1018 }
1019
1020 pub fn per_tool_calls_snapshot(&self) -> HashMap<String, u64> {
1022 self.per_tool
1023 .lock()
1024 .map(|m| m.calls.clone())
1025 .unwrap_or_default()
1026 }
1027
1028 pub fn per_tool_failures_snapshot(&self) -> HashMap<String, u64> {
1030 self.per_tool
1031 .lock()
1032 .map(|m| m.failures.clone())
1033 .unwrap_or_default()
1034 }
1035
1036 pub fn record_agent_tool_call(&self, agent_id: &str, tool_name: &str) {
1038 if let Ok(mut maps) = self.per_tool.lock() {
1039 *maps
1040 .agent_calls
1041 .entry(agent_id.to_owned())
1042 .or_default()
1043 .entry(tool_name.to_owned())
1044 .or_insert(0) += 1;
1045 }
1046 }
1047
1048 pub fn record_agent_tool_failure(&self, agent_id: &str, tool_name: &str) {
1050 if let Ok(mut maps) = self.per_tool.lock() {
1051 *maps
1052 .agent_failures
1053 .entry(agent_id.to_owned())
1054 .or_default()
1055 .entry(tool_name.to_owned())
1056 .or_insert(0) += 1;
1057 }
1058 }
1059
1060 pub fn per_agent_tool_calls_snapshot(&self) -> HashMap<String, HashMap<String, u64>> {
1062 self.per_tool
1063 .lock()
1064 .map(|m| m.agent_calls.clone())
1065 .unwrap_or_default()
1066 }
1067
1068 pub fn per_agent_tool_failures_snapshot(&self) -> HashMap<String, HashMap<String, u64>> {
1070 self.per_tool
1071 .lock()
1072 .map(|m| m.agent_failures.clone())
1073 .unwrap_or_default()
1074 }
1075
1076 pub fn snapshot(&self) -> MetricsSnapshot {
1083 let (per_tool_calls, per_tool_failures, per_agent_tool_calls, per_agent_tool_failures) =
1085 self.per_tool
1086 .lock()
1087 .map(|m| {
1088 (
1089 m.calls.clone(),
1090 m.failures.clone(),
1091 m.agent_calls.clone(),
1092 m.agent_failures.clone(),
1093 )
1094 })
1095 .unwrap_or_default();
1096
1097 MetricsSnapshot {
1098 active_sessions: self.active_sessions.load(Ordering::Relaxed),
1099 total_sessions: self.total_sessions.load(Ordering::Relaxed),
1100 total_steps: self.total_steps.load(Ordering::Relaxed),
1101 total_tool_calls: self.total_tool_calls.load(Ordering::Relaxed),
1102 failed_tool_calls: self.failed_tool_calls.load(Ordering::Relaxed),
1103 backpressure_shed_count: self.backpressure_shed_count.load(Ordering::Relaxed),
1104 memory_recall_count: self.memory_recall_count.load(Ordering::Relaxed),
1105 checkpoint_errors: self.checkpoint_errors.load(Ordering::Relaxed),
1106 per_tool_calls,
1107 per_tool_failures,
1108 step_latency_buckets: self.step_latency.buckets(),
1109 step_latency_mean_ms: self.step_latency.mean_ms(),
1110 per_agent_tool_calls,
1111 per_agent_tool_failures,
1112 }
1113 }
1114
1115 pub fn record_step_latency(&self, ms: u64) {
1117 self.step_latency.record(ms);
1118 }
1119
1120 pub fn reset(&self) {
1124 self.active_sessions.store(0, Ordering::Relaxed);
1125 self.total_sessions.store(0, Ordering::Relaxed);
1126 self.total_steps.store(0, Ordering::Relaxed);
1127 self.total_tool_calls.store(0, Ordering::Relaxed);
1128 self.failed_tool_calls.store(0, Ordering::Relaxed);
1129 self.backpressure_shed_count.store(0, Ordering::Relaxed);
1130 self.memory_recall_count.store(0, Ordering::Relaxed);
1131 self.checkpoint_errors.store(0, Ordering::Relaxed);
1132 if let Ok(mut maps) = self.per_tool.lock() {
1133 maps.calls.clear();
1134 maps.failures.clear();
1135 maps.agent_calls.clear();
1136 maps.agent_failures.clear();
1137 }
1138 self.step_latency.reset();
1139 }
1140
1141 pub fn failure_rate(&self) -> f64 {
1145 let total = self.total_tool_calls.load(Ordering::Relaxed);
1146 if total == 0 {
1147 return 0.0;
1148 }
1149 let failed = self.failed_tool_calls.load(Ordering::Relaxed);
1150 failed as f64 / total as f64
1151 }
1152
1153 pub fn success_rate(&self) -> f64 {
1157 1.0 - self.failure_rate()
1158 }
1159
1160 pub fn is_active(&self) -> bool {
1162 self.active_sessions.load(Ordering::Relaxed) > 0
1163 }
1164
1165 pub fn step_latency_p50(&self) -> u64 {
1170 self.step_latency.p50()
1171 }
1172
1173 pub fn step_latency_p99(&self) -> u64 {
1178 self.step_latency.p99()
1179 }
1180
1181 pub fn top_tools_by_calls(&self, n: usize) -> Vec<(String, u64)> {
1185 let snap = self.per_tool_calls_snapshot();
1186 let mut pairs: Vec<(String, u64)> = snap.into_iter().collect();
1187 pairs.sort_unstable_by(|a, b| b.1.cmp(&a.1));
1188 pairs.truncate(n);
1189 pairs
1190 }
1191
1192 pub fn top_tools_by_failures(&self, n: usize) -> Vec<(String, u64)> {
1199 let snap = self.per_tool_failures_snapshot();
1200 let mut pairs: Vec<(String, u64)> = snap.into_iter().collect();
1201 pairs.sort_unstable_by(|a, b| b.1.cmp(&a.1));
1202 pairs.truncate(n);
1203 pairs
1204 }
1205
1206 pub fn total_step_latency_ms(&self) -> u64 {
1208 self.step_latency.sum_ms()
1209 }
1210
1211 #[deprecated(since = "1.0.3", note = "use `snapshot()` which returns the named MetricsSnapshot struct")]
1229 pub fn to_snapshot(&self) -> (usize, u64, u64, u64, u64, u64, u64) {
1230 (
1231 self.active_sessions.load(Ordering::Relaxed),
1232 self.total_sessions.load(Ordering::Relaxed),
1233 self.total_steps.load(Ordering::Relaxed),
1234 self.total_tool_calls.load(Ordering::Relaxed),
1235 self.failed_tool_calls.load(Ordering::Relaxed),
1236 self.backpressure_shed_count.load(Ordering::Relaxed),
1237 self.memory_recall_count.load(Ordering::Relaxed),
1238 )
1239 }
1240}
1241
1242#[cfg(test)]
1245mod tests {
1246 use super::*;
1247
1248 #[test]
1249 fn test_metrics_new_returns_arc_with_zero_counters() {
1250 let m = RuntimeMetrics::new();
1251 assert_eq!(m.active_sessions(), 0);
1252 assert_eq!(m.total_sessions(), 0);
1253 assert_eq!(m.total_steps(), 0);
1254 assert_eq!(m.total_tool_calls(), 0);
1255 assert_eq!(m.failed_tool_calls(), 0);
1256 assert_eq!(m.backpressure_shed_count(), 0);
1257 assert_eq!(m.memory_recall_count(), 0);
1258 }
1259
1260 #[test]
1261 fn test_active_sessions_increments_and_decrements() {
1262 let m = RuntimeMetrics::new();
1263 m.active_sessions.fetch_add(1, Ordering::Relaxed);
1264 assert_eq!(m.active_sessions(), 1);
1265 m.active_sessions.fetch_sub(1, Ordering::Relaxed);
1266 assert_eq!(m.active_sessions(), 0);
1267 }
1268
1269 #[test]
1270 fn test_total_sessions_increments() {
1271 let m = RuntimeMetrics::new();
1272 m.total_sessions.fetch_add(1, Ordering::Relaxed);
1273 m.total_sessions.fetch_add(1, Ordering::Relaxed);
1274 assert_eq!(m.total_sessions(), 2);
1275 }
1276
1277 #[test]
1278 fn test_total_steps_increments() {
1279 let m = RuntimeMetrics::new();
1280 m.total_steps.fetch_add(5, Ordering::Relaxed);
1281 assert_eq!(m.total_steps(), 5);
1282 }
1283
1284 #[test]
1285 fn test_total_tool_calls_increments() {
1286 let m = RuntimeMetrics::new();
1287 m.total_tool_calls.fetch_add(3, Ordering::Relaxed);
1288 assert_eq!(m.total_tool_calls(), 3);
1289 }
1290
1291 #[test]
1292 fn test_failed_tool_calls_increments() {
1293 let m = RuntimeMetrics::new();
1294 m.failed_tool_calls.fetch_add(2, Ordering::Relaxed);
1295 assert_eq!(m.failed_tool_calls(), 2);
1296 }
1297
1298 #[test]
1299 fn test_backpressure_shed_count_increments() {
1300 let m = RuntimeMetrics::new();
1301 m.backpressure_shed_count.fetch_add(7, Ordering::Relaxed);
1302 assert_eq!(m.backpressure_shed_count(), 7);
1303 }
1304
1305 #[test]
1306 fn test_memory_recall_count_increments() {
1307 let m = RuntimeMetrics::new();
1308 m.memory_recall_count.fetch_add(4, Ordering::Relaxed);
1309 assert_eq!(m.memory_recall_count(), 4);
1310 }
1311
1312 #[test]
1313 fn test_reset_zeroes_all_counters() {
1314 let m = RuntimeMetrics::new();
1315 m.active_sessions.store(3, Ordering::Relaxed);
1316 m.total_sessions.store(10, Ordering::Relaxed);
1317 m.total_steps.store(50, Ordering::Relaxed);
1318 m.total_tool_calls.store(20, Ordering::Relaxed);
1319 m.failed_tool_calls.store(2, Ordering::Relaxed);
1320 m.backpressure_shed_count.store(1, Ordering::Relaxed);
1321 m.memory_recall_count.store(8, Ordering::Relaxed);
1322
1323 m.reset();
1324
1325 assert_eq!(m.active_sessions(), 0);
1326 assert_eq!(m.total_sessions(), 0);
1327 assert_eq!(m.total_steps(), 0);
1328 assert_eq!(m.total_tool_calls(), 0);
1329 assert_eq!(m.failed_tool_calls(), 0);
1330 assert_eq!(m.backpressure_shed_count(), 0);
1331 assert_eq!(m.memory_recall_count(), 0);
1332 }
1333
1334 #[test]
1335 fn test_to_snapshot_captures_correct_values() {
1336 let m = RuntimeMetrics::new();
1337 m.active_sessions.store(1, Ordering::Relaxed);
1338 m.total_sessions.store(2, Ordering::Relaxed);
1339 m.total_steps.store(3, Ordering::Relaxed);
1340 m.total_tool_calls.store(4, Ordering::Relaxed);
1341 m.failed_tool_calls.store(5, Ordering::Relaxed);
1342 m.backpressure_shed_count.store(6, Ordering::Relaxed);
1343 m.memory_recall_count.store(7, Ordering::Relaxed);
1344
1345 let snap = m.to_snapshot();
1346 assert_eq!(snap, (1, 2, 3, 4, 5, 6, 7));
1347 }
1348
1349 #[test]
1350 fn test_metrics_is_send_sync() {
1351 fn assert_send_sync<T: Send + Sync>() {}
1352 assert_send_sync::<RuntimeMetrics>();
1353 }
1354
1355 #[test]
1356 fn test_multiple_increments_are_cumulative() {
1357 let m = RuntimeMetrics::new();
1358 for _ in 0..100 {
1359 m.total_sessions.fetch_add(1, Ordering::Relaxed);
1360 }
1361 assert_eq!(m.total_sessions(), 100);
1362 }
1363
1364 #[test]
1365 fn test_arc_clone_shares_state() {
1366 let m = RuntimeMetrics::new();
1367 let m2 = Arc::clone(&m);
1368 m.total_sessions.fetch_add(1, Ordering::Relaxed);
1369 assert_eq!(m2.total_sessions(), 1);
1370 }
1371
1372 #[test]
1375 fn test_record_tool_call_increments_global_and_per_tool() {
1376 let m = RuntimeMetrics::new();
1377 m.record_tool_call("search");
1378 m.record_tool_call("search");
1379 m.record_tool_call("lookup");
1380 assert_eq!(m.total_tool_calls(), 3);
1381 let snap = m.per_tool_calls_snapshot();
1382 assert_eq!(snap.get("search").copied(), Some(2));
1383 assert_eq!(snap.get("lookup").copied(), Some(1));
1384 }
1385
1386 #[test]
1387 fn test_record_tool_failure_increments_global_and_per_tool() {
1388 let m = RuntimeMetrics::new();
1389 m.record_tool_failure("search");
1390 m.record_tool_failure("lookup");
1391 m.record_tool_failure("search");
1392 assert_eq!(m.failed_tool_calls(), 3);
1393 let snap = m.per_tool_failures_snapshot();
1394 assert_eq!(snap.get("search").copied(), Some(2));
1395 assert_eq!(snap.get("lookup").copied(), Some(1));
1396 }
1397
1398 #[test]
1399 fn test_reset_clears_per_tool_counters() {
1400 let m = RuntimeMetrics::new();
1401 m.record_tool_call("foo");
1402 m.record_tool_failure("foo");
1403 m.reset();
1404 assert!(m.per_tool_calls_snapshot().is_empty());
1405 assert!(m.per_tool_failures_snapshot().is_empty());
1406 }
1407
1408 #[test]
1409 fn test_per_tool_snapshot_is_independent_for_unknown_tools() {
1410 let m = RuntimeMetrics::new();
1411 let snap = m.per_tool_calls_snapshot();
1412 assert!(snap.is_empty());
1413 }
1414
1415 #[test]
1418 fn test_latency_histogram_records_sample() {
1419 let h = LatencyHistogram::default();
1420 h.record(10);
1421 assert_eq!(h.count(), 1);
1422 }
1423
1424 #[test]
1425 fn test_latency_histogram_mean_ms() {
1426 let h = LatencyHistogram::default();
1427 h.record(10);
1428 h.record(20);
1429 assert!((h.mean_ms() - 15.0).abs() < 1e-5);
1430 }
1431
1432 #[test]
1433 fn test_latency_histogram_buckets_correct_bucket() {
1434 let h = LatencyHistogram::default();
1435 h.record(3); let buckets = h.buckets();
1437 assert_eq!(buckets[1].1, 1, "3ms should land in ≤5ms bucket");
1439 assert_eq!(buckets[0].1, 0);
1441 assert_eq!(buckets[2].1, 0);
1442 }
1443
1444 #[test]
1447 fn test_snapshot_returns_all_fields() {
1448 let m = RuntimeMetrics::new();
1449 m.active_sessions.store(1, Ordering::Relaxed);
1450 m.total_sessions.store(2, Ordering::Relaxed);
1451 m.total_steps.store(3, Ordering::Relaxed);
1452 m.backpressure_shed_count.store(6, Ordering::Relaxed);
1453 m.memory_recall_count.store(7, Ordering::Relaxed);
1454 m.record_tool_call("my_tool");
1456 m.record_tool_call("my_tool");
1457 m.record_tool_failure("my_tool");
1458
1459 let snap = m.snapshot();
1460 assert_eq!(snap.active_sessions, 1);
1461 assert_eq!(snap.total_sessions, 2);
1462 assert_eq!(snap.total_steps, 3);
1463 assert_eq!(snap.total_tool_calls, 2);
1464 assert_eq!(snap.failed_tool_calls, 1);
1465 assert_eq!(snap.backpressure_shed_count, 6);
1466 assert_eq!(snap.memory_recall_count, 7);
1467 assert_eq!(snap.per_tool_calls.get("my_tool").copied(), Some(2));
1468 assert_eq!(snap.per_tool_failures.get("my_tool").copied(), Some(1));
1469 }
1470
1471 #[test]
1472 fn test_snapshot_default_is_zeroed() {
1473 let snap = MetricsSnapshot::default();
1474 assert_eq!(snap.active_sessions, 0);
1475 assert_eq!(snap.total_sessions, 0);
1476 assert_eq!(snap.total_steps, 0);
1477 assert!(snap.per_tool_calls.is_empty());
1478 assert!(snap.per_tool_failures.is_empty());
1479 }
1480
1481 #[test]
1484 fn test_metrics_snapshot_contains_all_fields() {
1485 let m = RuntimeMetrics::new();
1486 m.record_step_latency(5);
1487 m.record_step_latency(200);
1488 let snap = m.snapshot();
1489 assert_eq!(snap.step_latency_buckets.len(), 7);
1491 assert!(snap.step_latency_mean_ms > 0.0);
1492 }
1493
1494 #[test]
1497 fn test_per_agent_tool_call_tracking() {
1498 let m = RuntimeMetrics::new();
1499 m.record_agent_tool_call("agent-1", "search");
1500 m.record_agent_tool_call("agent-1", "search");
1501 m.record_agent_tool_call("agent-2", "lookup");
1502 m.record_agent_tool_failure("agent-1", "search");
1503
1504 let calls = m.per_agent_tool_calls_snapshot();
1505 assert_eq!(calls.get("agent-1").and_then(|t| t.get("search")).copied(), Some(2));
1506 assert_eq!(calls.get("agent-2").and_then(|t| t.get("lookup")).copied(), Some(1));
1507
1508 let failures = m.per_agent_tool_failures_snapshot();
1509 assert_eq!(failures.get("agent-1").and_then(|t| t.get("search")).copied(), Some(1));
1510
1511 let snap = m.snapshot();
1513 assert_eq!(snap.per_agent_tool_calls.get("agent-1").and_then(|t| t.get("search")).copied(), Some(2));
1514
1515 m.reset();
1517 assert!(m.per_agent_tool_calls_snapshot().is_empty());
1518 assert!(m.per_agent_tool_failures_snapshot().is_empty());
1519 }
1520
1521 #[test]
1524 fn test_latency_histogram_min_max_ms() {
1525 let h = LatencyHistogram::default();
1526 assert!(h.min_ms().is_none());
1527 assert!(h.max_ms().is_none());
1528
1529 h.record(3); h.record(200); assert!(h.min_ms().is_some());
1532 assert!(h.max_ms().is_some());
1533 assert!(h.min_ms().unwrap() <= h.max_ms().unwrap());
1534 }
1535
1536 #[test]
1537 fn test_latency_histogram_p50_p95_p99() {
1538 let h = LatencyHistogram::default();
1539 for _ in 0..100 {
1540 h.record(5); }
1542 let p50 = h.p50();
1544 let p95 = h.p95();
1545 let p99 = h.p99();
1546 assert_eq!(p50, p95);
1547 assert_eq!(p95, p99);
1548 }
1549
1550 #[test]
1551 fn test_metrics_snapshot_delta_reflects_increments() {
1552 let m = RuntimeMetrics::new();
1553 let before = m.snapshot();
1554 m.total_steps.fetch_add(5, std::sync::atomic::Ordering::Relaxed);
1555 m.total_tool_calls.fetch_add(3, std::sync::atomic::Ordering::Relaxed);
1556 let after = m.snapshot();
1557 let delta = MetricsSnapshot::delta(&after, &before);
1558 assert_eq!(delta.total_steps, 5);
1559 assert_eq!(delta.total_tool_calls, 3);
1560 }
1561
1562 #[test]
1563 fn test_metrics_snapshot_display_contains_key_fields() {
1564 let m = RuntimeMetrics::new();
1565 let snap = m.snapshot();
1566 let s = snap.to_string();
1567 assert!(s.contains("sessions"));
1568 assert!(s.contains("steps"));
1569 assert!(s.contains("latency_mean"));
1570 }
1571
1572 #[test]
1573 fn test_failure_rate_zero_when_no_calls() {
1574 let m = RuntimeMetrics::new();
1575 assert_eq!(m.failure_rate(), 0.0);
1576 }
1577
1578 #[test]
1579 fn test_failure_rate_correct_proportion() {
1580 let m = RuntimeMetrics::new();
1581 m.record_tool_call("tool_a");
1582 m.record_tool_call("tool_a");
1583 m.record_tool_failure("tool_a");
1584 assert!((m.failure_rate() - 0.5).abs() < 1e-9);
1586 }
1587
1588 #[test]
1589 fn test_failure_rate_all_failed() {
1590 let m = RuntimeMetrics::new();
1591 m.record_tool_call("x");
1592 m.record_tool_failure("x");
1593 assert!((m.failure_rate() - 1.0).abs() < 1e-9);
1594 }
1595
1596 #[test]
1597 fn test_top_tools_by_calls_returns_top_n() {
1598 let m = RuntimeMetrics::new();
1599 for _ in 0..5 { m.record_tool_call("a"); }
1600 for _ in 0..3 { m.record_tool_call("b"); }
1601 for _ in 0..1 { m.record_tool_call("c"); }
1602 let top = m.top_tools_by_calls(2);
1603 assert_eq!(top.len(), 2);
1604 assert_eq!(top[0].0, "a");
1605 assert_eq!(top[1].0, "b");
1606 }
1607
1608 #[test]
1609 fn test_top_tools_by_calls_returns_all_when_n_exceeds_count() {
1610 let m = RuntimeMetrics::new();
1611 m.record_tool_call("only");
1612 let top = m.top_tools_by_calls(10);
1613 assert_eq!(top.len(), 1);
1614 assert_eq!(top[0].0, "only");
1615 }
1616
1617 #[test]
1618 fn test_metrics_snapshot_to_json_contains_key_fields() {
1619 let m = RuntimeMetrics::new();
1620 m.record_tool_call("t");
1621 let snap = m.snapshot();
1622 let json = snap.to_json();
1623 assert!(json.get("total_sessions").is_some());
1624 assert!(json.get("total_steps").is_some());
1625 assert!(json.get("total_tool_calls").is_some());
1626 }
1627
1628 #[test]
1629 fn test_metrics_snapshot_is_zero_on_new_metrics() {
1630 let m = RuntimeMetrics::new();
1631 assert!(m.snapshot().is_zero());
1632 }
1633
1634 #[test]
1635 fn test_metrics_snapshot_is_zero_false_after_activity() {
1636 let m = RuntimeMetrics::new();
1637 m.record_tool_call("t");
1638 assert!(!m.snapshot().is_zero());
1639 }
1640
1641 #[test]
1642 fn test_tool_call_count_returns_per_tool_count() {
1643 let m = RuntimeMetrics::new();
1644 m.record_tool_call("search");
1645 m.record_tool_call("search");
1646 m.record_tool_call("fetch");
1647 let snap = m.snapshot();
1648 assert_eq!(snap.tool_call_count("search"), 2);
1649 assert_eq!(snap.tool_call_count("fetch"), 1);
1650 assert_eq!(snap.tool_call_count("absent"), 0);
1651 }
1652
1653 #[test]
1654 fn test_tool_failure_count_returns_per_tool_failures() {
1655 let m = RuntimeMetrics::new();
1656 m.record_tool_call("t");
1657 m.record_tool_failure("t");
1658 let snap = m.snapshot();
1659 assert_eq!(snap.tool_failure_count("t"), 1);
1660 assert_eq!(snap.tool_failure_count("other"), 0);
1661 }
1662
1663 #[test]
1664 fn test_latency_histogram_clear_resets_counts() {
1665 let h = LatencyHistogram::default();
1666 h.record(10);
1667 h.record(20);
1668 assert_eq!(h.count(), 2);
1669 h.clear();
1670 assert_eq!(h.count(), 0);
1671 }
1672
1673 #[test]
1674 fn test_metrics_snapshot_tool_names_sorted() {
1675 let m = RuntimeMetrics::new();
1676 m.record_tool_call("zebra");
1677 m.record_tool_call("alpha");
1678 m.record_tool_call("mango");
1679 let snap = m.snapshot();
1680 assert_eq!(snap.tool_names(), vec!["alpha", "mango", "zebra"]);
1681 }
1682
1683 #[test]
1686 fn test_top_tools_by_failures_returns_top_n_descending() {
1687 let m = RuntimeMetrics::new();
1688 m.record_tool_failure("a");
1689 m.record_tool_failure("a");
1690 m.record_tool_failure("a");
1691 m.record_tool_failure("b");
1692 m.record_tool_failure("b");
1693 m.record_tool_failure("c");
1694 let top2 = m.top_tools_by_failures(2);
1695 assert_eq!(top2.len(), 2);
1696 assert_eq!(top2[0].0, "a");
1697 assert_eq!(top2[0].1, 3);
1698 assert_eq!(top2[1].0, "b");
1699 assert_eq!(top2[1].1, 2);
1700 }
1701
1702 #[test]
1703 fn test_top_tools_by_failures_n_larger_than_tools() {
1704 let m = RuntimeMetrics::new();
1705 m.record_tool_failure("only");
1706 let top = m.top_tools_by_failures(10);
1707 assert_eq!(top.len(), 1);
1708 assert_eq!(top[0].0, "only");
1709 }
1710
1711 #[test]
1712 fn test_latency_histogram_sum_ms_accumulates() {
1713 let h = LatencyHistogram::default();
1714 h.record(100);
1715 h.record(200);
1716 h.record(300);
1717 assert_eq!(h.sum_ms(), 600);
1718 }
1719
1720 #[test]
1721 fn test_latency_histogram_sum_ms_zero_when_empty() {
1722 let h = LatencyHistogram::default();
1723 assert_eq!(h.sum_ms(), 0);
1724 }
1725
1726 #[test]
1729 fn test_latency_histogram_mean_ms_zero_when_empty() {
1730 let h = LatencyHistogram::default();
1731 assert_eq!(h.mean_ms(), 0.0);
1732 }
1733
1734 #[test]
1735 fn test_latency_histogram_mean_ms_computes_average() {
1736 let h = LatencyHistogram::default();
1737 h.record(100);
1738 h.record(200);
1739 h.record(300);
1740 assert!((h.mean_ms() - 200.0).abs() < 1.0);
1741 }
1742
1743 #[test]
1744 fn test_metrics_snapshot_failure_rate_zero_when_no_calls() {
1745 let m = RuntimeMetrics::new();
1746 let snap = m.snapshot();
1747 assert_eq!(snap.failure_rate(), 0.0);
1748 }
1749
1750 #[test]
1751 fn test_metrics_snapshot_failure_rate_correct() {
1752 let m = RuntimeMetrics::new();
1753 m.record_tool_call("t");
1754 m.record_tool_call("t");
1755 m.record_tool_failure("t");
1756 let snap = m.snapshot();
1757 assert!((snap.failure_rate() - 0.5).abs() < 1e-9);
1758 }
1759
1760 #[test]
1763 fn test_success_rate_one_when_no_failures() {
1764 let m = RuntimeMetrics::new();
1765 m.record_tool_call("x");
1766 assert!((m.success_rate() - 1.0).abs() < 1e-9);
1767 }
1768
1769 #[test]
1770 fn test_success_rate_half_when_half_failed() {
1771 let m = RuntimeMetrics::new();
1772 m.record_tool_call("x");
1773 m.record_tool_call("x");
1774 m.record_tool_failure("x");
1775 assert!((m.success_rate() - 0.5).abs() < 1e-9);
1776 }
1777
1778 #[test]
1779 fn test_success_rate_one_when_no_calls() {
1780 let m = RuntimeMetrics::new();
1781 assert!((m.success_rate() - 1.0).abs() < 1e-9);
1783 }
1784
1785 #[test]
1786 fn test_is_active_false_when_no_sessions() {
1787 let m = RuntimeMetrics::new();
1788 assert!(!m.is_active());
1789 }
1790
1791 #[test]
1792 fn test_is_active_true_when_session_active() {
1793 let m = RuntimeMetrics::new();
1794 m.active_sessions.fetch_add(1, Ordering::Relaxed);
1795 assert!(m.is_active());
1796 m.active_sessions.fetch_sub(1, Ordering::Relaxed);
1797 assert!(!m.is_active());
1798 }
1799
1800 #[test]
1801 fn test_checkpoint_errors_increments() {
1802 let m = RuntimeMetrics::new();
1803 assert_eq!(m.checkpoint_errors(), 0);
1804 m.checkpoint_errors.fetch_add(3, Ordering::Relaxed);
1805 assert_eq!(m.checkpoint_errors(), 3);
1806 }
1807
1808 #[test]
1809 fn test_checkpoint_errors_reset_to_zero() {
1810 let m = RuntimeMetrics::new();
1811 m.checkpoint_errors.fetch_add(5, Ordering::Relaxed);
1812 m.reset();
1813 assert_eq!(m.checkpoint_errors(), 0);
1814 }
1815
1816 #[test]
1819 fn test_std_dev_ms_zero_for_no_samples() {
1820 let h = LatencyHistogram::default();
1821 assert!((h.std_dev_ms() - 0.0).abs() < 1e-9);
1822 }
1823
1824 #[test]
1825 fn test_std_dev_ms_zero_for_single_sample() {
1826 let h = LatencyHistogram::default();
1827 h.record(5);
1828 assert!((h.std_dev_ms() - 0.0).abs() < 1e-9);
1829 }
1830
1831 #[test]
1832 fn test_std_dev_ms_positive_for_varied_samples() {
1833 let h = LatencyHistogram::default();
1834 h.record(1); h.record(200); assert!(h.std_dev_ms() > 0.0);
1838 }
1839
1840 #[test]
1841 fn test_std_dev_ms_zero_for_identical_samples() {
1842 let h = LatencyHistogram::default();
1843 h.record(5);
1844 h.record(5);
1845 h.record(5);
1846 assert!(h.std_dev_ms() < 1.0);
1848 }
1849
1850 #[test]
1853 fn test_tool_success_rate_one_when_no_calls() {
1854 let m = RuntimeMetrics::new();
1855 assert!((m.tool_success_rate() - 1.0).abs() < 1e-9);
1856 }
1857
1858 #[test]
1859 fn test_tool_success_rate_one_when_no_failures() {
1860 let m = RuntimeMetrics::new();
1861 m.total_tool_calls.fetch_add(10, Ordering::Relaxed);
1862 assert!((m.tool_success_rate() - 1.0).abs() < 1e-9);
1863 }
1864
1865 #[test]
1866 fn test_tool_success_rate_half_when_half_fail() {
1867 let m = RuntimeMetrics::new();
1868 m.total_tool_calls.fetch_add(10, Ordering::Relaxed);
1869 m.failed_tool_calls.fetch_add(5, Ordering::Relaxed);
1870 assert!((m.tool_success_rate() - 0.5).abs() < 1e-9);
1871 }
1872
1873 #[test]
1874 fn test_tool_success_rate_zero_when_all_fail() {
1875 let m = RuntimeMetrics::new();
1876 m.total_tool_calls.fetch_add(4, Ordering::Relaxed);
1877 m.failed_tool_calls.fetch_add(4, Ordering::Relaxed);
1878 assert!(m.tool_success_rate().abs() < 1e-9);
1879 }
1880
1881 #[test]
1884 fn test_step_latency_p50_zero_when_empty() {
1885 let m = RuntimeMetrics::new();
1886 assert_eq!(m.step_latency_p50(), 0);
1887 }
1888
1889 #[test]
1890 fn test_step_latency_p99_zero_when_empty() {
1891 let m = RuntimeMetrics::new();
1892 assert_eq!(m.step_latency_p99(), 0);
1893 }
1894
1895 #[test]
1896 fn test_step_latency_p50_after_recording() {
1897 let m = RuntimeMetrics::new();
1898 for _ in 0..10 {
1899 m.step_latency.record(100);
1900 }
1901 assert!(m.step_latency_p50() > 0);
1902 }
1903
1904 #[test]
1905 fn test_step_latency_p99_gte_p50() {
1906 let m = RuntimeMetrics::new();
1907 for v in [10, 20, 30, 40, 500] {
1908 m.step_latency.record(v);
1909 }
1910 assert!(m.step_latency_p99() >= m.step_latency_p50());
1911 }
1912
1913 #[test]
1914 fn test_latency_histogram_range_ms_none_when_empty() {
1915 let h = LatencyHistogram::default();
1916 assert!(h.range_ms().is_none());
1917 }
1918
1919 #[test]
1920 fn test_latency_histogram_range_ms_some_for_single_sample() {
1921 let h = LatencyHistogram::default();
1922 h.record(100);
1923 assert!(h.range_ms().is_some());
1925 }
1926
1927 #[test]
1928 fn test_latency_histogram_range_ms_positive_for_spread() {
1929 let h = LatencyHistogram::default();
1930 h.record(10);
1931 h.record(1000);
1932 let range = h.range_ms().unwrap();
1933 assert!(range > 0, "range should be > 0 for spread samples, got {range}");
1934 }
1935
1936 #[test]
1939 fn test_avg_tool_calls_per_session_zero_when_no_sessions() {
1940 let m = RuntimeMetrics::new();
1941 assert!((m.avg_tool_calls_per_session() - 0.0).abs() < 1e-9);
1942 }
1943
1944 #[test]
1945 fn test_avg_tool_calls_per_session_correct_ratio() {
1946 let m = RuntimeMetrics::new();
1947 m.total_sessions.fetch_add(2, Ordering::Relaxed);
1948 m.total_tool_calls.fetch_add(10, Ordering::Relaxed);
1949 assert!((m.avg_tool_calls_per_session() - 5.0).abs() < 1e-9);
1950 }
1951
1952 #[test]
1955 fn test_interquartile_range_ms_empty_is_zero() {
1956 let h = LatencyHistogram::default();
1957 assert_eq!(h.interquartile_range_ms(), 0);
1958 }
1959
1960 #[test]
1961 fn test_interquartile_range_ms_saturates_not_panics() {
1962 let h = LatencyHistogram::default();
1963 for _ in 0..50 {
1964 h.record(10);
1965 }
1966 for _ in 0..50 {
1967 h.record(500);
1968 }
1969 let iqr = h.interquartile_range_ms();
1970 assert!(iqr < u64::MAX);
1972 }
1973
1974 #[test]
1975 fn test_avg_steps_per_session_zero_when_no_sessions() {
1976 let snap = MetricsSnapshot::default();
1977 assert!((snap.avg_steps_per_session() - 0.0).abs() < 1e-9);
1978 }
1979
1980 #[test]
1981 fn test_avg_steps_per_session_correct_ratio() {
1982 let snap = MetricsSnapshot {
1983 total_sessions: 4,
1984 total_steps: 20,
1985 ..Default::default()
1986 };
1987 assert!((snap.avg_steps_per_session() - 5.0).abs() < 1e-9);
1988 }
1989
1990 #[test]
1993 fn test_latency_histogram_is_empty_true_initially() {
1994 let h = LatencyHistogram::default();
1995 assert!(h.is_empty());
1996 }
1997
1998 #[test]
1999 fn test_latency_histogram_is_empty_false_after_record() {
2000 let h = LatencyHistogram::default();
2001 h.record(10);
2002 assert!(!h.is_empty());
2003 }
2004
2005 #[test]
2006 fn test_checkpoint_error_rate_zero_when_no_sessions() {
2007 let m = RuntimeMetrics::new();
2008 assert!((m.checkpoint_error_rate() - 0.0).abs() < 1e-9);
2009 }
2010
2011 #[test]
2012 fn test_checkpoint_error_rate_ratio_correct() {
2013 let m = RuntimeMetrics::new();
2014 m.total_sessions.fetch_add(4, std::sync::atomic::Ordering::Relaxed);
2015 m.checkpoint_errors.fetch_add(2, std::sync::atomic::Ordering::Relaxed);
2016 assert!((m.checkpoint_error_rate() - 0.5).abs() < 1e-9);
2017 }
2018
2019 #[test]
2022 fn test_mode_bucket_ms_none_when_empty() {
2023 let h = LatencyHistogram::default();
2024 assert!(h.mode_bucket_ms().is_none());
2025 }
2026
2027 #[test]
2028 fn test_mode_bucket_ms_returns_bucket_with_most_samples() {
2029 let h = LatencyHistogram::default();
2030 for _ in 0..10 {
2032 h.record(5);
2033 }
2034 for _ in 0..2 {
2036 h.record(400);
2037 }
2038 let mode = h.mode_bucket_ms().unwrap();
2039 assert!(mode <= 50, "expected low-latency bucket, got {mode}");
2041 }
2042
2043 #[test]
2046 fn test_metrics_snapshot_error_rate_zero_when_no_tool_calls() {
2047 let snap = MetricsSnapshot::default();
2048 assert!((snap.error_rate() - 0.0).abs() < 1e-9);
2049 }
2050
2051 #[test]
2052 fn test_metrics_snapshot_error_rate_correct_ratio() {
2053 let snap = MetricsSnapshot {
2054 total_tool_calls: 10,
2055 failed_tool_calls: 3,
2056 ..Default::default()
2057 };
2058 assert!((snap.error_rate() - 0.3).abs() < 1e-9);
2059 }
2060
2061 #[test]
2062 fn test_metrics_snapshot_memory_recall_rate_zero_when_no_sessions() {
2063 let snap = MetricsSnapshot::default();
2064 assert!((snap.memory_recall_rate() - 0.0).abs() < 1e-9);
2065 }
2066
2067 #[test]
2068 fn test_metrics_snapshot_memory_recall_rate_correct_ratio() {
2069 let snap = MetricsSnapshot {
2070 total_sessions: 5,
2071 memory_recall_count: 15,
2072 ..Default::default()
2073 };
2074 assert!((snap.memory_recall_rate() - 3.0).abs() < 1e-9);
2075 }
2076
2077 #[test]
2080 fn test_latency_histogram_p10_zero_when_empty() {
2081 let h = LatencyHistogram::default();
2082 assert_eq!(h.p10(), 0);
2083 }
2084
2085 #[test]
2086 fn test_latency_histogram_p10_lte_p50_lte_p99() {
2087 let h = LatencyHistogram::default();
2088 for ms in [10, 20, 50, 100, 200, 500, 1000] {
2089 h.record(ms);
2090 }
2091 assert!(h.p10() <= h.p50());
2092 assert!(h.p50() <= h.p99());
2093 }
2094
2095 #[test]
2098 fn test_latency_histogram_is_below_p99_true_when_empty() {
2099 let h = LatencyHistogram::default();
2100 assert!(h.is_below_p99(1)); }
2102
2103 #[test]
2104 fn test_latency_histogram_is_below_p99_true_when_under_threshold() {
2105 let h = LatencyHistogram::default();
2106 for _ in 0..100 {
2107 h.record(50);
2108 }
2109 assert!(h.is_below_p99(100));
2110 }
2111
2112 #[test]
2113 fn test_latency_histogram_is_below_p99_false_when_at_threshold() {
2114 let h = LatencyHistogram::default();
2115 for _ in 0..100 {
2116 h.record(200);
2117 }
2118 assert!(!h.is_below_p99(200)); }
2120
2121 #[test]
2122 fn test_metrics_snapshot_is_healthy_true_when_default() {
2123 let snap = MetricsSnapshot::default();
2124 assert!(snap.is_healthy());
2125 }
2126
2127 #[test]
2128 fn test_metrics_snapshot_is_healthy_false_when_failed_tool_calls() {
2129 let snap = MetricsSnapshot { failed_tool_calls: 1, ..Default::default() };
2130 assert!(!snap.is_healthy());
2131 }
2132
2133 #[test]
2134 fn test_metrics_snapshot_is_healthy_false_when_backpressure_shed() {
2135 let snap = MetricsSnapshot { backpressure_shed_count: 2, ..Default::default() };
2136 assert!(!snap.is_healthy());
2137 }
2138
2139 #[test]
2140 fn test_metrics_snapshot_is_healthy_false_when_checkpoint_errors() {
2141 let snap = MetricsSnapshot { checkpoint_errors: 1, ..Default::default() };
2142 assert!(!snap.is_healthy());
2143 }
2144
2145 #[test]
2148 fn test_latency_histogram_median_ms_equals_p50() {
2149 let h = LatencyHistogram::default();
2150 for ms in [10, 50, 100, 200, 500] {
2151 h.record(ms);
2152 }
2153 assert_eq!(h.median_ms(), h.p50());
2154 }
2155
2156 #[test]
2157 fn test_latency_histogram_median_ms_zero_when_empty() {
2158 let h = LatencyHistogram::default();
2159 assert_eq!(h.median_ms(), 0);
2160 }
2161
2162 #[test]
2163 fn test_metrics_snapshot_steps_per_session_zero_when_no_sessions() {
2164 let snap = MetricsSnapshot::default();
2165 assert!((snap.steps_per_session() - 0.0).abs() < 1e-9);
2166 }
2167
2168 #[test]
2169 fn test_metrics_snapshot_steps_per_session_correct_ratio() {
2170 let snap = MetricsSnapshot {
2171 total_sessions: 4,
2172 total_steps: 20,
2173 ..Default::default()
2174 };
2175 assert!((snap.steps_per_session() - 5.0).abs() < 1e-9);
2176 }
2177
2178 #[test]
2179 fn test_runtime_metrics_p50_latency_ms_zero_when_no_data() {
2180 let m = RuntimeMetrics::new();
2181 assert_eq!(m.p50_latency_ms(), 0);
2182 }
2183
2184 #[test]
2185 fn test_runtime_metrics_p50_latency_ms_matches_histogram_p50() {
2186 let m = RuntimeMetrics::new();
2187 for ms in [10_u64, 50, 100, 200, 500] {
2188 m.step_latency.record(ms);
2189 }
2190 assert_eq!(m.p50_latency_ms(), m.step_latency.p50());
2191 }
2192
2193 #[test]
2196 fn test_latency_histogram_has_data_false_when_empty() {
2197 let h = LatencyHistogram::default();
2198 assert!(!h.has_data());
2199 }
2200
2201 #[test]
2202 fn test_latency_histogram_has_data_true_after_record() {
2203 let h = LatencyHistogram::default();
2204 h.record(100);
2205 assert!(h.has_data());
2206 }
2207
2208 #[test]
2209 fn test_latency_histogram_min_ms_none_when_empty() {
2210 let h = LatencyHistogram::default();
2211 assert_eq!(h.min_ms(), None);
2212 }
2213
2214 #[test]
2215 fn test_latency_histogram_min_ms_some_after_record() {
2216 let h = LatencyHistogram::default();
2217 h.record(50);
2218 assert!(h.min_ms().is_some());
2219 }
2220
2221 #[test]
2222 fn test_latency_histogram_p25_lte_p75() {
2223 let h = LatencyHistogram::default();
2224 for ms in [10_u64, 50, 100, 200, 500, 1000, 2000, 5000] {
2225 h.record(ms);
2226 }
2227 assert!(h.p25() <= h.p75());
2228 }
2229
2230 #[test]
2231 fn test_latency_histogram_p90_between_p50_and_p99() {
2232 let h = LatencyHistogram::default();
2233 for ms in [10_u64, 50, 100, 200, 500] {
2234 h.record(ms);
2235 }
2236 assert!(h.p50() <= h.p90());
2237 assert!(h.p90() <= h.p99());
2238 }
2239
2240 #[test]
2241 fn test_metrics_snapshot_tool_success_count_correct() {
2242 let snap = MetricsSnapshot {
2243 per_tool_calls: [("search".to_string(), 10u64)].into(),
2244 per_tool_failures: [("search".to_string(), 3u64)].into(),
2245 ..Default::default()
2246 };
2247 assert_eq!(snap.tool_success_count("search"), 7);
2248 }
2249
2250 #[test]
2251 fn test_metrics_snapshot_tool_success_count_zero_for_unknown_tool() {
2252 let snap = MetricsSnapshot::default();
2253 assert_eq!(snap.tool_success_count("unknown"), 0);
2254 }
2255
2256 #[test]
2257 fn test_metrics_snapshot_tool_failure_rate_correct_ratio() {
2258 let snap = MetricsSnapshot {
2259 per_tool_calls: [("lookup".to_string(), 4u64)].into(),
2260 per_tool_failures: [("lookup".to_string(), 1u64)].into(),
2261 ..Default::default()
2262 };
2263 assert!((snap.tool_failure_rate("lookup") - 0.25).abs() < 1e-9);
2264 }
2265
2266 #[test]
2267 fn test_metrics_snapshot_tool_failure_rate_zero_for_unknown_tool() {
2268 let snap = MetricsSnapshot::default();
2269 assert!((snap.tool_failure_rate("none") - 0.0).abs() < 1e-9);
2270 }
2271
2272 #[test]
2273 fn test_metrics_snapshot_total_successful_tool_calls() {
2274 let snap = MetricsSnapshot {
2275 total_tool_calls: 20,
2276 failed_tool_calls: 5,
2277 ..Default::default()
2278 };
2279 assert_eq!(snap.total_successful_tool_calls(), 15);
2280 }
2281
2282 #[test]
2283 fn test_runtime_metrics_per_tool_calls_snapshot_increments() {
2284 let m = RuntimeMetrics::new();
2285 m.record_tool_call("search");
2286 m.record_tool_call("search");
2287 m.record_tool_call("lookup");
2288 let snap = m.per_tool_calls_snapshot();
2289 assert_eq!(snap.get("search"), Some(&2));
2290 assert_eq!(snap.get("lookup"), Some(&1));
2291 }
2292
2293 #[test]
2294 fn test_runtime_metrics_per_tool_failures_snapshot() {
2295 let m = RuntimeMetrics::new();
2296 m.record_tool_call("search");
2297 m.record_tool_failure("search");
2298 let snap = m.per_tool_failures_snapshot();
2299 assert_eq!(snap.get("search"), Some(&1));
2300 }
2301
2302 #[test]
2303 fn test_runtime_metrics_record_agent_tool_call_tracked() {
2304 let m = RuntimeMetrics::new();
2305 m.record_agent_tool_call("agent-1", "search");
2306 m.record_agent_tool_call("agent-1", "search");
2307 let snap = m.per_agent_tool_calls_snapshot();
2308 assert_eq!(snap.get("agent-1").and_then(|t| t.get("search")), Some(&2));
2309 }
2310
2311 #[test]
2312 fn test_runtime_metrics_per_agent_tool_failures_snapshot() {
2313 let m = RuntimeMetrics::new();
2314 m.record_agent_tool_failure("agent-2", "lookup");
2315 let snap = m.per_agent_tool_failures_snapshot();
2316 assert_eq!(
2317 snap.get("agent-2").and_then(|t| t.get("lookup")),
2318 Some(&1)
2319 );
2320 }
2321
2322 #[test]
2325 fn test_coefficient_of_variation_zero_when_empty() {
2326 let h = LatencyHistogram::default();
2327 assert!((h.coefficient_of_variation() - 0.0).abs() < 1e-9);
2328 }
2329
2330 #[test]
2331 fn test_coefficient_of_variation_positive_with_spread() {
2332 let h = LatencyHistogram::default();
2333 for _ in 0..50 {
2335 h.record(10);
2336 }
2337 for _ in 0..50 {
2338 h.record(1000);
2339 }
2340 let cv = h.coefficient_of_variation();
2341 assert!(cv > 0.0, "CV should be positive for spread data, got {cv}");
2342 }
2343
2344 #[test]
2345 fn test_coefficient_of_variation_near_zero_for_uniform_data() {
2346 let h = LatencyHistogram::default();
2347 for _ in 0..100 {
2349 h.record(50);
2350 }
2351 assert!(h.coefficient_of_variation() < 1.0);
2353 }
2354
2355 #[test]
2358 fn test_latency_histogram_percentile_zero_when_empty() {
2359 let h = LatencyHistogram::default();
2360 assert_eq!(h.percentile(0.5), 0);
2361 }
2362
2363 #[test]
2364 fn test_latency_histogram_percentile_50_matches_p50() {
2365 let h = LatencyHistogram::default();
2366 for ms in [10, 20, 30, 40, 50] {
2367 h.record(ms);
2368 }
2369 assert_eq!(h.percentile(0.5), h.p50());
2370 }
2371
2372 #[test]
2373 fn test_latency_histogram_percentile_99_matches_p99() {
2374 let h = LatencyHistogram::default();
2375 for ms in [10, 50, 100, 500, 1000] {
2376 h.record(ms);
2377 }
2378 assert_eq!(h.percentile(0.99), h.p99());
2379 }
2380
2381 #[test]
2382 fn test_runtime_metrics_record_agent_tool_failure_appears_in_snapshot() {
2383 let m = RuntimeMetrics::new();
2384 m.record_agent_tool_failure("agent-1", "search_tool");
2385 let snapshot = m.per_agent_tool_failures_snapshot();
2386 assert_eq!(snapshot.get("agent-1").and_then(|t| t.get("search_tool")), Some(&1));
2387 }
2388
2389 #[test]
2390 fn test_runtime_metrics_per_agent_tool_calls_snapshot_empty_initially() {
2391 let m = RuntimeMetrics::new();
2392 assert!(m.per_agent_tool_calls_snapshot().is_empty());
2393 }
2394
2395 #[test]
2396 fn test_runtime_metrics_record_step_latency_is_reflected_in_p50() {
2397 let m = RuntimeMetrics::new();
2398 for _ in 0..20 {
2399 m.record_step_latency(100);
2400 }
2401 let snap = m.snapshot();
2404 assert!(snap.total_sessions == 0); }
2406
2407 #[test]
2410 fn test_metrics_snapshot_has_errors_false_when_clean() {
2411 let snap = MetricsSnapshot::default();
2412 assert!(!snap.has_errors());
2413 }
2414
2415 #[test]
2416 fn test_metrics_snapshot_has_errors_true_when_failed_tool_calls() {
2417 let snap = MetricsSnapshot { failed_tool_calls: 2, ..Default::default() };
2418 assert!(snap.has_errors());
2419 }
2420
2421 #[test]
2422 fn test_metrics_snapshot_has_errors_true_when_checkpoint_errors() {
2423 let snap = MetricsSnapshot { checkpoint_errors: 1, ..Default::default() };
2424 assert!(snap.has_errors());
2425 }
2426
2427 #[test]
2428 fn test_latency_histogram_is_above_p99_false_for_low_latency() {
2429 let h = LatencyHistogram::default();
2430 for _ in 0..200 {
2431 h.record(50);
2432 }
2433 assert!(!h.is_above_p99(50));
2434 }
2435
2436 #[test]
2437 fn test_latency_histogram_is_above_p99_true_for_high_latency() {
2438 let h = LatencyHistogram::default();
2439 for _ in 0..200 {
2440 h.record(50);
2441 }
2442 assert!(h.is_above_p99(10_000));
2444 }
2445
2446 #[test]
2449 fn test_latency_histogram_sample_count_zero_when_empty() {
2450 let h = LatencyHistogram::default();
2451 assert_eq!(h.sample_count(), 0);
2452 }
2453
2454 #[test]
2455 fn test_latency_histogram_sample_count_matches_records() {
2456 let h = LatencyHistogram::default();
2457 for _ in 0..7 {
2458 h.record(100);
2459 }
2460 assert_eq!(h.sample_count(), 7);
2461 }
2462
2463 #[test]
2464 fn test_metrics_snapshot_tool_call_rate_zero_when_no_sessions() {
2465 let snap = MetricsSnapshot::default();
2466 assert!((snap.tool_call_rate() - 0.0).abs() < 1e-9);
2467 }
2468
2469 #[test]
2470 fn test_metrics_snapshot_tool_call_rate_correct_ratio() {
2471 let snap = MetricsSnapshot {
2472 total_sessions: 4,
2473 total_tool_calls: 20,
2474 ..Default::default()
2475 };
2476 assert!((snap.tool_call_rate() - 5.0).abs() < 1e-9);
2477 }
2478
2479 #[test]
2482 fn test_metrics_snapshot_backpressure_rate_zero_when_no_sessions() {
2483 let snap = MetricsSnapshot::default();
2484 assert!((snap.backpressure_rate() - 0.0).abs() < 1e-9);
2485 }
2486
2487 #[test]
2488 fn test_metrics_snapshot_backpressure_rate_correct_ratio() {
2489 let snap = MetricsSnapshot {
2490 total_sessions: 2,
2491 backpressure_shed_count: 4,
2492 ..Default::default()
2493 };
2494 assert!((snap.backpressure_rate() - 2.0).abs() < 1e-9);
2495 }
2496
2497 #[test]
2498 fn test_latency_histogram_percentile_spread_zero_when_empty() {
2499 let h = LatencyHistogram::default();
2500 assert_eq!(h.percentile_spread(), 0);
2501 }
2502
2503 #[test]
2504 fn test_latency_histogram_percentile_spread_nonnegative() {
2505 let h = LatencyHistogram::default();
2506 for _ in 0..100 {
2507 h.record(50);
2508 }
2509 for _ in 0..5 {
2510 h.record(500);
2511 }
2512 assert!(h.percentile_spread() >= 0);
2513 }
2514
2515 #[test]
2518 fn test_metrics_snapshot_memory_efficiency_zero_when_no_steps() {
2519 let snap = MetricsSnapshot::default();
2520 assert!((snap.memory_efficiency() - 0.0).abs() < 1e-9);
2521 }
2522
2523 #[test]
2524 fn test_metrics_snapshot_memory_efficiency_correct_ratio() {
2525 let snap = MetricsSnapshot {
2526 total_steps: 10,
2527 memory_recall_count: 4,
2528 ..Default::default()
2529 };
2530 assert!((snap.memory_efficiency() - 0.4).abs() < 1e-9);
2531 }
2532
2533 #[test]
2534 fn test_latency_histogram_is_uniform_true_when_empty() {
2535 let h = LatencyHistogram::default();
2536 assert!(h.is_uniform());
2537 }
2538
2539 #[test]
2540 fn test_latency_histogram_is_uniform_true_for_single_bucket() {
2541 let h = LatencyHistogram::default();
2542 for _ in 0..50 {
2543 h.record(50); }
2545 assert!(h.is_uniform());
2546 }
2547
2548 #[test]
2549 fn test_latency_histogram_is_uniform_false_for_mixed_latencies() {
2550 let h = LatencyHistogram::default();
2551 h.record(1);
2552 h.record(1000);
2553 assert!(!h.is_uniform());
2554 }
2555
2556 #[test]
2559 fn test_latency_histogram_bucket_counts_all_zero_when_empty() {
2560 let h = LatencyHistogram::default();
2561 assert_eq!(h.bucket_counts(), [0u64; 7]);
2562 }
2563
2564 #[test]
2565 fn test_latency_histogram_bucket_counts_increments_correct_bucket() {
2566 let h = LatencyHistogram::default();
2567 h.record(1); let counts = h.bucket_counts();
2569 assert_eq!(counts[0], 1);
2570 assert!(counts[1..].iter().all(|&c| c == 0));
2571 }
2572
2573 #[test]
2574 fn test_metrics_snapshot_active_session_ratio_zero_when_no_sessions() {
2575 let snap = MetricsSnapshot::default();
2576 assert!((snap.active_session_ratio() - 0.0).abs() < 1e-9);
2577 }
2578
2579 #[test]
2580 fn test_metrics_snapshot_active_session_ratio_correct() {
2581 let snap = MetricsSnapshot {
2582 total_sessions: 10,
2583 active_sessions: 3,
2584 ..Default::default()
2585 };
2586 assert!((snap.active_session_ratio() - 0.3).abs() < 1e-9);
2587 }
2588
2589 #[test]
2590 fn test_step_to_tool_ratio_correct_value() {
2591 let snap = MetricsSnapshot {
2592 total_steps: 4,
2593 total_tool_calls: 2,
2594 ..Default::default()
2595 };
2596 assert!((snap.step_to_tool_ratio() - 0.5).abs() < 1e-9);
2597 }
2598
2599 #[test]
2600 fn test_step_to_tool_ratio_zero_steps_returns_zero() {
2601 let snap = MetricsSnapshot {
2602 total_steps: 0,
2603 total_tool_calls: 5,
2604 ..Default::default()
2605 };
2606 assert_eq!(snap.step_to_tool_ratio(), 0.0);
2607 }
2608
2609 #[test]
2610 fn test_latency_histogram_min_occupied_ms_returns_smallest_occupied_bucket() {
2611 let h = LatencyHistogram::default();
2612 h.record(10); h.record(200); assert_eq!(h.min_occupied_ms(), Some(10));
2616 }
2617
2618 #[test]
2619 fn test_latency_histogram_min_occupied_ms_empty_returns_none() {
2620 let h = LatencyHistogram::default();
2621 assert_eq!(h.min_occupied_ms(), None);
2622 }
2623
2624 #[test]
2625 fn test_metrics_snapshot_has_failures_true_when_failures_exist() {
2626 let snap = MetricsSnapshot {
2627 failed_tool_calls: 1,
2628 ..Default::default()
2629 };
2630 assert!(snap.has_failures());
2631 }
2632
2633 #[test]
2634 fn test_metrics_snapshot_has_failures_false_when_no_failures() {
2635 let snap = MetricsSnapshot::default();
2636 assert!(!snap.has_failures());
2637 }
2638
2639 #[test]
2640 fn test_latency_histogram_max_occupied_ms_returns_largest_occupied_bucket() {
2641 let h = LatencyHistogram::default();
2642 h.record(5); h.record(200); assert_eq!(h.max_occupied_ms(), Some(500));
2645 }
2646
2647 #[test]
2648 fn test_latency_histogram_max_occupied_ms_empty_returns_none() {
2649 let h = LatencyHistogram::default();
2650 assert_eq!(h.max_occupied_ms(), None);
2651 }
2652
2653 #[test]
2654 fn test_latency_histogram_occupied_bucket_count_correct() {
2655 let h = LatencyHistogram::default();
2656 h.record(5); h.record(200); assert_eq!(h.occupied_bucket_count(), 2);
2659 }
2660
2661 #[test]
2662 fn test_latency_histogram_occupied_bucket_count_empty_returns_zero() {
2663 let h = LatencyHistogram::default();
2664 assert_eq!(h.occupied_bucket_count(), 0);
2665 }
2666
2667 #[test]
2668 fn test_metrics_snapshot_tool_diversity_counts_distinct_tools() {
2669 let snap = MetricsSnapshot {
2670 per_tool_calls: [("a".to_string(), 1u64), ("b".to_string(), 2u64)]
2671 .into_iter()
2672 .collect(),
2673 ..Default::default()
2674 };
2675 assert_eq!(snap.tool_diversity(), 2);
2676 }
2677
2678 #[test]
2679 fn test_metrics_snapshot_tool_diversity_empty_returns_zero() {
2680 let snap = MetricsSnapshot::default();
2681 assert_eq!(snap.tool_diversity(), 0);
2682 }
2683
2684 #[test]
2685 fn test_runtime_metrics_total_step_latency_ms_sums_recorded_latencies() {
2686 let m = RuntimeMetrics::new();
2687 m.record_step_latency(100);
2688 m.record_step_latency(200);
2689 assert_eq!(m.total_step_latency_ms(), 300);
2690 }
2691
2692 #[test]
2693 fn test_runtime_metrics_total_step_latency_ms_zero_when_empty() {
2694 let m = RuntimeMetrics::new();
2695 assert_eq!(m.total_step_latency_ms(), 0);
2696 }
2697
2698 #[test]
2699 fn test_metrics_snapshot_avg_failures_per_session_correct() {
2700 let snap = MetricsSnapshot {
2701 total_sessions: 4,
2702 failed_tool_calls: 2,
2703 ..Default::default()
2704 };
2705 assert!((snap.avg_failures_per_session() - 0.5).abs() < 1e-9);
2706 }
2707
2708 #[test]
2709 fn test_metrics_snapshot_avg_failures_per_session_zero_when_no_sessions() {
2710 let snap = MetricsSnapshot::default();
2711 assert_eq!(snap.avg_failures_per_session(), 0.0);
2712 }
2713
2714 #[test]
2715 fn test_latency_histogram_is_skewed_true_when_p99_much_greater_than_p50() {
2716 let h = LatencyHistogram::default();
2717 for _ in 0..100 {
2719 h.record(1); }
2721 h.record(500); let _ = h.is_skewed();
2726 }
2727
2728 #[test]
2729 fn test_latency_histogram_is_skewed_false_when_empty() {
2730 let h = LatencyHistogram::default();
2731 assert!(!h.is_skewed());
2732 }
2733
2734 #[test]
2737 fn test_most_called_tool_returns_tool_with_most_calls() {
2738 let snap = MetricsSnapshot {
2739 per_tool_calls: [
2740 ("search".to_string(), 5u64),
2741 ("write".to_string(), 2u64),
2742 ]
2743 .into_iter()
2744 .collect(),
2745 ..Default::default()
2746 };
2747 assert_eq!(snap.most_called_tool(), Some("search".to_string()));
2748 }
2749
2750 #[test]
2751 fn test_most_called_tool_returns_none_when_empty() {
2752 let snap = MetricsSnapshot::default();
2753 assert!(snap.most_called_tool().is_none());
2754 }
2755
2756 #[test]
2757 fn test_tool_names_with_failures_returns_sorted_names_with_failures() {
2758 let snap = MetricsSnapshot {
2759 per_tool_failures: [
2760 ("search".to_string(), 3u64),
2761 ("write".to_string(), 0u64),
2762 ("calc".to_string(), 1u64),
2763 ]
2764 .into_iter()
2765 .collect(),
2766 ..Default::default()
2767 };
2768 assert_eq!(snap.tool_names_with_failures(), vec!["calc", "search"]);
2769 }
2770
2771 #[test]
2772 fn test_tool_names_with_failures_empty_when_no_failures() {
2773 let snap = MetricsSnapshot::default();
2774 assert!(snap.tool_names_with_failures().is_empty());
2775 }
2776
2777 #[test]
2780 fn test_agent_with_most_calls_returns_highest_total() {
2781 let snap = MetricsSnapshot {
2782 per_agent_tool_calls: [
2783 ("agent_a".to_string(), [("search".to_string(), 3u64), ("write".to_string(), 2u64)].into_iter().collect()),
2784 ("agent_b".to_string(), [("search".to_string(), 1u64)].into_iter().collect()),
2785 ]
2786 .into_iter()
2787 .collect(),
2788 ..Default::default()
2789 };
2790 assert_eq!(snap.agent_with_most_calls(), Some("agent_a".to_string()));
2791 }
2792
2793 #[test]
2794 fn test_agent_with_most_calls_returns_none_when_empty() {
2795 let snap = MetricsSnapshot::default();
2796 assert!(snap.agent_with_most_calls().is_none());
2797 }
2798
2799 #[test]
2802 fn test_total_agent_count_returns_number_of_distinct_agents() {
2803 let snap = MetricsSnapshot {
2804 per_agent_tool_calls: [
2805 ("a".to_string(), std::collections::HashMap::new()),
2806 ("b".to_string(), std::collections::HashMap::new()),
2807 ]
2808 .into_iter()
2809 .collect(),
2810 ..Default::default()
2811 };
2812 assert_eq!(snap.total_agent_count(), 2);
2813 }
2814
2815 #[test]
2816 fn test_total_agent_count_zero_when_empty() {
2817 let snap = MetricsSnapshot::default();
2818 assert_eq!(snap.total_agent_count(), 0);
2819 }
2820
2821 #[test]
2822 fn test_steps_per_tool_call_returns_ratio() {
2823 let snap = MetricsSnapshot {
2824 total_steps: 10,
2825 total_tool_calls: 5,
2826 ..Default::default()
2827 };
2828 assert!((snap.steps_per_tool_call() - 2.0).abs() < 1e-9);
2829 }
2830
2831 #[test]
2832 fn test_steps_per_tool_call_zero_when_no_tool_calls() {
2833 let snap = MetricsSnapshot::default();
2834 assert_eq!(snap.steps_per_tool_call(), 0.0);
2835 }
2836
2837 #[test]
2840 fn test_failed_tool_ratio_for_returns_failure_rate() {
2841 let snap = MetricsSnapshot {
2842 per_tool_calls: [("tool".to_string(), 10u64)].into_iter().collect(),
2843 per_tool_failures: [("tool".to_string(), 2u64)].into_iter().collect(),
2844 ..Default::default()
2845 };
2846 assert!((snap.failed_tool_ratio_for("tool") - 0.2).abs() < 1e-9);
2847 }
2848
2849 #[test]
2850 fn test_failed_tool_ratio_for_zero_when_no_calls() {
2851 let snap = MetricsSnapshot::default();
2852 assert_eq!(snap.failed_tool_ratio_for("missing"), 0.0);
2853 }
2854
2855 #[test]
2856 fn test_backpressure_shed_rate_returns_ratio() {
2857 let snap = MetricsSnapshot {
2858 total_tool_calls: 100,
2859 backpressure_shed_count: 5,
2860 ..Default::default()
2861 };
2862 assert!((snap.backpressure_shed_rate() - 0.05).abs() < 1e-9);
2863 }
2864
2865 #[test]
2866 fn test_backpressure_shed_rate_zero_when_no_tool_calls() {
2867 let snap = MetricsSnapshot::default();
2868 assert_eq!(snap.backpressure_shed_rate(), 0.0);
2869 }
2870}