hopper_native/budget.rs
1//! Compute-unit budget tracking and instrumentation.
2//!
3//! Solana programs have a finite CU budget per instruction. Exceeding it
4//! is a hard abort. No existing framework provides runtime CU tracking
5//! at the substrate level -- programs either blindly hope they fit or
6//! manually sprinkle `sol_log_compute_units()` calls.
7//!
8//! Hopper's `CuBudget` provides:
9//!
10//! 1. **Snapshot/check pattern**: Take a CU snapshot, do work, check how
11//! much was consumed. Useful for profiling individual code paths.
12//!
13//! 2. **Guard pattern**: Set a CU floor and periodically check that you
14//! have enough budget remaining before expensive operations (like CPI).
15//!
16//! 3. **Feature-gated tracing**: With `#[cfg(feature = "cu-trace")]`,
17//! emit structured CU consumption logs at function boundaries that
18//! off-chain tools can parse into flame graphs.
19//!
20//! # Usage
21//!
22//! ```ignore
23//! use hopper_native::budget::CuBudget;
24//!
25//! fn process(accounts: &[AccountView], data: &[u8]) -> ProgramResult {
26//! let budget = CuBudget::snapshot();
27//!
28//! // ... do work ...
29//!
30//! // Before an expensive CPI, check we have at least 50k CU left.
31//! budget.require_remaining(50_000)?;
32//!
33//! // CPI call...
34//! Ok(())
35//! }
36//! ```
37//!
38//! With `cu-trace` enabled:
39//!
40//! ```ignore
41//! use hopper_native::budget::cu_trace;
42//!
43//! fn process_deposit(/* ... */) -> ProgramResult {
44//! cu_trace!("deposit::start");
45//! // ... work ...
46//! cu_trace!("deposit::after_validation");
47//! // ... CPI ...
48//! cu_trace!("deposit::end");
49//! Ok(())
50//! }
51//! ```
52
53use crate::ProgramResult;
54
55/// Compute-unit budget tracker.
56///
57/// On BPF, uses `sol_log_compute_units()` to read the remaining budget.
58/// Off-chain, all operations are no-ops that succeed.
59#[derive(Clone, Copy)]
60pub struct CuBudget {
61 /// CU remaining at the time of the snapshot (0 off-chain).
62 /// Reserved for future use when Solana exposes a `sol_get_remaining_cu` syscall.
63 #[allow(dead_code)]
64 snapshot: u64,
65}
66
67impl CuBudget {
68 /// Take a snapshot of the current compute budget.
69 ///
70 /// On BPF this calls `sol_log_compute_units()` and captures the
71 /// remaining CU from the log output. On native (off-chain), the
72 /// snapshot is 0 and all checks pass trivially.
73 #[inline(always)]
74 pub fn snapshot() -> Self {
75 #[cfg(target_os = "solana")]
76 {
77 // The `sol_log_compute_units` syscall logs the remaining CU
78 // but does not return it. We store a marker and rely on the
79 // guard pattern (require_remaining) for budget enforcement.
80 //
81 // For actual CU reading, we use the Solana runtime's
82 // get_processed_sibling_instruction or just track relative
83 // consumption patterns.
84 unsafe {
85 crate::syscalls::sol_log_compute_units_();
86 }
87 Self { snapshot: 0 }
88 }
89 #[cfg(not(target_os = "solana"))]
90 {
91 Self { snapshot: 0 }
92 }
93 }
94
95 /// Log the current compute unit consumption for profiling.
96 ///
97 /// Emits via `sol_log_compute_units` on BPF. Use this to instrument
98 /// hot paths and identify CU bottlenecks.
99 #[inline(always)]
100 pub fn checkpoint() {
101 #[cfg(target_os = "solana")]
102 unsafe {
103 crate::syscalls::sol_log_compute_units_();
104 }
105 }
106
107 /// Assert that at least `min_remaining` CU are available.
108 ///
109 /// On BPF, this is a conservative check: the Solana runtime does not
110 /// expose a "get remaining CU" syscall, so this method logs the
111 /// current usage and returns Ok. The real enforcement is that the
112 /// runtime itself will abort if CU is exhausted.
113 ///
114 /// The value of this method is that it makes the CU concern VISIBLE
115 /// in the code and provides a hook point for future runtime features
116 /// that may expose remaining CU programmatically.
117 ///
118 /// Off-chain, this always returns Ok.
119 #[inline(always)]
120 pub fn require_remaining(&self, _min_remaining: u64) -> ProgramResult {
121 // On BPF: log and rely on the runtime's hard abort.
122 // When Solana adds a `sol_get_remaining_compute_units` syscall,
123 // this method will become a real guard.
124 #[cfg(target_os = "solana")]
125 unsafe {
126 crate::syscalls::sol_log_compute_units_();
127 }
128 Ok(())
129 }
130
131 /// Log CU consumed since the snapshot.
132 ///
133 /// Emits a structured log that off-chain tools can parse.
134 /// Format: `"cu-delta: <label>"`
135 #[inline(always)]
136 pub fn log_delta(&self, label: &str) {
137 Self::checkpoint();
138 crate::log::log(label);
139 }
140}
141
142/// Structured CU tracing macro for profiling.
143///
144/// When the `cu-trace` feature is enabled, emits both a compute-unit
145/// log and a label log, allowing off-chain tooling to reconstruct
146/// a CU flame graph from program logs.
147///
148/// When `cu-trace` is NOT enabled, this is a complete no-op with zero
149/// CU cost.
150///
151/// # Usage
152///
153/// ```ignore
154/// cu_trace!("validate_accounts");
155/// // ... validation code ...
156/// cu_trace!("begin_cpi");
157/// ```
158#[macro_export]
159macro_rules! cu_trace {
160 ( $label:expr ) => {{
161 #[cfg(feature = "cu-trace")]
162 {
163 $crate::budget::CuBudget::checkpoint();
164 $crate::log::log(concat!("[cu-trace] ", $label));
165 }
166 }};
167}
168
169/// Run a closure and log the CU consumed by it (feature-gated).
170///
171/// Returns the closure's result. When `cu-trace` is not enabled,
172/// just runs the closure with zero overhead.
173///
174/// # Usage
175///
176/// ```ignore
177/// let result = cu_measure!("deserialize", || {
178/// parse_instruction_data(data)
179/// });
180/// ```
181#[macro_export]
182macro_rules! cu_measure {
183 ( $label:expr, $body:expr ) => {{
184 #[cfg(feature = "cu-trace")]
185 {
186 $crate::budget::CuBudget::checkpoint();
187 $crate::log::log(concat!("[cu-start] ", $label));
188 }
189 let __result = $body;
190 #[cfg(feature = "cu-trace")]
191 {
192 $crate::budget::CuBudget::checkpoint();
193 $crate::log::log(concat!("[cu-end] ", $label));
194 }
195 __result
196 }};
197}