Skip to main content

thruput_native_exec/
rv32_stack_usage.rs

1//! The rv32 guest stack-usage diagnostic the test FFI records per invocation.
2
3use core::ffi::c_void;
4use core::ops::Range;
5use thruput_sys::FFIErrorCode;
6use thruput_xdr::types::ContractAddress;
7
8/// The value [`Rv32StackUsage::sp_initial`] and [`Rv32StackUsage::sp_min`]
9/// hold when the guest never wrote `sp`.
10pub const SP_UNWRITTEN: u32 = u32::MAX;
11
12/// What an rv32 guest did with its stack pointer over one invocation.
13///
14/// hogd records one of these per rv32 invocation, when the invocation ends,
15/// in Debug builds with `HOGD_RV32_STACK_USAGE` on -- which the prebuilt
16/// test_ffi library this crate links is. `sp_initial` is the first value the
17/// guest stored to `sp` (x2): the stack top its `_start` established.
18/// `sp_min` is the lowest, the deepest point the stack reached, so
19/// [`bytes_used`](Self::bytes_used) is their difference. A fused
20/// `lui sp; addi sp, sp, imm` prologue stores only the constant it
21/// materializes, so `sp_initial` is the top the program established, not the
22/// `lui` intermediate.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct Rv32StackUsage {
25    /// The invoked contract.
26    pub contract_address: ContractAddress,
27    /// The invoked method id.
28    pub method_id: u64,
29    /// First value the guest wrote to `sp`, or [`SP_UNWRITTEN`].
30    pub sp_initial: u32,
31    /// Lowest value the guest wrote to `sp`, or [`SP_UNWRITTEN`].
32    pub sp_min: u32,
33}
34
35impl Rv32StackUsage {
36    /// True if the guest wrote `sp` at all.
37    pub fn wrote_sp(&self) -> bool {
38        self.sp_initial != SP_UNWRITTEN
39    }
40
41    /// Bytes of stack the invocation used: `sp_initial - sp_min`. `None` if
42    /// the guest never wrote `sp`.
43    pub fn bytes_used(&self) -> Option<u32> {
44        self.wrote_sp()
45            .then(|| self.sp_initial.saturating_sub(self.sp_min))
46    }
47
48    fn from_ffi(record: &thruput_sys::FFIRv32StackUsage) -> Self {
49        Self {
50            contract_address: record.contract_address.into(),
51            method_id: record.method_id,
52            sp_initial: record.sp_initial,
53            sp_min: record.sp_min,
54        }
55    }
56}
57
58/// Decode the packed `FFIRv32StackUsage` array `get_rv32_stack_usage`
59/// returns, in the order the invocations finished.
60fn decode_records(bytes: &[u8]) -> Vec<Rv32StackUsage> {
61    let size = size_of::<thruput_sys::FFIRv32StackUsage>();
62    assert!(
63        bytes.len().is_multiple_of(size),
64        "rv32 stack usage buffer of {} bytes is not a whole number of \
65         {size}-byte records",
66        bytes.len()
67    );
68    bytes
69        .chunks_exact(size)
70        .map(|chunk| {
71            // The buffer is a C array of the struct, so every chunk is one
72            // complete record; read_unaligned makes no alignment assumption
73            // about the allocation.
74            let record: thruput_sys::FFIRv32StackUsage = unsafe {
75                core::ptr::read_unaligned(
76                    chunk.as_ptr() as *const thruput_sys::FFIRv32StackUsage
77                )
78            };
79            Rv32StackUsage::from_ffi(&record)
80        })
81        .collect()
82}
83
84/// Every record on `handler`, in the order the invocations finished: a
85/// callee's precedes its caller's. Empty if no rv32 invocation has finished
86/// on the handler, or if the library was built without the diagnostic.
87///
88/// The handler appends to this list for the life of its transaction and
89/// never removes from it, so the length a frame saw when it opened is where
90/// the records of the calls made under it begin; see [`frame_records`].
91pub(crate) fn records(handler: *mut c_void) -> Vec<Rv32StackUsage> {
92    // `records` frees itself on drop (see thruput_sys::FFIDataBuffer).
93    let res = unsafe { thruput_sys::get_rv32_stack_usage(handler) };
94    let err: FFIErrorCode = res.ffi_error_code.into();
95    if err != FFIErrorCode::None {
96        panic!("failed to read rv32 stack usage: {err}");
97    }
98    res.records.as_slice().map_or_else(Vec::new, decode_records)
99}
100
101/// The records a frame owns, out of the handler's `records`: those at the
102/// indices in `window` (appended while the frame was open) less those in any
103/// of `child_windows` (appended while one of its child frames was open; each
104/// lies inside `window`). Order is kept, so the last is the frame's most
105/// recent call.
106pub(crate) fn frame_records(
107    records: &[Rv32StackUsage],
108    window: Range<usize>,
109    child_windows: &[Range<usize>],
110) -> Vec<Rv32StackUsage> {
111    let start = window.start;
112    records
113        .get(window)
114        .unwrap_or(&[])
115        .iter()
116        .enumerate()
117        .filter(|(offset, _)| {
118            let index = start + offset;
119            !child_windows.iter().any(|child| child.contains(&index))
120        })
121        .map(|(_, record)| *record)
122        .collect()
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    fn ffi_record(
130        address_byte: u8,
131        method_id: u64,
132        sp_initial: u32,
133        sp_min: u32,
134    ) -> thruput_sys::FFIRv32StackUsage {
135        thruput_sys::FFIRv32StackUsage {
136            contract_address: [address_byte; 32],
137            method_id,
138            sp_initial,
139            sp_min,
140        }
141    }
142
143    fn as_bytes(records: &[thruput_sys::FFIRv32StackUsage]) -> &[u8] {
144        unsafe {
145            core::slice::from_raw_parts(
146                records.as_ptr() as *const u8,
147                size_of_val(records),
148            )
149        }
150    }
151
152    #[test]
153    fn decodes_records_in_order_and_computes_usage() {
154        let records = [
155            ffi_record(0xA1, 7, 0x10000, 0x10000 - 96),
156            ffi_record(0xB2, 9, SP_UNWRITTEN, SP_UNWRITTEN),
157        ];
158        let decoded = decode_records(as_bytes(&records));
159        assert_eq!(decoded.len(), 2);
160
161        let first = decoded[0];
162        assert_eq!(first.contract_address, [0xA1u8; 32].into());
163        assert_eq!(first.method_id, 7);
164        assert_eq!(first.sp_initial, 0x10000);
165        assert_eq!(first.sp_min, 0x10000 - 96);
166        assert!(first.wrote_sp());
167        assert_eq!(first.bytes_used(), Some(96));
168
169        let second = decoded[1];
170        assert_eq!(second.method_id, 9);
171        assert!(!second.wrote_sp());
172        assert_eq!(second.bytes_used(), None);
173    }
174
175    #[test]
176    fn empty_buffer_decodes_to_nothing() {
177        assert!(decode_records(&[]).is_empty());
178    }
179
180    #[test]
181    #[should_panic(expected = "not a whole number")]
182    fn partial_record_is_rejected() {
183        let records = [ffi_record(0, 0, 0, 0)];
184        let bytes = as_bytes(&records);
185        let _ = decode_records(&bytes[..bytes.len() - 1]);
186    }
187
188    /// A transaction's records numbered by finish order, so a frame's
189    /// selection reads as a list of indices.
190    fn numbered(count: u64) -> Vec<Rv32StackUsage> {
191        (0..count)
192            .map(|n| Rv32StackUsage {
193                contract_address: [n as u8; 32].into(),
194                method_id: n,
195                sp_initial: 0x1000,
196                sp_min: 0x0f00,
197            })
198            .collect()
199    }
200
201    fn method_ids(records: &[Rv32StackUsage]) -> Vec<u64> {
202        records.iter().map(|record| record.method_id).collect()
203    }
204
205    #[test]
206    fn frame_records_are_the_window_less_the_children() {
207        // 0 and 1 finished before the frame opened; 2 is its own; 3 and 4
208        // ran under its first child; 5 is its own; 6 ran under its second
209        // child; 7 finished after it closed.
210        let records = numbered(8);
211        let own = frame_records(&records, 2..7, &[3..5, 6..7]);
212        assert_eq!(method_ids(&own), [2, 5]);
213        assert_eq!(own.last().map(|record| record.method_id), Some(5));
214    }
215
216    #[test]
217    fn frame_records_without_children_is_the_whole_window() {
218        let records = numbered(4);
219        assert_eq!(method_ids(&frame_records(&records, 1..3, &[])), [1, 2]);
220    }
221
222    #[test]
223    fn frame_that_ran_no_rv32_code_has_no_records() {
224        let records = numbered(3);
225        // Opened after every record was appended.
226        assert!(frame_records(&records, 3..3, &[]).is_empty());
227        // Every record in its window was its child's.
228        assert!(frame_records(&records, 1..3, &[1..3]).is_empty());
229        // Nothing ran at all.
230        assert!(frame_records(&[], 0..0, &[]).is_empty());
231    }
232}