1pub mod compute_budget;
6pub mod cpi_tree;
7pub mod scope_markers;
8pub mod solana_logs;
9
10pub use cpi_tree::{CallNode, NodeStatus};
11pub use scope_markers::{AttributionMethod, MarkerKind, ScopeResult};
12pub use solana_logs::{LexResult, LogEvent};
13
14use crate::program_registry::ProgramRegistry;
15
16#[derive(Debug, Clone)]
18pub struct ParseAnalysis {
19 pub call_tree: CallNode,
21 pub total_cu: u64,
23 pub cpi_count: u32,
25 pub cpi_depth: u32,
27 pub requested_limit: Option<u64>,
29 pub over_requested: Option<u64>,
31 pub unattributed_pct: f64,
33 pub scopes: Vec<ScopeResult>,
35 pub scope_marker_count: usize,
37 pub simulation_success: bool,
39 pub log_line_count: usize,
41 pub validation_after_cpi: bool,
44 pub warnings: Vec<String>,
46 pub logs_complete: bool,
48}
49
50#[must_use]
52pub fn analyze(logs: &[String], registry: &ProgramRegistry) -> ParseAnalysis {
53 let lexed = solana_logs::lex(logs);
54 let events: Vec<LogEvent> = lexed.events().cloned().collect();
55 let mut warnings = lexed.warnings.clone();
56
57 let call_tree = cpi_tree::build(&events, registry);
58 let total_cu = sum_units_at(&call_tree, 1);
59 let cpi_attributed = sum_units_below(&call_tree, 2);
60 let cpi_count = call_tree.cpi_count();
61 let cpi_depth = call_tree.max_depth().saturating_sub(1);
62
63 let requested_limit = compute_budget::estimated_requested_limit(&events);
64 let over_requested = requested_limit.and_then(|lim| lim.checked_sub(total_cu));
65
66 let unattributed = total_cu.saturating_sub(cpi_attributed);
67 let unattributed_pct = if total_cu == 0 {
68 0.0
69 } else {
70 (unattributed as f64 / total_cu as f64) * 100.0
71 };
72
73 let (scopes, scope_marker_count, scope_warnings) = collect_scopes(&events, total_cu);
74 warnings.extend(scope_warnings);
75
76 let simulation_success = !events.iter().any(|e| matches!(e, LogEvent::Failed { .. }));
77
78 let log_line_count = events
79 .iter()
80 .filter(|e| matches!(e, LogEvent::Log { .. }))
81 .count();
82 let validation_after_cpi = detect_validation_after_cpi(&events);
83
84 let logs_complete = warnings.is_empty();
85
86 ParseAnalysis {
87 call_tree,
88 total_cu,
89 cpi_count,
90 cpi_depth,
91 requested_limit,
92 over_requested,
93 unattributed_pct,
94 scopes,
95 scope_marker_count,
96 simulation_success,
97 log_line_count,
98 validation_after_cpi,
99 warnings,
100 logs_complete,
101 }
102}
103
104fn detect_validation_after_cpi(events: &[LogEvent]) -> bool {
108 let mut cpi_seen = false;
109 for e in events {
110 match e {
111 LogEvent::Invoke { depth, .. } if *depth >= 2 => cpi_seen = true,
112 LogEvent::ScopeBegin { name, .. } if cpi_seen => {
113 let lower = name.to_ascii_lowercase();
114 if lower.contains("valid") || lower.contains("check") {
115 return true;
116 }
117 }
118 _ => {}
119 }
120 }
121 false
122}
123
124fn sum_units_at(node: &CallNode, depth: u32) -> u64 {
125 let here = if node.depth == depth {
126 node.units_consumed.unwrap_or(0)
127 } else {
128 0
129 };
130 here + node
131 .children
132 .iter()
133 .map(|c| sum_units_at(c, depth))
134 .sum::<u64>()
135}
136
137fn sum_units_below(node: &CallNode, min_depth: u32) -> u64 {
138 let here = if node.depth >= min_depth {
139 node.units_consumed.unwrap_or(0)
140 } else {
141 0
142 };
143 here + node
144 .children
145 .iter()
146 .map(|c| sum_units_below(c, min_depth))
147 .sum::<u64>()
148}
149
150fn collect_scopes(events: &[LogEvent], total_cu: u64) -> (Vec<ScopeResult>, usize, Vec<String>) {
151 use scope_markers::{AttributionMethod, Marker, MarkerKind};
152
153 let mut markers: Vec<Marker> = Vec::new();
154 for e in events {
155 match e {
156 LogEvent::ScopeBegin { name, cu } => markers.push(Marker {
157 kind: MarkerKind::Begin,
158 name: name.clone(),
159 cu: *cu,
160 }),
161 LogEvent::ScopeEnd { name, cu } => markers.push(Marker {
162 kind: MarkerKind::End,
163 name: name.clone(),
164 cu: *cu,
165 }),
166 LogEvent::ScopePoint { name, cu } => markers.push(Marker {
167 kind: MarkerKind::Point,
168 name: name.clone(),
169 cu: *cu,
170 }),
171 _ => {}
172 }
173 }
174 let mut warnings = scope_markers::balance_warnings(&markers);
175
176 let mut stack: Vec<(usize, Option<u64>)> = Vec::new();
181 let mut scopes: Vec<ScopeResult> = Vec::new();
182 for m in &markers {
183 match m.kind {
184 MarkerKind::Begin => {
185 let parent = stack.last().map(|&(i, _)| scopes[i].name.clone());
186 stack.push((scopes.len(), m.cu));
187 scopes.push(ScopeResult {
188 name: m.name.clone(),
189 parent,
190 units_estimated: None,
191 percentage_of_total: None,
192 attribution_method: AttributionMethod::Estimated,
193 warnings: Vec::new(),
194 });
195 }
196 MarkerKind::End => {
197 let matches_top = stack.last().is_some_and(|&(i, _)| scopes[i].name == m.name);
198 if matches_top {
199 if let Some((idx, begin_cu)) = stack.pop() {
200 if let (Some(begin), Some(end)) = (begin_cu, m.cu) {
201 let units = begin.saturating_sub(end);
202 scopes[idx].units_estimated = Some(units);
203 scopes[idx].attribution_method = AttributionMethod::LogDelta;
204 if total_cu > 0 && units <= total_cu {
209 scopes[idx].percentage_of_total =
210 Some((units as f64 / total_cu as f64) * 100.0);
211 } else if units > total_cu {
212 let warning = format!(
213 "scope `{}` CU delta ({units}) exceeds the measured total ({total_cu}); percentage withheld",
214 scopes[idx].name
215 );
216 scopes[idx].warnings.push(warning.clone());
217 warnings.push(warning);
218 }
219 }
220 }
221 }
222 }
223 MarkerKind::Point => {}
224 }
225 }
226
227 (scopes, markers.len(), warnings)
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 fn lines(raw: &[&str]) -> Vec<String> {
235 raw.iter().map(|s| (*s).to_string()).collect()
236 }
237
238 #[test]
239 fn computes_totals_and_unattributed() {
240 let logs = lines(&[
241 "Program User111 invoke [1]",
242 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]",
243 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 3000 of 197000 compute units",
244 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
245 "Program User111 consumed 12000 of 200000 compute units",
246 "Program User111 success",
247 ]);
248 let a = analyze(&logs, &ProgramRegistry::with_builtins());
249 assert_eq!(a.total_cu, 12_000);
250 assert_eq!(a.cpi_count, 1);
251 assert_eq!(a.cpi_depth, 1);
252 assert_eq!(a.requested_limit, Some(200_000));
253 assert_eq!(a.over_requested, Some(188_000));
254 assert!((a.unattributed_pct - 75.0).abs() < 0.01);
256 assert!(a.simulation_success);
257 }
258
259 #[test]
260 fn scopes_inferred_with_nesting() {
261 let logs = lines(&[
262 "Program User111 invoke [1]",
263 "Program log: CU_PROFILER_BEGIN name=outer",
264 "Program log: CU_PROFILER_BEGIN name=inner",
265 "Program log: CU_PROFILER_END name=inner",
266 "Program log: CU_PROFILER_END name=outer",
267 "Program User111 consumed 5000 of 200000 compute units",
268 "Program User111 success",
269 ]);
270 let a = analyze(&logs, &ProgramRegistry::with_builtins());
271 assert_eq!(a.scope_marker_count, 4);
272 assert_eq!(a.scopes.len(), 2);
273 assert_eq!(a.scopes[1].parent.as_deref(), Some("outer"));
274 assert!(a.warnings.is_empty());
275 }
276
277 #[test]
278 fn estimates_scope_cu_from_compute_snapshots() {
279 let logs = lines(&[
280 "Program User111 invoke [1]",
281 "Program log: CU_PROFILER_BEGIN name=swap::validate cu=200000",
282 "Program log: CU_PROFILER_END name=swap::validate cu=188000",
283 "Program User111 consumed 24000 of 200000 compute units",
284 "Program User111 success",
285 ]);
286 let a = analyze(&logs, &ProgramRegistry::with_builtins());
287 let scope = &a.scopes[0];
288 assert_eq!(scope.name, "swap::validate");
289 assert_eq!(scope.units_estimated, Some(12_000));
291 assert_eq!(scope.attribution_method, AttributionMethod::LogDelta);
292 assert!((scope.percentage_of_total.unwrap() - 50.0).abs() < 0.01);
294 }
295
296 #[test]
297 fn contradictory_snapshot_withholds_percentage_and_warns() {
298 let logs = lines(&[
301 "Program User111 invoke [1]",
302 "Program log: CU_PROFILER_BEGIN name=swap::math cu=200000",
303 "Program log: CU_PROFILER_END name=swap::math cu=188000",
304 "Program User111 consumed 5000 of 200000 compute units",
305 "Program User111 success",
306 ]);
307 let a = analyze(&logs, &ProgramRegistry::with_builtins());
308 assert_eq!(a.scopes[0].units_estimated, Some(12_000));
309 assert_eq!(a.scopes[0].percentage_of_total, None);
310 assert!(
311 a.warnings
312 .iter()
313 .any(|w| w.contains("exceeds the measured total"))
314 );
315 }
316
317 #[test]
318 fn scope_without_snapshots_stays_unquantified() {
319 let logs = lines(&[
320 "Program User111 invoke [1]",
321 "Program log: CU_PROFILER_BEGIN name=swap::validate",
322 "Program log: CU_PROFILER_END name=swap::validate",
323 "Program User111 consumed 24000 of 200000 compute units",
324 "Program User111 success",
325 ]);
326 let a = analyze(&logs, &ProgramRegistry::with_builtins());
327 assert_eq!(a.scopes[0].units_estimated, None);
328 assert_eq!(a.scopes[0].attribution_method, AttributionMethod::Estimated);
329 }
330
331 #[test]
332 fn detects_log_volume_and_late_validation() {
333 let logs = lines(&[
334 "Program User111 invoke [1]",
335 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]",
336 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
337 "Program log: CU_PROFILER_BEGIN name=swap::validate_accounts",
339 "Program log: CU_PROFILER_END name=swap::validate_accounts",
340 "Program log: emitting event one",
341 "Program log: emitting event two",
342 "Program User111 consumed 9000 of 200000 compute units",
343 "Program User111 success",
344 ]);
345 let a = analyze(&logs, &ProgramRegistry::with_builtins());
346 assert!(a.validation_after_cpi);
347 assert_eq!(a.log_line_count, 2); }
349
350 #[test]
351 fn validation_before_cpi_is_not_flagged() {
352 let logs = lines(&[
353 "Program User111 invoke [1]",
354 "Program log: CU_PROFILER_BEGIN name=swap::validate_accounts",
355 "Program log: CU_PROFILER_END name=swap::validate_accounts",
356 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]",
357 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
358 "Program User111 consumed 9000 of 200000 compute units",
359 "Program User111 success",
360 ]);
361 let a = analyze(&logs, &ProgramRegistry::with_builtins());
362 assert!(!a.validation_after_cpi);
363 }
364
365 #[test]
366 fn failure_path_flips_simulation_success() {
367 let logs = lines(&[
368 "Program User111 invoke [1]",
369 "Program User111 consumed 8000 of 200000 compute units",
370 "Program User111 failed: custom program error: 0x1",
371 ]);
372 let a = analyze(&logs, &ProgramRegistry::with_builtins());
373 assert!(!a.simulation_success);
374 assert_eq!(a.total_cu, 8000);
375 }
376}