use core::ffi::c_void;
use core::ops::Range;
use thruput_sys::FFIErrorCode;
use thruput_xdr::types::ContractAddress;
pub const SP_UNWRITTEN: u32 = u32::MAX;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rv32StackUsage {
pub contract_address: ContractAddress,
pub method_id: u64,
pub sp_initial: u32,
pub sp_min: u32,
}
impl Rv32StackUsage {
pub fn wrote_sp(&self) -> bool {
self.sp_initial != SP_UNWRITTEN
}
pub fn bytes_used(&self) -> Option<u32> {
self.wrote_sp()
.then(|| self.sp_initial.saturating_sub(self.sp_min))
}
fn from_ffi(record: &thruput_sys::FFIRv32StackUsage) -> Self {
Self {
contract_address: record.contract_address.into(),
method_id: record.method_id,
sp_initial: record.sp_initial,
sp_min: record.sp_min,
}
}
}
fn decode_records(bytes: &[u8]) -> Vec<Rv32StackUsage> {
let size = size_of::<thruput_sys::FFIRv32StackUsage>();
assert!(
bytes.len().is_multiple_of(size),
"rv32 stack usage buffer of {} bytes is not a whole number of \
{size}-byte records",
bytes.len()
);
bytes
.chunks_exact(size)
.map(|chunk| {
let record: thruput_sys::FFIRv32StackUsage = unsafe {
core::ptr::read_unaligned(
chunk.as_ptr() as *const thruput_sys::FFIRv32StackUsage
)
};
Rv32StackUsage::from_ffi(&record)
})
.collect()
}
pub(crate) fn records(handler: *mut c_void) -> Vec<Rv32StackUsage> {
let res = unsafe { thruput_sys::get_rv32_stack_usage(handler) };
let err: FFIErrorCode = res.ffi_error_code.into();
if err != FFIErrorCode::None {
panic!("failed to read rv32 stack usage: {err}");
}
res.records.as_slice().map_or_else(Vec::new, decode_records)
}
pub(crate) fn frame_records(
records: &[Rv32StackUsage],
window: Range<usize>,
child_windows: &[Range<usize>],
) -> Vec<Rv32StackUsage> {
let start = window.start;
records
.get(window)
.unwrap_or(&[])
.iter()
.enumerate()
.filter(|(offset, _)| {
let index = start + offset;
!child_windows.iter().any(|child| child.contains(&index))
})
.map(|(_, record)| *record)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn ffi_record(
address_byte: u8,
method_id: u64,
sp_initial: u32,
sp_min: u32,
) -> thruput_sys::FFIRv32StackUsage {
thruput_sys::FFIRv32StackUsage {
contract_address: [address_byte; 32],
method_id,
sp_initial,
sp_min,
}
}
fn as_bytes(records: &[thruput_sys::FFIRv32StackUsage]) -> &[u8] {
unsafe {
core::slice::from_raw_parts(
records.as_ptr() as *const u8,
size_of_val(records),
)
}
}
#[test]
fn decodes_records_in_order_and_computes_usage() {
let records = [
ffi_record(0xA1, 7, 0x10000, 0x10000 - 96),
ffi_record(0xB2, 9, SP_UNWRITTEN, SP_UNWRITTEN),
];
let decoded = decode_records(as_bytes(&records));
assert_eq!(decoded.len(), 2);
let first = decoded[0];
assert_eq!(first.contract_address, [0xA1u8; 32].into());
assert_eq!(first.method_id, 7);
assert_eq!(first.sp_initial, 0x10000);
assert_eq!(first.sp_min, 0x10000 - 96);
assert!(first.wrote_sp());
assert_eq!(first.bytes_used(), Some(96));
let second = decoded[1];
assert_eq!(second.method_id, 9);
assert!(!second.wrote_sp());
assert_eq!(second.bytes_used(), None);
}
#[test]
fn empty_buffer_decodes_to_nothing() {
assert!(decode_records(&[]).is_empty());
}
#[test]
#[should_panic(expected = "not a whole number")]
fn partial_record_is_rejected() {
let records = [ffi_record(0, 0, 0, 0)];
let bytes = as_bytes(&records);
let _ = decode_records(&bytes[..bytes.len() - 1]);
}
fn numbered(count: u64) -> Vec<Rv32StackUsage> {
(0..count)
.map(|n| Rv32StackUsage {
contract_address: [n as u8; 32].into(),
method_id: n,
sp_initial: 0x1000,
sp_min: 0x0f00,
})
.collect()
}
fn method_ids(records: &[Rv32StackUsage]) -> Vec<u64> {
records.iter().map(|record| record.method_id).collect()
}
#[test]
fn frame_records_are_the_window_less_the_children() {
let records = numbered(8);
let own = frame_records(&records, 2..7, &[3..5, 6..7]);
assert_eq!(method_ids(&own), [2, 5]);
assert_eq!(own.last().map(|record| record.method_id), Some(5));
}
#[test]
fn frame_records_without_children_is_the_whole_window() {
let records = numbered(4);
assert_eq!(method_ids(&frame_records(&records, 1..3, &[])), [1, 2]);
}
#[test]
fn frame_that_ran_no_rv32_code_has_no_records() {
let records = numbered(3);
assert!(frame_records(&records, 3..3, &[]).is_empty());
assert!(frame_records(&records, 1..3, &[1..3]).is_empty());
assert!(frame_records(&[], 0..0, &[]).is_empty());
}
}