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            // 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}