Skip to main content

cu_profiler_core/
model.rs

1//! The report data model: the raw, serializable result of a profiling run.
2//!
3//! This module owns *data*. Rendering to table/JSON/Markdown/JUnit lives in the
4//! `cu-profiler-report` crate, keeping raw data and presentation separate.
5
6use serde::{Deserialize, Serialize};
7
8use crate::baseline::BaselineComparison;
9use crate::budget::PolicyResult;
10use crate::confidence::Confidence;
11use crate::diagnostics::Diagnostic;
12use crate::metadata::RunMetadata;
13use crate::parser::{CallNode, ScopeResult};
14
15/// Headline pass/warn/fail/unknown status of a scenario.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "lowercase")]
18pub enum Status {
19    /// All policies satisfied.
20    Pass,
21    /// A soft threshold tripped.
22    Warn,
23    /// A hard limit breached, or an unexpected outcome.
24    Fail,
25    /// Not enough information to judge.
26    Unknown,
27}
28
29impl Status {
30    /// Uppercase label used in tables and JUnit.
31    #[must_use]
32    pub fn label(self) -> &'static str {
33        match self {
34            Self::Pass => "PASS",
35            Self::Warn => "WARN",
36            Self::Fail => "FAIL",
37            Self::Unknown => "UNKNOWN",
38        }
39    }
40
41    /// Derive a status from a rolled-up budget [`crate::budget::PolicyStatus`].
42    #[must_use]
43    pub fn from_policy(p: crate::budget::PolicyStatus) -> Self {
44        match p {
45            crate::budget::PolicyStatus::Pass => Self::Pass,
46            crate::budget::PolicyStatus::Warn => Self::Warn,
47            crate::budget::PolicyStatus::Fail => Self::Fail,
48        }
49    }
50}
51
52/// Compute attributed to a single instruction within a transaction.
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54pub struct InstructionMeasurement {
55    /// Zero-based index in the transaction.
56    pub index: usize,
57    /// Program that owns the instruction.
58    pub program_id: String,
59    /// Resolved label, if known.
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub label: Option<String>,
62    /// CU consumed, if the logs reported it.
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub consumed: Option<u64>,
65}
66
67/// The quantitative core of a scenario result.
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69pub struct Measurement {
70    /// Total CU consumed across the transaction.
71    pub total_cu: u64,
72    /// CU actually consumed (equals `total_cu` for single-tx scenarios).
73    pub consumed: u64,
74    /// Compute-budget requested limit, if a `SetComputeUnitLimit` was present.
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub requested_limit: Option<u64>,
77    /// CU requested but unused (`requested_limit - consumed`), if known.
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub over_requested: Option<u64>,
80    /// Number of CPI invocations observed.
81    pub cpi_count: u32,
82    /// Maximum CPI invoke depth observed.
83    pub cpi_depth: u32,
84    /// Percentage of total CU not attributed to any scope (0..=100).
85    pub unattributed_pct: f64,
86    /// Instrumentation overhead as a percentage of total CU, if estimable.
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub instrumentation_overhead_pct: Option<f64>,
89    /// Per-instruction breakdown.
90    #[serde(default, skip_serializing_if = "Vec::is_empty")]
91    pub per_instruction: Vec<InstructionMeasurement>,
92    /// Whether the simulation completed successfully.
93    pub simulation_success: bool,
94}
95
96impl Measurement {
97    /// A zeroed measurement (successful, no compute) — a useful base for tests
98    /// and for `..Measurement::empty()` struct-update syntax.
99    #[must_use]
100    pub fn empty() -> Self {
101        Self {
102            total_cu: 0,
103            consumed: 0,
104            requested_limit: None,
105            over_requested: None,
106            cpi_count: 0,
107            cpi_depth: 0,
108            unattributed_pct: 0.0,
109            instrumentation_overhead_pct: None,
110            per_instruction: Vec::new(),
111            simulation_success: true,
112        }
113    }
114}
115
116/// The full result for one scenario.
117#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
118pub struct ScenarioReport {
119    /// Scenario name.
120    pub name: String,
121    /// Headline status.
122    pub status: Status,
123    /// Quantitative measurement.
124    pub measurement: Measurement,
125    /// Reconstructed CPI call tree, if logs allowed it.
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub call_tree: Option<CallNode>,
128    /// Scope attribution results.
129    #[serde(default, skip_serializing_if = "Vec::is_empty")]
130    pub scopes: Vec<ScopeResult>,
131    /// Budget policy results.
132    #[serde(default, skip_serializing_if = "Vec::is_empty")]
133    pub policy_results: Vec<PolicyResult>,
134    /// Diagnostics raised for this scenario.
135    #[serde(default, skip_serializing_if = "Vec::is_empty")]
136    pub diagnostics: Vec<Diagnostic>,
137    /// Confidence in this measurement.
138    pub confidence: Confidence,
139    /// Baseline comparison, if a baseline was available.
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub baseline_comparison: Option<BaselineComparison>,
142    /// Non-fatal parser warnings.
143    #[serde(default, skip_serializing_if = "Vec::is_empty")]
144    pub parser_warnings: Vec<String>,
145    /// Raw logs, included only when the caller opts in (they can be large).
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub raw_logs: Option<Vec<String>>,
148}
149
150/// Aggregate counts across all scenarios.
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
152pub struct Summary {
153    /// Number of scenarios profiled.
154    pub total_scenarios: usize,
155    /// How many passed.
156    pub passed: usize,
157    /// How many warned.
158    pub warned: usize,
159    /// How many failed.
160    pub failed: usize,
161    /// Sum of total CU across scenarios.
162    pub total_cu: u64,
163}
164
165/// The complete profiling report.
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
167pub struct Report {
168    /// Aggregate summary.
169    pub summary: Summary,
170    /// Per-scenario results.
171    pub scenarios: Vec<ScenarioReport>,
172    /// Run metadata.
173    pub metadata: RunMetadata,
174}
175
176impl Report {
177    /// Assemble a report from scenario results, computing the summary.
178    #[must_use]
179    pub fn new(scenarios: Vec<ScenarioReport>, metadata: RunMetadata) -> Self {
180        let mut summary = Summary {
181            total_scenarios: scenarios.len(),
182            passed: 0,
183            warned: 0,
184            failed: 0,
185            total_cu: 0,
186        };
187        for s in &scenarios {
188            summary.total_cu = summary.total_cu.saturating_add(s.measurement.total_cu);
189            match s.status {
190                Status::Pass => summary.passed += 1,
191                Status::Warn => summary.warned += 1,
192                Status::Fail | Status::Unknown => summary.failed += 1,
193            }
194        }
195        Self {
196            summary,
197            scenarios,
198            metadata,
199        }
200    }
201
202    /// Did any scenario fail?
203    #[must_use]
204    pub fn has_failures(&self) -> bool {
205        self.summary.failed > 0
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212    use crate::confidence::Confidence;
213    use crate::metadata::RunMetadata;
214
215    fn scenario(name: &str, status: Status, cu: u64) -> ScenarioReport {
216        ScenarioReport {
217            name: name.into(),
218            status,
219            measurement: Measurement {
220                total_cu: cu,
221                ..Measurement::empty()
222            },
223            call_tree: None,
224            scopes: Vec::new(),
225            policy_results: Vec::new(),
226            diagnostics: Vec::new(),
227            confidence: Confidence::high(),
228            baseline_comparison: None,
229            parser_warnings: Vec::new(),
230            raw_logs: None,
231        }
232    }
233
234    #[test]
235    fn summary_counts_and_totals() {
236        let r = Report::new(
237            vec![
238                scenario("a", Status::Pass, 100),
239                scenario("b", Status::Warn, 200),
240                scenario("c", Status::Fail, 300),
241            ],
242            RunMetadata::recorded("0.1.0"),
243        );
244        assert_eq!(r.summary.passed, 1);
245        assert_eq!(r.summary.warned, 1);
246        assert_eq!(r.summary.failed, 1);
247        assert_eq!(r.summary.total_cu, 600);
248        assert!(r.has_failures());
249    }
250}