Skip to content

Commit

Permalink
Alloc example (#972)
Browse files Browse the repository at this point in the history
* Add EnvBase::augment_err_result to help gather debug info.

* Charge linear memory growth delta to budget, not the total new size.

* Add an example that does allocation using the SDK's alloc function.

---------

Co-authored-by: Jay Geng <[email protected]>
  • Loading branch information
graydon and jayz22 authored Aug 3, 2023
1 parent c5607a2 commit 876fb16
Show file tree
Hide file tree
Showing 13 changed files with 127 additions and 11 deletions.
12 changes: 12 additions & 0 deletions soroban-env-common/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ pub trait EnvBase: Sized + Clone {
#[cfg(feature = "testutils")]
fn escalate_error_to_panic(&self, e: Self::Error) -> !;

/// If `x` is `Err(...)`, ensure as much debug information as possible is
/// attached to that error; in any case return "essentially the same" `x` --
/// either `Ok(...)` or `Err(...)` -- just with extra error context.
///
/// This is called on a best-effort basis while propagating errors in the
/// host, to attach context "as soon as possible", and is necessary because
/// some errors are generated in contexts that do not have access to a Host,
/// and so cannot attach error context at the site of error generation.
fn augment_err_result<T>(&self, x: Result<T, Self::Error>) -> Result<T, Self::Error> {
x
}

/// Used for recovering the concrete type of the Host.
fn as_mut_any(&mut self) -> &mut dyn any::Any;

Expand Down
2 changes: 1 addition & 1 deletion soroban-env-common/src/vmcaller_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ macro_rules! vmcaller_none_function_helper {
=>
{
fn $fn_id(&self, $($arg:$type),*) -> Result<$ret, Self::Error> {
<Self as VmCallerEnv>::$fn_id(self, &mut VmCaller::none(), $($arg),*)
self.augment_err_result(<Self as VmCallerEnv>::$fn_id(self, &mut VmCaller::none(), $($arg),*))
}
};
}
Expand Down
5 changes: 3 additions & 2 deletions soroban-env-host/src/budget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1044,7 +1044,7 @@ impl Default for BudgetImpl {
impl ResourceLimiter for Host {
fn memory_growing(
&mut self,
_current: usize,
current: usize,
desired: usize,
maximum: Option<usize>,
) -> Result<bool, errors::MemoryError> {
Expand All @@ -1063,8 +1063,9 @@ impl ResourceLimiter for Host {
};

if allow {
let delta = (desired as u64).saturating_sub(current as u64);
self.as_budget()
.bulk_charge(ContractCostType::WasmMemAlloc, desired as u64, None)
.bulk_charge(ContractCostType::WasmMemAlloc, delta, None)
.map(|_| true)
.map_err(|_| errors::MemoryError::OutOfBoundsGrowth)
} else {
Expand Down
9 changes: 9 additions & 0 deletions soroban-env-host/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,15 @@ impl EnvBase for Host {
panic!("{:?}", escalation)
}

fn augment_err_result<T>(&self, mut x: Result<T, Self::Error>) -> Result<T, Self::Error> {
if let Err(e) = &mut x {
if e.info.is_none() {
e.info = self.maybe_get_debug_info()
}
}
x
}

fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
todo!()
}
Expand Down
21 changes: 15 additions & 6 deletions soroban-env-host/src/host/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ impl Debug for HostError {
|| frame_name_matches(frame, "host::err")
|| frame_name_matches(frame, "Host::err")
|| frame_name_matches(frame, "Host>::err")
|| frame_name_matches(frame, "::map_err")
|| frame_name_matches(frame, "::augment_err_result")
}

writeln!(f, "HostError: {:?}", self.error)?;
Expand Down Expand Up @@ -236,19 +236,28 @@ impl Host {
if let Err(e) = self.err_diagnostics(events_refmut.deref_mut(), error, msg, args) {
return e;
}
}
let info = self.maybe_get_debug_info();
return HostError { error, info };
}
error.into()
}

pub(crate) fn maybe_get_debug_info(&self) -> Option<Box<DebugInfo>> {
if let Ok(true) = self.is_debug() {
if let Ok(events_ref) = self.0.events.try_borrow() {
let events = match self
.as_budget()
.with_free_budget(|| events_refmut.externalize(self))
.with_free_budget(|| events_ref.externalize(self))
{
Ok(events) => events,
Err(e) => return e,
Err(e) => return None,
};
let backtrace = Backtrace::new_unresolved();
let info = Some(Box::new(DebugInfo { backtrace, events }));
return HostError { error, info };
return Some(Box::new(DebugInfo { backtrace, events }));
}
}
error.into()
None
}

// Some common error patterns here.
Expand Down
30 changes: 29 additions & 1 deletion soroban-env-host/src/test/invocation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use soroban_env_common::{
use crate::{
events::HostEvent, xdr::ScErrorType, ContractFunctionSet, Error, Host, HostError, Symbol, Tag,
};
use soroban_test_wasms::{ADD_I32, ERR, INVOKE_CONTRACT, VEC};
use soroban_test_wasms::{ADD_I32, ALLOC, ERR, INVOKE_CONTRACT, VEC};

#[test]
fn invoke_single_contract_function() -> Result<(), HostError> {
Expand All @@ -36,6 +36,34 @@ fn invoke_single_contract_function() -> Result<(), HostError> {
Ok(())
}

#[test]
fn invoke_alloc() -> Result<(), HostError> {
let host = Host::test_host_with_recording_footprint();
host.enable_debug()?;
let contract_id_obj = host.register_test_contract_wasm(ALLOC);
let res = host.call(
contract_id_obj,
Symbol::try_from_small_str("sum")?,
host.test_vec_obj::<u32>(&[128])?,
)?;
assert!(res.shallow_eq(&8128_u32.into()));
let used_bytes = host.budget_cloned().get_mem_bytes_consumed()?;
// The general pattern of memory growth in this contract will be a sequence
// of vector-doublings, but these are masked by the fact that we only see
// the calls that cause the backing vector of wasm linear memory to grow,
// which happens as the guest vector crosses 64k boundaries (and eventually
// starts growing in steps larger than 64k itself).
//
// So we wind up with a growth-sequence that's a bit irregular: +0x10000,
// +0x20000, +0x30000, +0x50000, +0x90000. Total is 1 + 2 + 3 + 5 + 9 = 20
// pages or about 1.3 MiB, plus the initial 17 pages (1.1MiB) plus some more
// slop from general host machinery allocations, we get around 2.5MiB. Call
// is "less than 3MiB".
assert!(used_bytes > (128 * 4096));
assert!(used_bytes < 0x30_0000);
Ok(())
}

fn invoke_cross_contract(diagnostics: bool) -> Result<(), HostError> {
let host = Host::test_host_with_recording_footprint();
if diagnostics {
Expand Down
8 changes: 7 additions & 1 deletion soroban-env-host/src/vm/dispatch.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::FuelRefillable;
use crate::{xdr::ContractCostType, Host, HostError, VmCaller, VmCallerEnv};
use crate::{xdr::ContractCostType, EnvBase, Host, HostError, VmCaller, VmCallerEnv};
use crate::{
AddressObject, Bool, BytesObject, DurationObject, Error, I128Object, I256Object, I256Val,
I32Val, I64Object, MapObject, StorageType, StringObject, Symbol, SymbolObject, TimepointObject,
Expand Down Expand Up @@ -164,6 +164,12 @@ macro_rules! generate_dispatch_functions {
// wasmi::Value.
let res: Result<_, HostError> = host.$fn_id(&mut vmcaller, $(<$type>::try_marshal_from_relative_value(Value::I64($arg), &host)?),*);

// On the off chance we got an error with no context, we can
// at least attach some here "at each host function call",
// fairly systematically. This will cause the context to
// propagate back through wasmi to its caller.
let res = host.augment_err_result(res);

let res = match res {
Ok(ok) => {
let val: Value = ok.marshal_relative_from_self(&host)?;
Expand Down
1 change: 1 addition & 0 deletions soroban-test-wasms/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

pub const ADD_I32: &[u8] = include_bytes!("../wasm-workspace/opt/example_add_i32.wasm").as_slice();
pub const ADD_F32: &[u8] = include_bytes!("../wasm-workspace/opt/example_add_f32.wasm").as_slice();
pub const ALLOC: &[u8] = include_bytes!("../wasm-workspace/opt/example_alloc.wasm").as_slice();
pub const CREATE_CONTRACT: &[u8] =
include_bytes!("../wasm-workspace/opt/example_create_contract.wasm").as_slice();
pub const CONTRACT_STORAGE: &[u8] =
Expand Down
7 changes: 7 additions & 0 deletions soroban-test-wasms/wasm-workspace/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions soroban-test-wasms/wasm-workspace/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ resolver = "2"
members = [
"add_i32",
"add_f32",
"alloc",
"auth",
"fib",
"contract_data",
Expand Down
15 changes: 15 additions & 0 deletions soroban-test-wasms/wasm-workspace/alloc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "example_alloc"
version = "0.0.0"
authors = ["Stellar Development Foundation <[email protected]>"]
license = "Apache-2.0"
edition = "2021"
publish = false
rust-version = "1.65"

[lib]
crate-type = ["cdylib", "rlib"]
doctest = false

[dependencies]
soroban-sdk = { workspace = true, features = ["alloc"] }
27 changes: 27 additions & 0 deletions soroban-test-wasms/wasm-workspace/alloc/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#![no_std]
use soroban_sdk::{contract, contractimpl, Env};

extern crate alloc;

#[contract]
pub struct AllocContract;

struct Large {
x: u32,
space: [u8;4096]
}

#[contractimpl]
impl AllocContract {
/// Allocates a temporary vector holding values (0..count), then computes
/// and returns their sum. Also allocates these values in a "large"
/// structure (with a bunch of pointless padding) to ensure the contract
/// allocates lots of memory.
pub fn sum(_env: Env, count: u32) -> u32 {
let mut v1 = alloc::vec![];
for i in 0..count {
v1.push(Large{x: i, space: [0u8; 4096]})
}
v1.iter().map(|l| l.x + l.space[0] as u32).sum()
}
}
Binary file not shown.

0 comments on commit 876fb16

Please sign in to comment.