1#![allow(clippy::must_use_candidate)]
12#![allow(clippy::missing_panics_doc)]
13#![allow(clippy::missing_errors_doc)]
14#![allow(clippy::module_name_repetitions)]
15#![allow(clippy::missing_const_for_fn)]
16#![allow(clippy::cast_possible_truncation)]
17#![allow(clippy::cast_precision_loss)]
18#![allow(clippy::cast_lossless)]
19#![allow(clippy::cast_sign_loss)]
20#![allow(clippy::suboptimal_flops)]
21#![allow(clippy::format_push_string)]
22#![allow(clippy::uninlined_format_args)]
23#![allow(clippy::doc_markdown)]
24#![allow(clippy::use_self)]
25#![allow(clippy::unwrap_used)]
26#![allow(clippy::useless_format)]
27
28use serde::{Deserialize, Serialize};
29use std::collections::HashMap;
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct VarianceTree {
38 pub total_variance: f64,
40 pub components: Vec<VarianceComponent>,
42}
43
44impl VarianceTree {
45 pub fn new() -> Self {
47 Self {
48 total_variance: 0.0,
49 components: Vec::new(),
50 }
51 }
52
53 pub fn add_component(&mut self, component: VarianceComponent) {
55 self.total_variance += component.variance;
56 self.components.push(component);
57 }
58
59 pub fn recalculate_percentages(&mut self) {
61 if self.total_variance > 0.0 {
62 for comp in &mut self.components {
63 comp.percentage = (comp.variance / self.total_variance) * 100.0;
64 comp.recalculate_percentages(self.total_variance);
65 }
66 }
67 }
68
69 pub fn from_samples(samples: &[LatencySample]) -> Self {
71 let mut tree = Self::new();
72
73 let mut component_samples: HashMap<String, Vec<f64>> = HashMap::new();
75 for sample in samples {
76 for (component, latency) in &sample.components {
77 component_samples
78 .entry(component.clone())
79 .or_default()
80 .push(*latency);
81 }
82 }
83
84 for (name, values) in component_samples {
86 let variance = calculate_variance(&values);
87 tree.add_component(VarianceComponent {
88 name,
89 variance,
90 percentage: 0.0,
91 children: Vec::new(),
92 });
93 }
94
95 tree.recalculate_percentages();
96 tree
97 }
98}
99
100impl Default for VarianceTree {
101 fn default() -> Self {
102 Self::new()
103 }
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct VarianceComponent {
109 pub name: String,
111 pub variance: f64,
113 pub percentage: f64,
115 pub children: Vec<VarianceComponent>,
117}
118
119impl VarianceComponent {
120 pub fn new(name: &str, variance: f64) -> Self {
122 Self {
123 name: name.to_string(),
124 variance,
125 percentage: 0.0,
126 children: Vec::new(),
127 }
128 }
129
130 pub fn add_child(&mut self, child: VarianceComponent) {
132 self.children.push(child);
133 }
134
135 fn recalculate_percentages(&mut self, total: f64) {
137 for child in &mut self.children {
138 child.percentage = (child.variance / total) * 100.0;
139 child.recalculate_percentages(total);
140 }
141 }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct LatencySample {
147 pub total_ms: f64,
149 pub components: HashMap<String, f64>,
151 pub timestamp_ms: u64,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct ApdexCalculator {
162 pub satisfied_threshold_ms: u64,
164 pub tolerating_threshold_ms: u64,
166 satisfied_count: u64,
168 tolerating_count: u64,
170 frustrated_count: u64,
172}
173
174impl ApdexCalculator {
175 pub fn new(satisfied_ms: u64, tolerating_ms: u64) -> Self {
178 Self {
179 satisfied_threshold_ms: satisfied_ms,
180 tolerating_threshold_ms: tolerating_ms,
181 satisfied_count: 0,
182 tolerating_count: 0,
183 frustrated_count: 0,
184 }
185 }
186
187 pub fn record(&mut self, latency_ms: u64) {
189 if latency_ms <= self.satisfied_threshold_ms {
190 self.satisfied_count += 1;
191 } else if latency_ms <= self.tolerating_threshold_ms {
192 self.tolerating_count += 1;
193 } else {
194 self.frustrated_count += 1;
195 }
196 }
197
198 pub fn score(&self) -> f64 {
200 let total = self.total_count();
201 if total == 0 {
202 return 1.0; }
204 (self.satisfied_count as f64 + self.tolerating_count as f64 / 2.0) / total as f64
205 }
206
207 pub fn total_count(&self) -> u64 {
209 self.satisfied_count + self.tolerating_count + self.frustrated_count
210 }
211
212 pub fn satisfied(&self) -> u64 {
214 self.satisfied_count
215 }
216
217 pub fn tolerating(&self) -> u64 {
219 self.tolerating_count
220 }
221
222 pub fn frustrated(&self) -> u64 {
224 self.frustrated_count
225 }
226
227 pub fn rating(&self) -> ApdexRating {
229 let score = self.score();
230 if score >= 0.94 {
231 ApdexRating::Excellent
232 } else if score >= 0.85 {
233 ApdexRating::Good
234 } else if score >= 0.70 {
235 ApdexRating::Fair
236 } else if score >= 0.50 {
237 ApdexRating::Poor
238 } else {
239 ApdexRating::Unacceptable
240 }
241 }
242
243 pub fn reset(&mut self) {
245 self.satisfied_count = 0;
246 self.tolerating_count = 0;
247 self.frustrated_count = 0;
248 }
249}
250
251impl Default for ApdexCalculator {
252 fn default() -> Self {
253 Self::new(100, 400) }
255}
256
257#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
259pub enum ApdexRating {
260 Excellent,
262 Good,
264 Fair,
266 Poor,
268 Unacceptable,
270}
271
272impl ApdexRating {
273 pub fn as_str(&self) -> &'static str {
275 match self {
276 Self::Excellent => "Excellent",
277 Self::Good => "Good",
278 Self::Fair => "Fair",
279 Self::Poor => "Poor",
280 Self::Unacceptable => "Unacceptable",
281 }
282 }
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct KneeDetector {
292 points: Vec<(f64, f64)>,
294 pub knee_point: Option<(f64, f64)>,
296 pub recommended_capacity: Option<f64>,
298}
299
300impl KneeDetector {
301 pub fn new() -> Self {
303 Self {
304 points: Vec::new(),
305 knee_point: None,
306 recommended_capacity: None,
307 }
308 }
309
310 pub fn add_point(&mut self, load: f64, latency: f64) {
312 self.points.push((load, latency));
313 }
314
315 pub fn detect(&mut self) {
317 if self.points.len() < 3 {
318 return;
319 }
320
321 self.points.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
323
324 let mut max_curvature = 0.0;
326 let mut knee_idx = 0;
327
328 for i in 1..self.points.len() - 1 {
329 let (x0, y0) = self.points[i - 1];
330 let (x1, y1) = self.points[i];
331 let (x2, y2) = self.points[i + 1];
332
333 let dy1 = (y1 - y0) / (x1 - x0);
335 let dy2 = (y2 - y1) / (x2 - x1);
336
337 let d2y = (dy2 - dy1) / ((x2 - x0) / 2.0);
339
340 if d2y > max_curvature {
341 max_curvature = d2y;
342 knee_idx = i;
343 }
344 }
345
346 if max_curvature > 0.0 {
347 self.knee_point = Some(self.points[knee_idx]);
348 self.recommended_capacity = Some(self.points[knee_idx].0 * 0.8);
349 }
350 }
351
352 pub fn points(&self) -> &[(f64, f64)] {
354 &self.points
355 }
356}
357
358impl Default for KneeDetector {
359 fn default() -> Self {
360 Self::new()
361 }
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct TailAttribution {
371 pub percentile: u8,
373 pub latency_ms: u64,
375 pub primary_cause: String,
377 pub contributing_factors: Vec<(String, f64)>,
379}
380
381impl TailAttribution {
382 pub fn new(percentile: u8, latency_ms: u64, primary_cause: &str) -> Self {
384 Self {
385 percentile,
386 latency_ms,
387 primary_cause: primary_cause.to_string(),
388 contributing_factors: Vec::new(),
389 }
390 }
391
392 pub fn add_factor(&mut self, factor: &str, weight: f64) {
394 self.contributing_factors.push((factor.to_string(), weight));
395 }
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize)]
400pub struct QuantileRegression {
401 pub quantiles: Vec<f64>,
403 pub attributions: Vec<TailAttribution>,
405}
406
407impl QuantileRegression {
408 pub fn new() -> Self {
410 Self {
411 quantiles: vec![0.5, 0.9, 0.95, 0.99, 0.999],
412 attributions: Vec::new(),
413 }
414 }
415
416 pub fn add_attribution(&mut self, attr: TailAttribution) {
418 self.attributions.push(attr);
419 }
420}
421
422impl Default for QuantileRegression {
423 fn default() -> Self {
424 Self::new()
425 }
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct StatisticalAnalysis {
435 pub scenario_name: String,
437 pub variance_tree: VarianceTree,
439 pub apdex: ApdexCalculator,
441 pub knee_detector: KneeDetector,
443 pub quantile_regression: QuantileRegression,
445 pub coefficient_of_variation: f64,
447}
448
449impl StatisticalAnalysis {
450 pub fn new(scenario_name: &str) -> Self {
452 Self {
453 scenario_name: scenario_name.to_string(),
454 variance_tree: VarianceTree::new(),
455 apdex: ApdexCalculator::default(),
456 knee_detector: KneeDetector::new(),
457 quantile_regression: QuantileRegression::new(),
458 coefficient_of_variation: 0.0,
459 }
460 }
461}
462
463pub fn render_statistical_report(analysis: &StatisticalAnalysis) -> String {
469 let mut out = String::new();
470
471 out.push_str(&format!(
472 "STATISTICAL ANALYSIS: {}\n",
473 analysis.scenario_name
474 ));
475 out.push_str("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n");
476
477 out.push_str("VARIANCE DECOMPOSITION\n");
479 out.push_str("┌───────────────────────────────────────────────────────────────┐\n");
480 out.push_str(&format!(
481 "│ Total Latency Variance: {:.0} ms² │\n",
482 analysis.variance_tree.total_variance
483 ));
484 out.push_str("│ │\n");
485
486 for comp in &analysis.variance_tree.components {
487 let bar_len = (comp.percentage / 5.0) as usize;
488 let bar: String = "█".repeat(bar_len.min(20));
489 out.push_str(&format!(
490 "│ ├── {:<12}: {:>6.0} ms² ({:>4.1}%) {:20} │\n",
491 truncate(&comp.name, 12),
492 comp.variance,
493 comp.percentage,
494 bar
495 ));
496 }
497 out.push_str("└───────────────────────────────────────────────────────────────┘\n\n");
498
499 out.push_str("APDEX SCORE\n");
501 out.push_str("┌───────────────────────────────────────────────────────────────┐\n");
502 out.push_str(&format!(
503 "│ Target: {}ms (Satisfied), {}ms (Tolerating) │\n",
504 analysis.apdex.satisfied_threshold_ms, analysis.apdex.tolerating_threshold_ms
505 ));
506 out.push_str(&format!(
507 "│ │\n"
508 ));
509 out.push_str(&format!(
510 "│ Satisfied: {:>6} requests ({:>4.1}%) │\n",
511 analysis.apdex.satisfied(),
512 (analysis.apdex.satisfied() as f64 / analysis.apdex.total_count().max(1) as f64) * 100.0
513 ));
514 out.push_str(&format!(
515 "│ Tolerating: {:>6} requests ({:>4.1}%) │\n",
516 analysis.apdex.tolerating(),
517 (analysis.apdex.tolerating() as f64 / analysis.apdex.total_count().max(1) as f64) * 100.0
518 ));
519 out.push_str(&format!(
520 "│ Frustrated: {:>6} requests ({:>4.1}%) │\n",
521 analysis.apdex.frustrated(),
522 (analysis.apdex.frustrated() as f64 / analysis.apdex.total_count().max(1) as f64) * 100.0
523 ));
524 out.push_str(&format!(
525 "│ │\n"
526 ));
527 out.push_str(&format!(
528 "│ Apdex Score: {:.2} ({}) │\n",
529 analysis.apdex.score(),
530 analysis.apdex.rating().as_str()
531 ));
532 out.push_str("└───────────────────────────────────────────────────────────────┘\n\n");
533
534 if let Some((load, latency)) = analysis.knee_detector.knee_point {
536 out.push_str("THROUGHPUT KNEE DETECTION\n");
537 out.push_str("┌───────────────────────────────────────────────────────────────┐\n");
538 out.push_str(&format!(
539 "│ Knee detected at: {:.0} concurrent users │\n",
540 load
541 ));
542 out.push_str(&format!(
543 "│ Latency at knee: {:.0}ms │\n",
544 latency
545 ));
546 if let Some(rec) = analysis.knee_detector.recommended_capacity {
547 out.push_str(&format!(
548 "│ Recommended capacity: {:.0} users (80% of knee) │\n",
549 rec
550 ));
551 }
552 out.push_str("└───────────────────────────────────────────────────────────────┘\n\n");
553 }
554
555 out
556}
557
558pub fn render_statistical_json(analysis: &StatisticalAnalysis) -> String {
560 serde_json::to_string_pretty(analysis).unwrap_or_else(|_| "{}".to_string())
561}
562
563fn calculate_variance(values: &[f64]) -> f64 {
569 if values.is_empty() {
570 return 0.0;
571 }
572 let mean = values.iter().sum::<f64>() / values.len() as f64;
573 let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
574 variance
575}
576
577fn truncate(s: &str, max: usize) -> String {
579 if s.len() <= max {
580 s.to_string()
581 } else {
582 format!("{}…", &s[..max - 1])
583 }
584}
585
586#[cfg(test)]
591#[allow(clippy::unwrap_used, clippy::expect_used, clippy::float_cmp)]
592mod tests {
593 use super::*;
594
595 #[test]
596 fn test_variance_component() {
597 let comp = VarianceComponent::new("Network", 1000.0);
598 assert_eq!(comp.name, "Network");
599 assert_eq!(comp.variance, 1000.0);
600 }
601
602 #[test]
603 fn test_variance_tree() {
604 let mut tree = VarianceTree::new();
605 tree.add_component(VarianceComponent::new("Network", 600.0));
606 tree.add_component(VarianceComponent::new("WASM", 400.0));
607 tree.recalculate_percentages();
608
609 assert_eq!(tree.total_variance, 1000.0);
610 assert_eq!(tree.components[0].percentage, 60.0);
611 assert_eq!(tree.components[1].percentage, 40.0);
612 }
613
614 #[test]
615 fn test_apdex_calculator() {
616 let mut apdex = ApdexCalculator::new(100, 400);
617
618 for _ in 0..8 {
620 apdex.record(50);
621 }
622 for _ in 0..2 {
623 apdex.record(200);
624 }
625
626 assert_eq!(apdex.satisfied(), 8);
627 assert_eq!(apdex.tolerating(), 2);
628 assert_eq!(apdex.frustrated(), 0);
629 assert!((apdex.score() - 0.9).abs() < 0.001);
631 assert_eq!(apdex.rating(), ApdexRating::Good);
632 }
633
634 #[test]
635 fn test_apdex_frustrated() {
636 let mut apdex = ApdexCalculator::new(100, 400);
637 apdex.record(500); apdex.record(1000); assert_eq!(apdex.frustrated(), 2);
641 assert_eq!(apdex.score(), 0.0);
642 assert_eq!(apdex.rating(), ApdexRating::Unacceptable);
643 }
644
645 #[test]
646 fn test_apdex_rating() {
647 assert_eq!(ApdexRating::Excellent.as_str(), "Excellent");
648 assert_eq!(ApdexRating::Poor.as_str(), "Poor");
649 }
650
651 #[test]
652 fn test_knee_detector() {
653 let mut detector = KneeDetector::new();
654
655 detector.add_point(10.0, 50.0);
657 detector.add_point(20.0, 55.0);
658 detector.add_point(30.0, 60.0);
659 detector.add_point(40.0, 70.0);
660 detector.add_point(50.0, 100.0);
661 detector.add_point(60.0, 200.0);
662 detector.add_point(70.0, 400.0);
663
664 detector.detect();
665
666 assert!(detector.knee_point.is_some());
667 assert!(detector.recommended_capacity.is_some());
668 }
669
670 #[test]
671 fn test_tail_attribution() {
672 let mut attr = TailAttribution::new(99, 456, "Network congestion");
673 attr.add_factor("DNS", 0.15);
674 attr.add_factor("TLS handshake", 0.25);
675
676 assert_eq!(attr.percentile, 99);
677 assert_eq!(attr.contributing_factors.len(), 2);
678 }
679
680 #[test]
681 fn test_quantile_regression() {
682 let mut qr = QuantileRegression::new();
683 qr.add_attribution(TailAttribution::new(50, 78, "Typical case"));
684 qr.add_attribution(TailAttribution::new(99, 456, "Network"));
685
686 assert_eq!(qr.attributions.len(), 2);
687 }
688
689 #[test]
690 fn test_calculate_variance() {
691 let values = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
692 let variance = calculate_variance(&values);
693 assert!((variance - 4.0).abs() < 0.001);
695 }
696
697 #[test]
698 fn test_statistical_analysis() {
699 let analysis = StatisticalAnalysis::new("Test Scenario");
700 assert_eq!(analysis.scenario_name, "Test Scenario");
701 }
702
703 #[test]
704 fn test_render_statistical_report() {
705 let mut analysis = StatisticalAnalysis::new("WASM Boot");
706 analysis
707 .variance_tree
708 .add_component(VarianceComponent::new("Network", 500.0));
709 analysis.variance_tree.recalculate_percentages();
710 analysis.apdex.record(50);
711 analysis.apdex.record(100);
712
713 let report = render_statistical_report(&analysis);
714 assert!(report.contains("WASM Boot"));
715 assert!(report.contains("VARIANCE"));
716 assert!(report.contains("APDEX"));
717 }
718
719 #[test]
720 fn test_render_statistical_json() {
721 let analysis = StatisticalAnalysis::new("JSON Test");
722 let json = render_statistical_json(&analysis);
723 assert!(json.contains("JSON Test"));
724 }
725
726 #[test]
727 fn test_variance_tree_default() {
728 let tree = VarianceTree::default();
729 assert_eq!(tree.total_variance, 0.0);
730 assert!(tree.components.is_empty());
731 }
732
733 #[test]
734 fn test_variance_tree_from_samples() {
735 let mut samples = Vec::new();
736
737 let mut components1 = HashMap::new();
739 components1.insert("network".to_string(), 50.0);
740 components1.insert("compute".to_string(), 20.0);
741 samples.push(LatencySample {
742 total_ms: 70.0,
743 components: components1,
744 timestamp_ms: 1000,
745 });
746
747 let mut components2 = HashMap::new();
749 components2.insert("network".to_string(), 60.0);
750 components2.insert("compute".to_string(), 25.0);
751 samples.push(LatencySample {
752 total_ms: 85.0,
753 components: components2,
754 timestamp_ms: 2000,
755 });
756
757 let mut components3 = HashMap::new();
759 components3.insert("network".to_string(), 55.0);
760 components3.insert("compute".to_string(), 22.0);
761 samples.push(LatencySample {
762 total_ms: 77.0,
763 components: components3,
764 timestamp_ms: 3000,
765 });
766
767 let tree = VarianceTree::from_samples(&samples);
768
769 assert_eq!(tree.components.len(), 2);
771 assert!(tree.total_variance > 0.0);
772
773 for comp in &tree.components {
775 assert!(comp.percentage >= 0.0);
776 assert!(comp.percentage <= 100.0);
777 }
778 }
779
780 #[test]
781 fn test_variance_tree_recalculate_empty() {
782 let mut tree = VarianceTree::new();
783 tree.recalculate_percentages();
785 assert_eq!(tree.total_variance, 0.0);
786 }
787
788 #[test]
789 fn test_variance_tree_recalculate_with_children() {
790 let mut tree = VarianceTree::new();
791
792 let mut parent = VarianceComponent::new("parent", 100.0);
794 parent.add_child(VarianceComponent::new("child1", 60.0));
795 parent.add_child(VarianceComponent::new("child2", 40.0));
796
797 tree.add_component(parent);
798 tree.recalculate_percentages();
799
800 assert!((tree.components[0].percentage - 100.0).abs() < 0.001);
802 }
803
804 #[test]
805 fn test_latency_sample() {
806 let mut components = HashMap::new();
807 components.insert("dns".to_string(), 10.0);
808 components.insert("connect".to_string(), 20.0);
809
810 let sample = LatencySample {
811 total_ms: 30.0,
812 components,
813 timestamp_ms: 1234567890,
814 };
815
816 assert_eq!(sample.total_ms, 30.0);
817 assert_eq!(sample.components.len(), 2);
818 }
819
820 #[test]
821 fn test_apdex_reset() {
822 let mut apdex = ApdexCalculator::new(100, 400);
823 apdex.record(50);
824 apdex.record(200);
825 apdex.record(500);
826
827 assert_eq!(apdex.total_count(), 3);
828
829 apdex.reset();
830 assert_eq!(apdex.total_count(), 0);
831 assert_eq!(apdex.satisfied(), 0);
832 assert_eq!(apdex.tolerating(), 0);
833 assert_eq!(apdex.frustrated(), 0);
834 }
835
836 #[test]
837 fn test_apdex_default() {
838 let apdex = ApdexCalculator::default();
839 assert_eq!(apdex.satisfied_threshold_ms, 100);
840 assert_eq!(apdex.tolerating_threshold_ms, 400);
841 }
842
843 #[test]
844 fn test_apdex_rating_fair() {
845 let mut apdex = ApdexCalculator::new(100, 400);
846 for _ in 0..7 {
849 apdex.record(50); }
851 apdex.record(200); apdex.record(500); apdex.record(600); assert_eq!(apdex.rating(), ApdexRating::Fair);
856 }
857
858 #[test]
859 fn test_apdex_rating_excellent() {
860 let mut apdex = ApdexCalculator::new(100, 400);
861 for _ in 0..10 {
863 apdex.record(50); }
865 assert_eq!(apdex.rating(), ApdexRating::Excellent);
866 }
867
868 #[test]
869 fn test_apdex_empty_score() {
870 let apdex = ApdexCalculator::new(100, 400);
871 assert_eq!(apdex.score(), 1.0);
873 }
874
875 #[test]
876 fn test_apdex_rating_all_variants() {
877 assert_eq!(ApdexRating::Excellent.as_str(), "Excellent");
878 assert_eq!(ApdexRating::Good.as_str(), "Good");
879 assert_eq!(ApdexRating::Fair.as_str(), "Fair");
880 assert_eq!(ApdexRating::Poor.as_str(), "Poor");
881 assert_eq!(ApdexRating::Unacceptable.as_str(), "Unacceptable");
882 }
883
884 #[test]
885 fn test_knee_detector_insufficient_points() {
886 let mut detector = KneeDetector::new();
887 detector.add_point(10.0, 50.0);
888 detector.add_point(20.0, 60.0);
889 detector.detect();
891 assert!(detector.knee_point.is_none());
892 }
893
894 #[test]
895 fn test_knee_detector_default() {
896 let detector = KneeDetector::default();
897 assert!(detector.knee_point.is_none());
898 assert!(detector.recommended_capacity.is_none());
899 }
900
901 #[test]
902 fn test_calculate_variance_empty() {
903 let empty: Vec<f64> = vec![];
904 assert_eq!(calculate_variance(&empty), 0.0);
905 }
906
907 #[test]
908 fn test_calculate_variance_single() {
909 let single = vec![5.0];
910 assert_eq!(calculate_variance(&single), 0.0);
911 }
912
913 #[test]
914 fn test_quantile_regression_default() {
915 let qr = QuantileRegression::default();
916 assert!(qr.attributions.is_empty());
917 }
918
919 #[test]
920 fn test_variance_component_with_child_percentages() {
921 let mut parent = VarianceComponent::new("parent", 100.0);
922 let mut child1 = VarianceComponent::new("child1", 60.0);
923 child1.percentage = 60.0;
924 let mut child2 = VarianceComponent::new("child2", 40.0);
925 child2.percentage = 40.0;
926 parent.add_child(child1);
927 parent.add_child(child2);
928
929 parent.recalculate_percentages(200.0);
931
932 assert_eq!(parent.children[0].percentage, 30.0); assert_eq!(parent.children[1].percentage, 20.0); }
936
937 #[test]
938 fn test_statistical_analysis_all_fields() {
939 let mut analysis = StatisticalAnalysis::new("Full Test");
940
941 analysis
943 .variance_tree
944 .add_component(VarianceComponent::new("A", 100.0));
945 analysis.variance_tree.recalculate_percentages();
946
947 analysis.apdex.record(50);
949 analysis.apdex.record(200);
950
951 analysis.knee_detector.add_point(10.0, 50.0);
953 analysis.knee_detector.add_point(20.0, 55.0);
954 analysis.knee_detector.add_point(30.0, 80.0);
955 analysis.knee_detector.detect();
956
957 analysis
959 .quantile_regression
960 .add_attribution(TailAttribution::new(95, 200, "Network latency"));
961
962 let report = render_statistical_report(&analysis);
964 assert!(report.contains("Full Test"));
965 assert!(report.contains("VARIANCE"));
966 assert!(report.contains("APDEX"));
967 }
968
969 #[test]
970 fn test_tail_attribution_multiple_factors() {
971 let mut attr = TailAttribution::new(99, 500, "High tail latency");
972 attr.add_factor("GC pause", 0.45);
973 attr.add_factor("Network", 0.30);
974 attr.add_factor("Disk I/O", 0.25);
975
976 assert_eq!(attr.contributing_factors.len(), 3);
977 assert_eq!(attr.contributing_factors[0].0, "GC pause");
978 assert!((attr.contributing_factors[0].1 - 0.45).abs() < 0.01);
979 }
980}