1use std::{
8 cell::RefCell,
9 sync::{Arc, Mutex, OnceLock},
10};
11
12use alloy_primitives::{Address, Bytes};
13
14fn stderr_trace_on() -> bool {
17 static ON: OnceLock<bool> = OnceLock::new();
18 *ON.get_or_init(|| std::env::var("STYLUS_HOSTIO_TRACE").is_ok())
19}
20
21#[derive(Debug, Clone)]
23pub struct HostioRecord {
24 pub name: &'static str,
25 pub args: Bytes,
26 pub outs: Bytes,
27 pub start_ink: u64,
28 pub end_ink: u64,
29 pub address: Option<Address>,
30 pub steps: Vec<HostioRecord>,
32}
33
34thread_local! {
41 static ACTIVE: RefCell<Option<Arc<Mutex<Vec<HostioRecord>>>>> = const { RefCell::new(None) };
42 static FRAMES: RefCell<Vec<Vec<HostioRecord>>> = const { RefCell::new(Vec::new()) };
46}
47
48pub fn enter_subcall() {
51 FRAMES.with(|f| f.borrow_mut().push(Vec::new()));
52}
53
54pub fn exit_subcall() -> Vec<HostioRecord> {
57 FRAMES.with(|f| f.borrow_mut().pop().unwrap_or_default())
58}
59
60pub fn enable(buf: Arc<Mutex<Vec<HostioRecord>>>) {
63 ACTIVE.with(|slot| *slot.borrow_mut() = Some(buf));
64}
65
66pub fn disable() {
68 ACTIVE.with(|slot| *slot.borrow_mut() = None);
69}
70
71pub fn take() -> Vec<HostioRecord> {
73 ACTIVE
74 .with(|slot| {
75 slot.borrow()
76 .as_ref()
77 .and_then(|b| b.lock().ok().map(|mut v| std::mem::take(&mut *v)))
78 })
79 .unwrap_or_default()
80}
81
82pub fn record(
85 name: &'static str,
86 args: Bytes,
87 outs: Bytes,
88 start_ink: u64,
89 end_ink: u64,
90 address: Option<Address>,
91) {
92 record_with_steps(name, args, outs, start_ink, end_ink, address, Vec::new());
93}
94
95pub fn record_with_steps(
99 name: &'static str,
100 args: Bytes,
101 outs: Bytes,
102 start_ink: u64,
103 end_ink: u64,
104 address: Option<Address>,
105 steps: Vec<HostioRecord>,
106) {
107 if stderr_trace_on() {
108 let delta = start_ink.saturating_sub(end_ink);
109 eprintln!(
110 "[hostio] {name} ink_delta={delta} start={start_ink} end={end_ink} args_len={} outs_len={}",
111 args.len(),
112 outs.len(),
113 );
114 }
115 let rec = HostioRecord {
116 name,
117 args,
118 outs,
119 start_ink,
120 end_ink,
121 address,
122 steps,
123 };
124 let leftover = FRAMES.with(|f| {
125 let mut frames = f.borrow_mut();
126 if let Some(top) = frames.last_mut() {
127 top.push(rec);
128 None
129 } else {
130 Some(rec)
131 }
132 });
133 if let Some(rec) = leftover {
134 ACTIVE.with(|slot| {
135 if let Some(buf) = slot.borrow().as_ref()
136 && let Ok(mut v) = buf.lock()
137 {
138 v.push(rec);
139 }
140 });
141 }
142}
143
144pub fn is_active() -> bool {
147 stderr_trace_on() || ACTIVE.with(|slot| slot.borrow().is_some())
148}
149
150#[inline]
154pub fn record_leaf(name: &'static str, args: Bytes, outs: Bytes) {
155 if is_active() {
156 record(name, args, outs, 0, 0, None);
157 }
158}
159
160#[inline]
163pub fn record_ink(name: &'static str, start_ink: u64, end_ink: u64) {
164 if is_active() {
165 record(name, Bytes::new(), Bytes::new(), start_ink, end_ink, None);
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn record_when_disabled_is_noop() {
175 disable();
176 assert!(!is_active());
177 record("x", Bytes::new(), Bytes::new(), 100, 50, None);
178 assert!(take().is_empty());
179 }
180
181 #[test]
182 fn enable_captures_records() {
183 let buf = Arc::new(Mutex::new(Vec::new()));
184 enable(buf.clone());
185 assert!(is_active());
186 record(
187 "storage_load_bytes32",
188 Bytes::from(vec![1]),
189 Bytes::from(vec![2]),
190 100,
191 90,
192 None,
193 );
194 record(
195 "contract_call",
196 Bytes::new(),
197 Bytes::new(),
198 90,
199 40,
200 Some(Address::repeat_byte(0xAA)),
201 );
202 let records = take();
203 assert_eq!(records.len(), 2);
204 assert_eq!(records[0].name, "storage_load_bytes32");
205 assert_eq!(records[1].address, Some(Address::repeat_byte(0xAA)));
206 disable();
207 }
208
209 #[test]
210 fn subcall_frame_nests_records() {
211 let buf = Arc::new(Mutex::new(Vec::new()));
212 enable(buf.clone());
213
214 record(
216 "storage_load_bytes32",
217 Bytes::new(),
218 Bytes::new(),
219 100,
220 90,
221 None,
222 );
223
224 enter_subcall();
226 record(
227 "storage_load_bytes32",
228 Bytes::new(),
229 Bytes::new(),
230 80,
231 70,
232 None,
233 );
234 record("emit_log", Bytes::new(), Bytes::new(), 70, 60, None);
235 let steps = exit_subcall();
236 assert_eq!(steps.len(), 2);
237 record_with_steps(
238 "call_contract",
239 Bytes::new(),
240 Bytes::new(),
241 85,
242 55,
243 Some(Address::repeat_byte(0xCC)),
244 steps,
245 );
246
247 let records = take();
248 assert_eq!(records.len(), 2);
249 assert_eq!(records[0].name, "storage_load_bytes32");
250 assert_eq!(records[0].steps.len(), 0);
251 assert_eq!(records[1].name, "call_contract");
252 assert_eq!(records[1].steps.len(), 2);
253 assert_eq!(records[1].steps[0].name, "storage_load_bytes32");
254 assert_eq!(records[1].steps[1].name, "emit_log");
255 disable();
256 }
257
258 #[test]
259 fn nested_subcalls_compose() {
260 let buf = Arc::new(Mutex::new(Vec::new()));
261 enable(buf.clone());
262 enter_subcall();
263 record("a", Bytes::new(), Bytes::new(), 0, 0, None);
264 enter_subcall();
265 record("b", Bytes::new(), Bytes::new(), 0, 0, None);
266 record("c", Bytes::new(), Bytes::new(), 0, 0, None);
267 let inner = exit_subcall();
268 assert_eq!(inner.len(), 2);
269 record_with_steps("inner_call", Bytes::new(), Bytes::new(), 0, 0, None, inner);
270 record("d", Bytes::new(), Bytes::new(), 0, 0, None);
271 let outer = exit_subcall();
272 assert_eq!(outer.len(), 3);
273 assert_eq!(outer[1].name, "inner_call");
274 assert_eq!(outer[1].steps.len(), 2);
275 record_with_steps("outer_call", Bytes::new(), Bytes::new(), 0, 0, None, outer);
276 let records = take();
277 assert_eq!(records.len(), 1);
278 assert_eq!(records[0].name, "outer_call");
279 assert_eq!(records[0].steps.len(), 3);
280 disable();
281 }
282
283 #[test]
284 fn take_clears_buffer() {
285 let buf = Arc::new(Mutex::new(Vec::new()));
286 enable(buf);
287 record("foo", Bytes::new(), Bytes::new(), 10, 5, None);
288 assert_eq!(take().len(), 1);
289 assert_eq!(take().len(), 0);
290 disable();
291 }
292}