Skip to main content

cu_profiler_core/baseline/
compare.rs

1//! Comparing a current measurement against a stored baseline record.
2
3use serde::{Deserialize, Serialize};
4
5use crate::baseline::fingerprint::Fingerprint;
6use crate::model::Measurement;
7
8/// The result of comparing a current run to a baseline.
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10pub struct BaselineComparison {
11    /// Whether the fingerprints matched (a non-stale comparison).
12    pub matched: bool,
13    /// Reasons the baseline is stale (empty when `matched`).
14    #[serde(default, skip_serializing_if = "Vec::is_empty")]
15    pub stale_reasons: Vec<String>,
16    /// CU recorded in the baseline.
17    pub baseline_units: u64,
18    /// CU measured in the current run.
19    pub current_units: u64,
20    /// Signed delta (`current - baseline`).
21    pub delta_units: i64,
22    /// Percentage delta relative to the baseline.
23    pub delta_pct: f64,
24}
25
26impl BaselineComparison {
27    /// Compute a comparison from a baseline figure + fingerprint and the current
28    /// measurement + fingerprint.
29    #[must_use]
30    pub fn compute(
31        baseline_units: u64,
32        baseline_fp: &Fingerprint,
33        current: &Measurement,
34        current_fp: &Fingerprint,
35    ) -> Self {
36        let stale_reasons = baseline_fp.staleness_reasons(current_fp);
37        let current_units = current.total_cu;
38        let delta_units = current_units as i64 - baseline_units as i64;
39        let delta_pct = if baseline_units == 0 {
40            0.0
41        } else {
42            (delta_units as f64 / baseline_units as f64) * 100.0
43        };
44        Self {
45            matched: stale_reasons.is_empty(),
46            stale_reasons,
47            baseline_units,
48            current_units,
49            delta_units,
50            delta_pct,
51        }
52    }
53
54    /// A one-line human summary, e.g. `+6.15% (+5608 CU) vs baseline`.
55    #[must_use]
56    pub fn summary(&self) -> String {
57        format!(
58            "{:+.2}% ({:+} CU) vs baseline {}",
59            self.delta_pct, self.delta_units, self.baseline_units
60        )
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    fn fp(fixture: &str) -> Fingerprint {
69        Fingerprint::new("s", fixture, "cfg", None)
70    }
71
72    fn measurement(cu: u64) -> Measurement {
73        Measurement {
74            total_cu: cu,
75            ..Measurement::empty()
76        }
77    }
78
79    #[test]
80    fn matched_comparison_reports_delta() {
81        let c = BaselineComparison::compute(91_204, &fp("a"), &measurement(96_812), &fp("a"));
82        assert!(c.matched);
83        assert_eq!(c.delta_units, 5_608);
84        assert!((c.delta_pct - 6.15).abs() < 0.01);
85    }
86
87    #[test]
88    fn changed_fixture_is_stale() {
89        let c = BaselineComparison::compute(91_204, &fp("a"), &measurement(96_812), &fp("b"));
90        assert!(!c.matched);
91        assert!(c.stale_reasons.iter().any(|r| r.contains("fixture hash")));
92    }
93}