Skip to main content

cu_profiler_core/budget/
policy.rs

1//! Declarative budget policy. Every field is optional; an absent field means
2//! "no opinion", so policies compose by merging defaults with per-scenario
3//! overrides.
4
5use serde::{Deserialize, Serialize};
6
7/// A budget policy attached to a scenario (or used as a project default).
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
9#[serde(default)]
10pub struct BudgetPolicy {
11    /// Absolute maximum compute units. Exceeding this is a hard `Fail`.
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub absolute_max_cu: Option<u64>,
14    /// Warn once consumption reaches this percentage of `absolute_max_cu`.
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub warn_at_budget_pct: Option<f64>,
17    /// Maximum tolerated regression versus baseline, as a percentage.
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub max_regression_pct: Option<f64>,
20    /// Maximum tolerated regression versus baseline, in absolute units.
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub max_regression_units: Option<u64>,
23    /// Minimum required margin below `absolute_max_cu`, as a percentage.
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub min_margin_pct: Option<f64>,
26    /// Maximum number of CPI invocations.
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub max_cpi_count: Option<u32>,
29    /// Maximum CPI invoke depth.
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub max_cpi_depth: Option<u32>,
32    /// Maximum percentage of CU left unattributed to a scope before warning.
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub max_unattributed_pct: Option<f64>,
35    /// Warn when instrumentation overhead exceeds this percentage.
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub instrumentation_overhead_warn_pct: Option<f64>,
38}
39
40impl BudgetPolicy {
41    /// Overlay `override_with` onto `self`: any field set in the override wins.
42    #[must_use]
43    pub fn merged_with(&self, override_with: &BudgetPolicy) -> BudgetPolicy {
44        BudgetPolicy {
45            absolute_max_cu: override_with.absolute_max_cu.or(self.absolute_max_cu),
46            warn_at_budget_pct: override_with.warn_at_budget_pct.or(self.warn_at_budget_pct),
47            max_regression_pct: override_with.max_regression_pct.or(self.max_regression_pct),
48            max_regression_units: override_with
49                .max_regression_units
50                .or(self.max_regression_units),
51            min_margin_pct: override_with.min_margin_pct.or(self.min_margin_pct),
52            max_cpi_count: override_with.max_cpi_count.or(self.max_cpi_count),
53            max_cpi_depth: override_with.max_cpi_depth.or(self.max_cpi_depth),
54            max_unattributed_pct: override_with
55                .max_unattributed_pct
56                .or(self.max_unattributed_pct),
57            instrumentation_overhead_warn_pct: override_with
58                .instrumentation_overhead_warn_pct
59                .or(self.instrumentation_overhead_warn_pct),
60        }
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn override_wins_but_keeps_base_fields() {
70        let base = BudgetPolicy {
71            absolute_max_cu: Some(100_000),
72            max_regression_pct: Some(5.0),
73            ..Default::default()
74        };
75        let over = BudgetPolicy {
76            max_regression_pct: Some(3.0),
77            ..Default::default()
78        };
79        let merged = base.merged_with(&over);
80        assert_eq!(merged.absolute_max_cu, Some(100_000));
81        assert_eq!(merged.max_regression_pct, Some(3.0));
82    }
83}