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 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
85 unsafe {
86 crate::syscalls::sol_log_compute_units_();
87 }
88 Self { snapshot: 0 }
89 }
90 #[cfg(not(target_os = "solana"))]
91 {
92 Self { snapshot: 0 }
93 }
94 }
95
96 /// Log the current compute unit consumption for profiling.
97 ///
98 /// Emits via `sol_log_compute_units` on BPF. Use this to instrument
99 /// hot paths and identify CU bottlenecks.
100 #[inline(always)]
101 pub fn checkpoint() {
102 #[cfg(target_os = "solana")]
103 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
104 unsafe {
105 crate::syscalls::sol_log_compute_units_();
106 }
107 }
108
109 /// Assert that at least `min_remaining` CU are available.
110 ///
111 /// On BPF, this is a conservative check: the Solana runtime does not
112 /// expose a "get remaining CU" syscall, so this method logs the
113 /// current usage and returns Ok. The real enforcement is that the
114 /// runtime itself will abort if CU is exhausted.
115 ///
116 /// The value of this method is that it makes the CU concern VISIBLE
117 /// in the code and provides a hook point for future runtime features
118 /// that may expose remaining CU programmatically.
119 ///
120 /// Off-chain, this always returns Ok.
121 #[inline(always)]
122 pub fn require_remaining(&self, _min_remaining: u64) -> ProgramResult {
123 // On BPF: log and rely on the runtime's hard abort.
124 // When Solana adds a `sol_get_remaining_compute_units` syscall,
125 // this method will become a real guard.
126 #[cfg(target_os = "solana")]
127 // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
128 unsafe {
129 crate::syscalls::sol_log_compute_units_();
130 }
131 Ok(())
132 }
133
134 /// Log CU consumed since the snapshot.
135 ///
136 /// Emits a structured log that off-chain tools can parse.
137 /// Format: `"cu-delta: <label>"`
138 #[inline(always)]
139 pub fn log_delta(&self, label: &str) {
140 Self::checkpoint();
141 crate::log::log(label);
142 }
143}
144
145/// Structured CU tracing macro for profiling.
146///
147/// When the `cu-trace` feature is enabled, emits both a compute-unit
148/// log and a label log, allowing off-chain tooling to reconstruct
149/// a CU flame graph from program logs.
150///
151/// When `cu-trace` is NOT enabled, this is a complete no-op with zero
152/// CU cost.
153///
154/// # Usage
155///
156/// ```ignore
157/// cu_trace!("validate_accounts");
158/// // ... validation code ...
159/// cu_trace!("begin_cpi");
160/// ```
161#[macro_export]
162macro_rules! cu_trace {
163 ( $label:expr ) => {{
164 #[cfg(feature = "cu-trace")]
165 {
166 $crate::budget::CuBudget::checkpoint();
167 $crate::log::log(concat!("[cu-trace] ", $label));
168 }
169 }};
170}
171
172/// Run a closure and log the CU consumed by it (feature-gated).
173///
174/// Returns the closure's result. When `cu-trace` is not enabled,
175/// just runs the closure with zero overhead.
176///
177/// # Usage
178///
179/// ```ignore
180/// let result = cu_measure!("deserialize", || {
181/// parse_instruction_data(data)
182/// });
183/// ```
184#[macro_export]
185macro_rules! cu_measure {
186 ( $label:expr, $body:expr ) => {{
187 #[cfg(feature = "cu-trace")]
188 {
189 $crate::budget::CuBudget::checkpoint();
190 $crate::log::log(concat!("[cu-start] ", $label));
191 }
192 let __result = $body;
193 #[cfg(feature = "cu-trace")]
194 {
195 $crate::budget::CuBudget::checkpoint();
196 $crate::log::log(concat!("[cu-end] ", $label));
197 }
198 __result
199 }};
200}