Skip to content

Commit

Permalink
Add a test-only mechanism for metering most of the relevant resources…
Browse files Browse the repository at this point in the history
… per-invocation (#1482)

### What

This is an opt-in feature that will reset the budget and take a storage
snapshot for every 'logical' invocation (such as `call` or a lifecycle
operation). Then when the invocation is done we use the snapshot and
current budget to produce the estimate for the resources consumed by the
invocation (budget-related, IO-related and rent bumps).

This also provides a rough estimation for the respective fee breakdown
given a fee config.

### Why

Make unit tests more useful for rough performance evaluation. This is
the initial env-side implementation for
stellar/rs-soroban-sdk#1319

### Known limitations

Some of the resources are tricky to model. Specifically, this omits:

- transaction size
- return value size
- some XDR roundtrips that always happen for production scenarios

These shouldn't be too significant though and are likely better
addressed via e2e runs (like simulation or some `e2e_invoke`-based
mechanism)
  • Loading branch information
dmkozh authored Nov 7, 2024
1 parent 3efa65b commit 05219cf
Show file tree
Hide file tree
Showing 11 changed files with 834 additions and 9 deletions.
6 changes: 5 additions & 1 deletion soroban-env-host/src/budget/dimension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,12 @@ impl BudgetDimension {

pub(crate) fn reset(&mut self, limit: u64) {
self.limit = limit;
self.total_count = 0;
self.shadow_limit = limit;
self.reset_count();
}

pub(crate) fn reset_count(&mut self) {
self.total_count = 0;
self.shadow_total_count = 0;
}

Expand Down
11 changes: 10 additions & 1 deletion soroban-env-host/src/budget/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ impl Budget {
Ok(())
}

pub fn reset(&self) -> Result<(), HostError> {
self.with_mut_budget(|mut b| {
b.cpu_insns.reset_count();
b.mem_bytes.reset_count();
Ok(())
})?;
self.reset_tracker()
}

pub fn reset_cpu_limit(&self, cpu: u64) -> Result<(), HostError> {
self.with_mut_budget(|mut b| {
b.cpu_insns.reset(cpu);
Expand Down Expand Up @@ -157,7 +166,7 @@ impl Budget {
}
}

#[cfg(any(test, feature = "recording_mode"))]
#[cfg(any(test, feature = "recording_mode", feature = "testutils"))]
impl Budget {
/// Variant of `with_shadow_mode`, enabled only in testing and
/// non-production scenarios, that produces a `Result<>` rather than eating
Expand Down
5 changes: 5 additions & 0 deletions soroban-env-host/src/events/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,4 +267,9 @@ impl InternalEventsBuffer {

Ok(Events(vec))
}

#[cfg(any(test, feature = "testutils"))]
pub(crate) fn clear(&mut self) {
self.vec.clear();
}
}
4 changes: 2 additions & 2 deletions soroban-env-host/src/fees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ pub const TX_BASE_RESULT_SIZE: u32 = 300;
/// Estimate for any `TtlEntry` ledger entry
pub const TTL_ENTRY_SIZE: u32 = 48;

const INSTRUCTIONS_INCREMENT: i64 = 10000;
const DATA_SIZE_1KB_INCREMENT: i64 = 1024;
pub const INSTRUCTIONS_INCREMENT: i64 = 10000;
pub const DATA_SIZE_1KB_INCREMENT: i64 = 1024;

// minimum effective write fee per 1KB
pub const MINIMUM_WRITE_FEE_PER_1KB: i64 = 1000;
Expand Down
44 changes: 44 additions & 0 deletions soroban-env-host/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ mod data_helper;
mod declared_size;
pub(crate) mod error;
pub(crate) mod frame;
#[cfg(any(test, feature = "testutils"))]
mod invocation_metering;
pub(crate) mod ledger_info_helper;
pub(crate) mod lifecycle;
mod mem_helper;
Expand Down Expand Up @@ -61,6 +63,9 @@ pub(crate) use frame::Frame;
#[cfg(any(test, feature = "recording_mode"))]
use rand_chacha::ChaCha20Rng;

#[cfg(any(test, feature = "testutils"))]
use invocation_metering::InvocationMeter;

#[cfg(any(test, feature = "testutils"))]
#[derive(Clone, Copy)]
pub enum ContractInvocationEvent {
Expand Down Expand Up @@ -166,6 +171,9 @@ struct HostImpl {
#[doc(hidden)]
#[cfg(any(test, feature = "recording_mode"))]
need_to_build_module_cache: RefCell<bool>,

#[cfg(any(test, feature = "testutils"))]
pub(crate) invocation_meter: RefCell<InvocationMeter>,
}

// Host is a newtype on Rc<HostImpl> so we can impl Env for it below.
Expand Down Expand Up @@ -382,6 +390,8 @@ impl Host {
suppress_diagnostic_events: RefCell::new(false),
#[cfg(any(test, feature = "recording_mode"))]
need_to_build_module_cache: RefCell::new(false),
#[cfg(any(test, feature = "testutils"))]
invocation_meter: Default::default(),
}))
}

Expand Down Expand Up @@ -2330,6 +2340,9 @@ impl VmCallerEnv for Host {
_vmcaller: &mut VmCaller<Host>,
wasm: BytesObject,
) -> Result<BytesObject, HostError> {
#[cfg(any(test, feature = "testutils"))]
let _invocation_meter_scope = self.maybe_meter_invocation()?;

let wasm_vec =
self.visit_obj(wasm, |bytes: &ScBytes| bytes.as_vec().metered_clone(self))?;
self.upload_contract_wasm(wasm_vec)
Expand Down Expand Up @@ -2369,6 +2382,9 @@ impl VmCallerEnv for Host {
func: Symbol,
args: VecObject,
) -> Result<Val, HostError> {
#[cfg(any(test, feature = "testutils"))]
let _invocation_meter_scope = self.maybe_meter_invocation()?;

let argvec = self.call_args_from_obj(args)?;
// this is the recommended path of calling a contract, with `reentry`
// always set `ContractReentryMode::Prohibited`
Expand Down Expand Up @@ -2396,6 +2412,9 @@ impl VmCallerEnv for Host {
func: Symbol,
args: VecObject,
) -> Result<Val, HostError> {
#[cfg(any(test, feature = "testutils"))]
let _invocation_meter_scope = self.maybe_meter_invocation()?;

let argvec = self.call_args_from_obj(args)?;
// this is the "loosened" path of calling a contract.
// TODO: A `reentry` flag will be passed from `try_call` into here.
Expand Down Expand Up @@ -3477,6 +3496,31 @@ impl Host {
)
})
}

/// Returns the resources metered during the last logical contract invocation.
///
/// Logical invocations include the direct `invoke_host_function` calls,
/// `call`/`try_call` functions, contract lifecycle management operations.
///
/// Take the return value with a grain of salt. The returned resources mostly
/// correspond only to the operations that have happened during the host
/// invocation, i.e. this won't try to simulate the work that happens in
/// production scenarios (e.g. certain XDR rountrips). This also doesn't try
/// to model resources related to the transaction size.
///
/// The returned value is as useful as the preceding setup, e.g. if a test
/// contract is used instead of a Wasm contract, all the costs related to
/// VM instantiation and execution, as well as Wasm reads/rent bumps will be
/// missed.
pub fn get_last_invocation_resources(
&self,
) -> Option<invocation_metering::InvocationResources> {
if let Ok(scope) = self.0.invocation_meter.try_borrow() {
scope.get_invocation_resources()
} else {
None
}
}
}

impl Host {
Expand Down
4 changes: 4 additions & 0 deletions soroban-env-host/src/host/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,7 @@ impl Host {
where
F: FnOnce() -> Result<Val, HostError>,
{
let _invocation_meter_scope = self.maybe_meter_invocation()?;
self.with_frame(
Frame::TestContract(self.create_test_contract_frame(id, func, vec![])?),
f,
Expand Down Expand Up @@ -1069,6 +1070,9 @@ impl Host {

// Notes on metering: covered by the called components.
pub fn invoke_function(&self, hf: HostFunction) -> Result<ScVal, HostError> {
#[cfg(any(test, feature = "testutils"))]
let _invocation_meter_scope = self.maybe_meter_invocation()?;

let rv = self.invoke_function_and_return_val(hf)?;
self.from_host_val(rv)
}
Expand Down
Loading

0 comments on commit 05219cf

Please sign in to comment.