1use crate::backend::ExecutionBackend;
6use crate::baseline::{BaselineComparison, BaselineStore, Fingerprint};
7use crate::budget::{self, PolicyResult};
8use crate::confidence::{self, Confidence, ConfidenceFactors};
9use crate::diagnostics::{self, Context};
10use crate::metadata::RunMetadata;
11use crate::model::{InstructionMeasurement, Measurement, Report, ScenarioReport, Status};
12use crate::parser::{self, ParseAnalysis};
13use crate::program_registry::ProgramRegistry;
14use crate::scenario::{ExpectedResult, Scenario};
15
16#[derive(Debug, Clone)]
18pub struct Profiler {
19 registry: ProgramRegistry,
20 config_repr: String,
21 include_raw_logs: bool,
22}
23
24impl Default for Profiler {
25 fn default() -> Self {
26 Self {
27 registry: ProgramRegistry::with_builtins(),
28 config_repr: String::new(),
29 include_raw_logs: false,
30 }
31 }
32}
33
34impl Profiler {
35 #[must_use]
37 pub fn new() -> Self {
38 Self::default()
39 }
40
41 #[must_use]
43 pub fn with_registry(mut self, registry: ProgramRegistry) -> Self {
44 self.registry = registry;
45 self
46 }
47
48 #[must_use]
51 pub fn with_config_repr(mut self, repr: impl Into<String>) -> Self {
52 self.config_repr = repr.into();
53 self
54 }
55
56 #[must_use]
58 pub fn include_raw_logs(mut self, yes: bool) -> Self {
59 self.include_raw_logs = yes;
60 self
61 }
62
63 #[must_use]
65 pub fn fingerprint(&self, scenario: &Scenario) -> Fingerprint {
66 Fingerprint::new(
67 &format!("{scenario:?}"),
68 &scenario.name,
69 &self.config_repr,
70 None,
71 )
72 }
73
74 #[must_use]
77 pub fn run(
78 &self,
79 backend: &dyn ExecutionBackend,
80 scenarios: &[Scenario],
81 baseline: Option<&BaselineStore>,
82 metadata: RunMetadata,
83 ) -> Report {
84 let reports = scenarios
85 .iter()
86 .map(|s| self.profile_one(backend, s, baseline))
87 .collect();
88 Report::new(reports, metadata)
89 }
90
91 fn profile_one(
92 &self,
93 backend: &dyn ExecutionBackend,
94 scenario: &Scenario,
95 baseline: Option<&BaselineStore>,
96 ) -> ScenarioReport {
97 match backend.run(scenario) {
98 Ok(output) => {
99 let analysis = parser::analyze(&output.logs, &self.registry);
100 self.assemble(scenario, analysis, output.success, output.logs, baseline)
101 }
102 Err(e) => self.simulation_error_report(scenario, &e.to_string()),
103 }
104 }
105
106 fn assemble(
107 &self,
108 scenario: &Scenario,
109 analysis: ParseAnalysis,
110 sim_success: bool,
111 logs: Vec<String>,
112 baseline: Option<&BaselineStore>,
113 ) -> ScenarioReport {
114 let per_instruction: Vec<InstructionMeasurement> = analysis
116 .call_tree
117 .children
118 .iter()
119 .enumerate()
120 .map(|(index, node)| InstructionMeasurement {
121 index,
122 program_id: node.program_id.clone(),
123 label: node.label.clone(),
124 consumed: node.units_consumed,
125 })
126 .collect();
127
128 let measurement = Measurement {
129 total_cu: analysis.total_cu,
130 consumed: analysis.total_cu,
131 requested_limit: analysis.requested_limit,
132 over_requested: analysis.over_requested,
133 cpi_count: analysis.cpi_count,
134 cpi_depth: analysis.cpi_depth,
135 unattributed_pct: analysis.unattributed_pct,
136 instrumentation_overhead_pct: None,
137 per_instruction,
138 simulation_success: sim_success && analysis.simulation_success,
139 };
140
141 let current_fp = self.fingerprint(scenario);
143 let comparison = baseline
144 .and_then(|store| store.get(&scenario.name))
145 .map(|record| {
146 BaselineComparison::compute(
147 record.actual_units,
148 &record.fingerprint,
149 &measurement,
150 ¤t_fp,
151 )
152 });
153 let baseline_units = comparison
154 .as_ref()
155 .filter(|c| c.matched)
156 .map(|c| c.baseline_units);
157
158 let policy_results: Vec<PolicyResult> =
160 budget::evaluate(&measurement, &scenario.budget, baseline_units);
161
162 let confidence = self.score_confidence(&analysis, comparison.as_ref());
164
165 let status = self.derive_status(&measurement, &policy_results, scenario.expected);
167
168 let ctx = Context {
170 scenario: &scenario.name,
171 measurement: &measurement,
172 policy_results: &policy_results,
173 baseline: comparison.as_ref(),
174 confidence: &confidence,
175 expected: scenario.expected,
176 scope_count: analysis.scope_marker_count,
177 log_line_count: analysis.log_line_count,
178 late_validation: analysis.validation_after_cpi,
179 };
180 let diags = diagnostics::evaluate(&ctx);
181
182 ScenarioReport {
183 name: scenario.name.clone(),
184 status,
185 measurement,
186 call_tree: Some(analysis.call_tree),
187 scopes: analysis.scopes,
188 policy_results,
189 diagnostics: diags,
190 confidence,
191 baseline_comparison: comparison,
192 parser_warnings: analysis.warnings,
193 raw_logs: self.include_raw_logs.then_some(logs),
194 }
195 }
196
197 fn score_confidence(
198 &self,
199 analysis: &ParseAnalysis,
200 comparison: Option<&BaselineComparison>,
201 ) -> Confidence {
202 let unattributed_pct = if analysis.scope_marker_count > 0 {
205 analysis.unattributed_pct
206 } else {
207 0.0
208 };
209 let factors = ConfidenceFactors {
210 simulation_ok: analysis.simulation_success,
211 logs_complete: analysis.logs_complete,
212 parser_warnings: analysis.warnings.len(),
213 baseline_matched: comparison.map(|c| c.matched),
214 unattributed_pct,
215 scope_markers: analysis.scope_marker_count,
216 metadata_available: true,
217 };
218 confidence::score(&factors)
219 }
220
221 fn derive_status(
222 &self,
223 measurement: &Measurement,
224 policy_results: &[PolicyResult],
225 expected: ExpectedResult,
226 ) -> Status {
227 let outcome_ok = match expected {
229 ExpectedResult::Success => measurement.simulation_success,
230 ExpectedResult::Failure => !measurement.simulation_success,
231 };
232 if !outcome_ok {
233 return Status::Fail;
234 }
235 Status::from_policy(budget::overall_status(policy_results))
236 }
237
238 fn simulation_error_report(&self, scenario: &Scenario, error: &str) -> ScenarioReport {
239 ScenarioReport {
240 name: scenario.name.clone(),
241 status: Status::Unknown,
242 measurement: Measurement {
243 simulation_success: false,
244 ..Measurement::empty()
245 },
246 call_tree: None,
247 scopes: Vec::new(),
248 policy_results: Vec::new(),
249 diagnostics: Vec::new(),
250 confidence: Confidence::unknown(format!("simulation error: {error}")),
251 baseline_comparison: None,
252 parser_warnings: vec![format!("simulation error: {error}")],
253 raw_logs: None,
254 }
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use crate::backend::RecordedLogsBackend;
262 use crate::budget::BudgetPolicy;
263
264 fn backend() -> RecordedLogsBackend {
265 let mut b = RecordedLogsBackend::new();
266 b.insert_blob(
267 "swap",
268 "Program User111 invoke [1]\n\
269 Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]\n\
270 Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 3000 of 197000 compute units\n\
271 Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success\n\
272 Program User111 consumed 96000 of 200000 compute units\n\
273 Program User111 success",
274 true,
275 );
276 b
277 }
278
279 fn swap_scenario(max: u64) -> Scenario {
280 let mut s = Scenario::new("swap");
281 s.budget = BudgetPolicy {
282 absolute_max_cu: Some(max),
283 warn_at_budget_pct: Some(90.0),
284 ..Default::default()
285 };
286 s
287 }
288
289 #[test]
290 fn end_to_end_pass() {
291 let report = Profiler::new().run(
292 &backend(),
293 &[swap_scenario(200_000)],
294 None,
295 RunMetadata::recorded("0.1.0"),
296 );
297 assert_eq!(report.scenarios[0].status, Status::Pass);
298 assert_eq!(report.scenarios[0].measurement.total_cu, 96_000);
299 assert_eq!(
300 report.scenarios[0].confidence.level,
301 confidence::ConfidenceLevel::High
302 );
303
304 let per = &report.scenarios[0].measurement.per_instruction;
306 assert_eq!(per.len(), 1);
307 assert_eq!(per[0].index, 0);
308 assert_eq!(per[0].program_id, "User111");
309 assert_eq!(per[0].consumed, Some(96_000));
310 }
311
312 #[test]
313 fn end_to_end_warn_near_budget() {
314 let report = Profiler::new().run(
315 &backend(),
316 &[swap_scenario(100_000)],
317 None,
318 RunMetadata::recorded("0.1.0"),
319 );
320 assert_eq!(report.scenarios[0].status, Status::Warn);
321 assert!(
322 report.scenarios[0]
323 .diagnostics
324 .iter()
325 .any(|d| d.id == "near_budget_limit")
326 );
327 }
328
329 #[test]
330 fn missing_scenario_yields_unknown() {
331 let report = Profiler::new().run(
332 &RecordedLogsBackend::new(),
333 &[Scenario::new("ghost")],
334 None,
335 RunMetadata::recorded("0.1.0"),
336 );
337 assert_eq!(report.scenarios[0].status, Status::Unknown);
338 assert!(report.has_failures());
339 }
340}