Skip to content

Commit

Permalink
Add fuel consumption modes (#706)
Browse files Browse the repository at this point in the history
* add fuel consumption mode to Config

* make Config::wasm_features crate private

This API was never intened to be public.

* add doc note to Config::fuel_consumption_mode

* implement both fuel consumption modes in executor

* fix internal doc link

* add tests for fuel consumption modes

* Update crates/wasmi/src/engine/config.rs

Co-authored-by: Alexander Theißen <alex.theissen@me.com>

* try fix performance regressions

* add some inline annotations in executor

* deduplicate some code

* Revert "deduplicate some code"

This reverts commit 165d94d.

* try to fix perf regressions (take 2)

* remove some inline annotations

* refactor code

* refactor code (split into more funcs)

* apply #[cold] attribute

* Revert "apply #[cold] attribute"

This reverts commit 13e017e.

* try fix regression (take 3)

* replace inlines

* remove cold attribute again

* replace some inline(always) with inline

* Revert "replace some inline(always) with inline"

This reverts commit 482abc1.

* add cold attribute again ...

* Revert "add cold attribute again ..."

This reverts commit 925cc22.

* put inline on all UntypedValue public methods

* Revert "put inline on all UntypedValue public methods"

This reverts commit df19822.

* put inline(always) on ret method

* refactor code

* apply rustfmt

* try to fix Wasm regressions

* Revert "try to fix Wasm regressions"

This reverts commit 267666f.

* deduplicate some control flow code in executor

* apply rustfmt

* use Default impl

* try to mutate inline annotations a bit

* adjust inline

* use inline

* remove one inline

* next try

* go back to stage 1 ...

---------

Co-authored-by: Alexander Theißen <alex.theissen@me.com>
  • Loading branch information
Robbepop and athei committed Mar 6, 2023
1 parent cf7736f commit 6dbbada
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 66 deletions.
75 changes: 74 additions & 1 deletion crates/wasmi/src/engine/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,60 @@ pub struct Config {
floats: bool,
/// Is `true` if `wasmi` executions shall consume fuel.
consume_fuel: bool,
/// The fuel consumption mode of the `wasmi` [`Engine`](crate::Engine).
fuel_consumption_mode: FuelConsumptionMode,
/// The configured fuel costs of all `wasmi` bytecode instructions.
fuel_costs: FuelCosts,
}

/// The fuel consumption mode of the `wasmi` [`Engine`].
///
/// This mode affects when fuel is charged for Wasm bulk-operations.
/// Affected Wasm instructions are:
///
/// - `memory.{grow, copy, fill}`
/// - `data.init`
/// - `table.{grow, copy, fill}`
/// - `element.init`
///
/// The default fuel consumption mode is [`FuelConsumptionMode::Lazy`].
///
/// [`Engine`]: crate::Engine
#[derive(Debug, Default, Copy, Clone)]
pub enum FuelConsumptionMode {
/// Fuel consumption for bulk-operations is lazy.
///
/// Lazy fuel consumption means that fuel for bulk-operations
/// is checked before executing the instruction but only consumed
/// if the executed instruction suceeded. The reason for this is
/// that bulk-operations fail fast and therefore do not cost
/// a lot of compute power in case of failure.
///
/// # Note
///
/// Lazy fuel consumption makes sense as default mode since the
/// affected bulk-operations usually are very costly if they are
/// successful. Therefore users generally want to avoid having to
/// using more fuel than what was actually used, especially if there
/// is an underlying cost model associated to the used fuel.
#[default]
Lazy,
/// Fuel consumption for bulk-operations is eager.
///
/// Eager fuel consumption means that fuel for bulk-operations
/// is always consumed before executing the instruction independent
/// of it suceeding or failing.
///
/// # Note
///
/// A use case for when a user might prefer eager fuel consumption
/// is when the fuel **required** to perform an execution should be identical
/// to the actual fuel **consumed** by an execution. Otherwise it can be confusing
/// that the execution consumed `x` gas while it needs `x + gas_for_bulk_op` to
/// not run out of fuel.
Eager,
}

/// Type storing all kinds of fuel costs of instructions.
#[derive(Debug, Copy, Clone)]
pub struct FuelCosts {
Expand Down Expand Up @@ -154,6 +204,7 @@ impl Default for Config {
floats: true,
consume_fuel: false,
fuel_costs: FuelCosts::default(),
fuel_consumption_mode: FuelConsumptionMode::default(),
}
}
}
Expand Down Expand Up @@ -312,8 +363,30 @@ impl Config {
&self.fuel_costs
}

/// Configures the [`FuelConsumptionMode`] for the [`Engine`].
///
/// # Note
///
/// This has no effect if fuel metering is disabled for the [`Engine`].
///
/// [`Engine`]: crate::Engine
pub fn fuel_consumption_mode(&mut self, mode: FuelConsumptionMode) -> &mut Self {
self.fuel_consumption_mode = mode;
self
}

/// Returns the [`FuelConsumptionMode`] for the [`Engine`].
///
/// Returns `None` if fuel metering is disabled for the [`Engine`].
///
/// [`Engine`]: crate::Engine
pub(crate) fn get_fuel_consumption_mode(&self) -> Option<FuelConsumptionMode> {
self.get_consume_fuel()
.then_some(self.fuel_consumption_mode)
}

/// Returns the [`WasmFeatures`] represented by the [`Config`].
pub fn wasm_features(&self) -> WasmFeatures {
pub(crate) fn wasm_features(&self) -> WasmFeatures {
WasmFeatures {
multi_value: self.multi_value,
mutable_global: self.mutable_global,
Expand Down
170 changes: 106 additions & 64 deletions crates/wasmi/src/engine/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use crate::{
},
func::FuncEntity,
table::TableEntity,
FuelConsumptionMode,
Func,
FuncRef,
Instance,
Expand Down Expand Up @@ -164,6 +165,21 @@ struct Executor<'ctx, 'engine> {
code_map: &'engine CodeMap,
}

macro_rules! forward_call {
($expr:expr) => {{
if let CallOutcome::Call {
host_func,
instance,
} = $expr?
{
return Ok(WasmOutcome::Call {
host_func,
instance,
});
}
}};
}

impl<'ctx, 'engine> Executor<'ctx, 'engine> {
/// Creates a new [`Executor`] for executing a `wasmi` function frame.
#[inline(always)]
Expand Down Expand Up @@ -214,56 +230,18 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
}
}
Instr::ReturnCall { drop_keep, func } => {
if let CallOutcome::Call {
host_func,
instance,
} = self.visit_return_call(drop_keep, func)?
{
return Ok(WasmOutcome::Call {
host_func,
instance,
});
}
forward_call!(self.visit_return_call(drop_keep, func))
}
Instr::ReturnCallIndirect {
drop_keep,
table,
func_type,
} => {
if let CallOutcome::Call {
host_func,
instance,
} = self.visit_return_call_indirect(drop_keep, table, func_type)?
{
return Ok(WasmOutcome::Call {
host_func,
instance,
});
}
}
Instr::Call(func) => {
if let CallOutcome::Call {
host_func,
instance,
} = self.visit_call(func)?
{
return Ok(WasmOutcome::Call {
host_func,
instance,
});
}
forward_call!(self.visit_return_call_indirect(drop_keep, table, func_type))
}
Instr::Call(func) => forward_call!(self.visit_call(func)),
Instr::CallIndirect { table, func_type } => {
if let CallOutcome::Call {
host_func,
instance,
} = self.visit_call_indirect(table, func_type)?
{
return Ok(WasmOutcome::Call {
host_func,
instance,
});
}
forward_call!(self.visit_call_indirect(table, func_type))
}
Instr::Drop => self.visit_drop(),
Instr::Select => self.visit_select(),
Expand Down Expand Up @@ -599,7 +577,7 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
///
/// This also modifies the stack as the caller would expect it
/// and synchronizes the execution state with the outer structures.
#[inline]
#[inline(always)]
fn ret(&mut self, drop_keep: DropKeep) -> ReturnOutcome {
self.sp.drop_keep(drop_keep);
self.sync_stack_ptr();
Expand All @@ -622,49 +600,113 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
/// for amount of required fuel determined by `delta` or if
/// fuel metering is disabled.
///
/// Only if `exec` runs successfully and fuel metering
/// is enabled the fuel determined by `delta` is charged.
///
/// # Errors
///
/// - If the [`StoreInner`] ran out of fuel.
/// - If the `exec` closure traps.
fn consume_fuel_on_success<T, E>(
#[inline(always)]
fn consume_fuel_with<T, E>(
&mut self,
delta: impl FnOnce(&FuelCosts) -> u64,
exec: impl FnOnce(&mut Self) -> Result<T, E>,
) -> Result<T, E>
where
E: From<TrapCode>,
{
if !self.is_fuel_metering_enabled() {
return exec(self);
match self.get_fuel_consumption_mode() {
None => exec(self),
Some(mode) => self.consume_fuel_with_mode(mode, delta, exec),
}
// At this point we know that fuel metering is enabled.
}

/// Consume an amount of fuel specified by `delta` and executes `exec`.
///
/// The `mode` determines when and if the fuel determined by `delta` is charged.
///
/// # Errors
///
/// - If the [`StoreInner`] ran out of fuel.
/// - If the `exec` closure traps.
#[inline(always)]
fn consume_fuel_with_mode<T, E>(
&mut self,
mode: FuelConsumptionMode,
delta: impl FnOnce(&FuelCosts) -> u64,
exec: impl FnOnce(&mut Self) -> Result<T, E>,
) -> Result<T, E>
where
E: From<TrapCode>,
{
let delta = delta(self.fuel_costs());
match mode {
FuelConsumptionMode::Lazy => self.consume_fuel_with_lazy(delta, exec),
FuelConsumptionMode::Eager => self.consume_fuel_with_eager(delta, exec),
}
}

/// Consume an amount of fuel specified by `delta` if `exec` succeeds.
///
/// Prior to executing `exec` it is checked if enough fuel is remaining
/// determined by `delta`. The fuel is charged only after `exec` has been
/// finished successfully.
///
/// # Errors
///
/// - If the [`StoreInner`] ran out of fuel.
/// - If the `exec` closure traps.
#[inline(always)]
fn consume_fuel_with_lazy<T, E>(
&mut self,
delta: u64,
exec: impl FnOnce(&mut Self) -> Result<T, E>,
) -> Result<T, E>
where
E: From<TrapCode>,
{
self.ctx.fuel().sufficient_fuel(delta)?;
let result = exec(self)?;
self.ctx
.fuel_mut()
.consume_fuel(delta)
.unwrap_or_else(|error| {
panic!("remaining fuel has already been approved prior but encountered: {error}")
});
.expect("remaining fuel has already been approved prior");
Ok(result)
}

/// Returns `true` if fuel metering is enabled.
fn is_fuel_metering_enabled(&self) -> bool {
self.ctx.engine().config().get_consume_fuel()
/// Consume an amount of fuel specified by `delta` and executes `exec`.
///
/// # Errors
///
/// - If the [`StoreInner`] ran out of fuel.
/// - If the `exec` closure traps.
#[inline(always)]
fn consume_fuel_with_eager<T, E>(
&mut self,
delta: u64,
exec: impl FnOnce(&mut Self) -> Result<T, E>,
) -> Result<T, E>
where
E: From<TrapCode>,
{
self.ctx.fuel_mut().consume_fuel(delta)?;
exec(self)
}

/// Returns a shared reference to the [`FuelCosts`] of the [`Engine`].
///
/// [`Engine`]: crate::Engine
#[inline]
fn fuel_costs(&self) -> &FuelCosts {
self.ctx.engine().config().fuel_costs()
}

/// Returns the [`FuelConsumptionMode`] of the [`Engine`].
///
/// [`Engine`]: crate::Engine
#[inline]
fn get_fuel_consumption_mode(&self) -> Option<FuelConsumptionMode> {
self.ctx.engine().config().get_fuel_consumption_mode()
}

/// Executes a `call` or `return_call` instruction.
#[inline(always)]
fn execute_call(
Expand Down Expand Up @@ -892,7 +934,7 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
return self.try_next_instr();
}
};
let result = self.consume_fuel_on_success(
let result = self.consume_fuel_with(
|costs| {
let delta_in_bytes = delta.to_bytes().unwrap_or(0) as u64;
costs.fuel_for_bytes(delta_in_bytes)
Expand Down Expand Up @@ -928,7 +970,7 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
let n = i32::from(n) as usize;
let offset = i32::from(d) as usize;
let byte = u8::from(val);
self.consume_fuel_on_success(
self.consume_fuel_with(
|costs| costs.fuel_for_bytes(n as u64),
|this| {
let memory = this
Expand All @@ -951,7 +993,7 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
let n = i32::from(n) as usize;
let src_offset = i32::from(s) as usize;
let dst_offset = i32::from(d) as usize;
self.consume_fuel_on_success(
self.consume_fuel_with(
|costs| costs.fuel_for_bytes(n as u64),
|this| {
let data = this.cache.default_memory_bytes(this.ctx);
Expand All @@ -976,7 +1018,7 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
let n = i32::from(n) as usize;
let src_offset = i32::from(s) as usize;
let dst_offset = i32::from(d) as usize;
self.consume_fuel_on_success(
self.consume_fuel_with(
|costs| costs.fuel_for_bytes(n as u64),
|this| {
let (memory, data) = this
Expand Down Expand Up @@ -1018,7 +1060,7 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
fn visit_table_grow(&mut self, table_index: TableIdx) -> Result<(), TrapCode> {
let (init, delta) = self.sp.pop2();
let delta: u32 = delta.into();
let result = self.consume_fuel_on_success(
let result = self.consume_fuel_with(
|costs| costs.fuel_for_elements(u64::from(delta)),
|this| {
let table = this.cache.get_table(this.ctx, table_index);
Expand All @@ -1043,7 +1085,7 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
let (i, val, n) = self.sp.pop3();
let dst: u32 = i.into();
let len: u32 = n.into();
self.consume_fuel_on_success(
self.consume_fuel_with(
|costs| costs.fuel_for_elements(u64::from(len)),
|this| {
let table = this.cache.get_table(this.ctx, table_index);
Expand Down Expand Up @@ -1088,7 +1130,7 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
let len = u32::from(n);
let src_index = u32::from(s);
let dst_index = u32::from(d);
self.consume_fuel_on_success(
self.consume_fuel_with(
|costs| costs.fuel_for_elements(u64::from(len)),
|this| {
// Query both tables and check if they are the same:
Expand Down Expand Up @@ -1120,7 +1162,7 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> {
let len = u32::from(n);
let src_index = u32::from(s);
let dst_index = u32::from(d);
self.consume_fuel_on_success(
self.consume_fuel_with(
|costs| costs.fuel_for_elements(u64::from(len)),
|this| {
let (instance, table, element) = this
Expand Down
Loading

0 comments on commit 6dbbada

Please sign in to comment.