scirs2_metrics/serialization/
comparison.rs

1//! Metric comparison utilities
2//!
3//! This module provides tools for comparing metric results between runs.
4
5use chrono::Duration;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use super::{MetricCollection, MetricResult};
10use crate::error::Result;
11
12/// Comparison result between metric values
13///
14/// This struct represents the result of comparing two metric values.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct MetricComparison {
17    /// Name of the metric
18    pub name: String,
19    /// First value
20    pub value1: f64,
21    /// Second value
22    pub value2: f64,
23    /// Absolute difference (value2 - value1)
24    pub absolute_diff: f64,
25    /// Relative difference ((value2 - value1) / value1)
26    pub relative_diff: f64,
27    /// Whether the difference is significant
28    pub is_significant: bool,
29    /// Optional threshold used for significance determination
30    pub threshold: Option<f64>,
31}
32
33/// Collection comparison result
34///
35/// This struct represents the result of comparing two metric collections.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct CollectionComparison {
38    /// Name of the first collection
39    pub name1: String,
40    /// Name of the second collection
41    pub name2: String,
42    /// Comparison results for each metric
43    pub metric_comparisons: Vec<MetricComparison>,
44    /// Summary statistics
45    pub summary: ComparisonSummary,
46}
47
48/// Summary statistics for a collection comparison
49///
50/// This struct represents summary statistics for a collection comparison.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ComparisonSummary {
53    /// Number of metrics compared
54    pub total_metrics: usize,
55    /// Number of metrics that improved
56    pub improved: usize,
57    /// Number of metrics that degraded
58    pub degraded: usize,
59    /// Number of metrics that remained unchanged
60    pub unchanged: usize,
61    /// Number of metrics only in the first collection
62    pub only_in_first: usize,
63    /// Number of metrics only in the second collection
64    pub only_in_second: usize,
65    /// Average absolute difference
66    pub avg_absolute_diff: f64,
67    /// Average relative difference
68    pub avg_relative_diff: f64,
69}
70
71/// Compare two metric results
72///
73/// # Arguments
74///
75/// * `metric1` - First metric result
76/// * `metric2` - Second metric result
77/// * `threshold` - Optional threshold for significance determination
78///
79/// # Returns
80///
81/// * A MetricComparison
82#[allow(dead_code)]
83pub fn compare_metrics(
84    metric1: &MetricResult,
85    metric2: &MetricResult,
86    threshold: Option<f64>,
87) -> MetricComparison {
88    let value1 = metric1.value;
89    let value2 = metric2.value;
90
91    let absolute_diff = value2 - value1;
92    let relative_diff = if value1 != 0.0 {
93        absolute_diff / value1
94    } else if value2 == 0.0 {
95        0.0
96    } else {
97        1.0
98    };
99
100    let is_significant = if let Some(t) = threshold {
101        relative_diff.abs() > t
102    } else {
103        false
104    };
105
106    MetricComparison {
107        name: metric1.name.clone(),
108        value1,
109        value2,
110        absolute_diff,
111        relative_diff,
112        is_significant,
113        threshold,
114    }
115}
116
117/// Compare two metric collections
118///
119/// # Arguments
120///
121/// * `collection1` - First collection
122/// * `collection2` - Second collection
123/// * `threshold` - Optional threshold for significance determination
124///
125/// # Returns
126///
127/// * A CollectionComparison
128#[allow(dead_code)]
129pub fn compare_collections(
130    collection1: &MetricCollection,
131    collection2: &MetricCollection,
132    threshold: Option<f64>,
133) -> CollectionComparison {
134    // Create maps for faster lookup
135    let mut metrics1_map = HashMap::new();
136    for metric in &collection1.metrics {
137        metrics1_map.insert(metric.name.clone(), metric);
138    }
139
140    let mut metrics2_map = HashMap::new();
141    for metric in &collection2.metrics {
142        metrics2_map.insert(metric.name.clone(), metric);
143    }
144
145    // Compare metrics that exist in both collections
146    let mut metric_comparisons = Vec::new();
147
148    let mut improved = 0;
149    let mut degraded = 0;
150    let mut unchanged = 0;
151    let mut only_in_first = 0;
152    let mut only_in_second = 0;
153
154    let mut total_absolute_diff = 0.0;
155    let mut total_relative_diff = 0.0;
156
157    // Process all metrics from collection1
158    for metric1 in &collection1.metrics {
159        if let Some(metric2) = metrics2_map.get(&metric1.name) {
160            // Metric exists in both collections
161            let comparison = compare_metrics(metric1, metric2, threshold);
162
163            // Update counters
164            if comparison.absolute_diff > 0.0 {
165                improved += 1;
166            } else if comparison.absolute_diff < 0.0 {
167                degraded += 1;
168            } else {
169                unchanged += 1;
170            }
171
172            total_absolute_diff += comparison.absolute_diff.abs();
173            total_relative_diff += comparison.relative_diff.abs();
174
175            metric_comparisons.push(comparison);
176        } else {
177            // Metric only in collection1
178            only_in_first += 1;
179        }
180    }
181
182    // Find metrics only in collection2
183    for metric2 in &collection2.metrics {
184        if !metrics1_map.contains_key(&metric2.name) {
185            only_in_second += 1;
186        }
187    }
188
189    // Calculate averages
190    let compared_count = metric_comparisons.len();
191    let avg_absolute_diff = if compared_count > 0 {
192        total_absolute_diff / compared_count as f64
193    } else {
194        0.0
195    };
196
197    let avg_relative_diff = if compared_count > 0 {
198        total_relative_diff / compared_count as f64
199    } else {
200        0.0
201    };
202
203    // Create summary
204    let summary = ComparisonSummary {
205        total_metrics: compared_count,
206        improved,
207        degraded,
208        unchanged,
209        only_in_first,
210        only_in_second,
211        avg_absolute_diff,
212        avg_relative_diff,
213    };
214
215    CollectionComparison {
216        name1: collection1.name.clone(),
217        name2: collection2.name.clone(),
218        metric_comparisons,
219        summary,
220    }
221}
222
223/// Find metrics that differ significantly between collections
224///
225/// # Arguments
226///
227/// * `collection1` - First collection
228/// * `collection2` - Second collection
229/// * `threshold` - Threshold for significance determination
230///
231/// # Returns
232///
233/// * Vector of significantly different metrics
234#[allow(dead_code)]
235pub fn find_significant_differences(
236    collection1: &MetricCollection,
237    collection2: &MetricCollection,
238    threshold: f64,
239) -> Vec<MetricComparison> {
240    let comparison = compare_collections(collection1, collection2, Some(threshold));
241
242    comparison
243        .metric_comparisons
244        .into_iter()
245        .filter(|comp| comp.is_significant)
246        .collect()
247}
248
249/// Combine multiple metric collections into one
250///
251/// # Arguments
252///
253/// * `collections` - Collections to combine
254/// * `name` - Name for the combined collection
255/// * `description` - Optional description for the combined collection
256///
257/// # Returns
258///
259/// * A combined MetricCollection
260#[allow(dead_code)]
261pub fn combine_collections(
262    collections: &[MetricCollection],
263    name: &str,
264    description: Option<&str>,
265) -> MetricCollection {
266    let mut combined = MetricCollection::new(name, description);
267
268    for collection in collections {
269        for metric in &collection.metrics {
270            // Create a new metric with original metadata plus collection source
271            let mut metadata = metric.metadata.clone().unwrap_or_default();
272
273            if metadata.additional_metadata.is_none() {
274                metadata.additional_metadata = Some(HashMap::new());
275            }
276
277            if let Some(ref mut additional) = metadata.additional_metadata {
278                additional.insert("source_collection".to_string(), collection.name.clone());
279                additional.insert(
280                    "source_timestamp".to_string(),
281                    collection.created_at.to_string(),
282                );
283            }
284
285            let new_metric = MetricResult {
286                name: metric.name.clone(),
287                value: metric.value,
288                additional_values: metric.additional_values.clone(),
289                timestamp: metric.timestamp,
290                metadata: Some(metadata),
291            };
292
293            combined.add_metric(new_metric);
294        }
295    }
296
297    combined
298}
299
300/// Get metrics from a collection by filtering
301///
302/// # Arguments
303///
304/// * `collection` - Collection to filter
305/// * `filter_fn` - Function to filter metrics
306///
307/// # Returns
308///
309/// * A new collection with filtered metrics
310#[allow(dead_code)]
311pub fn filter_metrics<F>(_collection: &MetricCollection, filterfn: F) -> MetricCollection
312where
313    F: Fn(&MetricResult) -> bool,
314{
315    let mut filtered = MetricCollection::new(
316        &format!("{} (filtered)", _collection.name),
317        _collection.description.as_deref(),
318    );
319
320    for metric in &_collection.metrics {
321        if filterfn(metric) {
322            filtered.add_metric(metric.clone());
323        }
324    }
325
326    filtered
327}
328
329/// Get metrics from a collection by name pattern
330///
331/// # Arguments
332///
333/// * `collection` - Collection to filter
334/// * `pattern` - Pattern to match metric names against
335///
336/// # Returns
337///
338/// * A new collection with filtered metrics
339#[allow(dead_code)]
340pub fn filter_by_name(collection: &MetricCollection, pattern: &str) -> MetricCollection {
341    filter_metrics(collection, |metric| metric.name.contains(pattern))
342}
343
344/// Get metrics from a collection by time range
345///
346/// # Arguments
347///
348/// * `collection` - Collection to filter
349/// * `start_age` - Oldest age to include (Duration from now)
350/// * `end_age` - Newest age to include (Duration from now)
351///
352/// # Returns
353///
354/// * A new collection with filtered metrics
355#[allow(dead_code)]
356pub fn filter_by_time_range(
357    collection: &MetricCollection,
358    start_age: Duration,
359    end_age: Duration,
360) -> Result<MetricCollection> {
361    let now = chrono::Utc::now();
362
363    Ok(filter_metrics(collection, |metric| {
364        let _age = now - metric.timestamp;
365        _age >= end_age && _age <= start_age
366    }))
367}