Skip to main content

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}