mirror of
https://github.com/fluencelabs/wasm-utils
synced 2025-03-15 11:10:49 +00:00
Merge pull request #121 from jimpo/gas-docs
Documentation of gas metering instrumentation process & cleanup.
This commit is contained in:
commit
c3d10a2619
43
src/gas.rs
43
src/gas.rs
@ -1,3 +1,9 @@
|
|||||||
|
//! This module is used to instrument a Wasm module with gas metering code.
|
||||||
|
//!
|
||||||
|
//! The primary public interface is the `inject_gas_counter` function which transforms a given
|
||||||
|
//! module into one that charges gas for code to be executed. See function documentation for usage
|
||||||
|
//! and details.
|
||||||
|
|
||||||
use std::vec::Vec;
|
use std::vec::Vec;
|
||||||
|
|
||||||
use parity_wasm::{elements, builder};
|
use parity_wasm::{elements, builder};
|
||||||
@ -185,10 +191,37 @@ pub fn inject_counter(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Injects gas counter.
|
/// Transforms a given module into one that charges gas for code to be executed by proxy of an
|
||||||
|
/// imported gas metering function.
|
||||||
///
|
///
|
||||||
/// Can only fail if encounters operation forbidden by gas rules,
|
/// The output module imports a function "gas" from the module "env" with type signature
|
||||||
/// in this case it returns error with the original module.
|
/// [i32] -> []. The argument is the amount of gas required to continue execution. The external
|
||||||
|
/// function is meant to keep track of the total amount of gas used and trap or otherwise halt
|
||||||
|
/// execution of the runtime if the gas usage exceeds some allowed limit.
|
||||||
|
///
|
||||||
|
/// The calls to charge gas are inserted at the beginning of every block of code. A block is
|
||||||
|
/// defined by `block`, `if`, `else`, `loop`, and `end` boundaries. Blocks form a nested hierarchy
|
||||||
|
/// where `block`, `if`, `else`, and `loop` begin a new nested block, and `end` and `else` mark the
|
||||||
|
/// end of a block. The gas cost of a block is determined statically as 1 plus the gas cost of all
|
||||||
|
/// instructions directly in that block. Each instruction is only counted in the most deeply
|
||||||
|
/// nested block containing it (ie. a block's cost does not include the cost of instructions in any
|
||||||
|
/// blocks nested within it). The cost of the `begin`, `if`, and `loop` instructions is counted
|
||||||
|
/// towards the block containing them, not the nested block that they open. There is no gas cost
|
||||||
|
/// added for `end`/`else`, as they are pseudo-instructions. The gas cost of each instruction is
|
||||||
|
/// determined by a `rules::Set` parameter. At the beginning of each block, this procedure injects
|
||||||
|
/// new instructions to call the "gas" function with the gas cost of the block as an argument.
|
||||||
|
///
|
||||||
|
/// Additionally, each `memory.grow` instruction found in the module is instrumented to first make
|
||||||
|
/// a call to charge gas for the additional pages requested. This cannot be done as part of the
|
||||||
|
/// block level gas charges as the gas cost is not static and depends on the stack argument to
|
||||||
|
/// `memory.grow`.
|
||||||
|
///
|
||||||
|
/// The above transformations are performed for every function body defined in the module. This
|
||||||
|
/// function also rewrites all function indices references by code, table elements, etc., since
|
||||||
|
/// the addition of an imported functions changes the indices of module-defined functions.
|
||||||
|
///
|
||||||
|
/// The function fails if the module contains any operation forbidden by gas rule set, returning
|
||||||
|
/// the original module as an Err.
|
||||||
pub fn inject_gas_counter(module: elements::Module, rules: &rules::Set)
|
pub fn inject_gas_counter(module: elements::Module, rules: &rules::Set)
|
||||||
-> Result<elements::Module, elements::Module>
|
-> Result<elements::Module, elements::Module>
|
||||||
{
|
{
|
||||||
@ -212,7 +245,7 @@ pub fn inject_gas_counter(module: elements::Module, rules: &rules::Set)
|
|||||||
let mut module = mbuilder.build();
|
let mut module = mbuilder.build();
|
||||||
|
|
||||||
// calculate actual function index of the imported definition
|
// calculate actual function index of the imported definition
|
||||||
// (substract all imports that are NOT functions)
|
// (subtract all imports that are NOT functions)
|
||||||
|
|
||||||
let gas_func = module.import_count(elements::ImportCountType::Function) as u32 - 1;
|
let gas_func = module.import_count(elements::ImportCountType::Function) as u32 - 1;
|
||||||
let total_func = module.functions_space() as u32;
|
let total_func = module.functions_space() as u32;
|
||||||
@ -244,6 +277,8 @@ pub fn inject_gas_counter(module: elements::Module, rules: &rules::Set)
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
&mut elements::Section::Element(ref mut elements_section) => {
|
&mut elements::Section::Element(ref mut elements_section) => {
|
||||||
|
// Note that we do not need to check the element type referenced because in the
|
||||||
|
// WebAssembly 1.0 spec, the only allowed element type is funcref.
|
||||||
for ref mut segment in elements_section.entries_mut() {
|
for ref mut segment in elements_section.entries_mut() {
|
||||||
// update all indirect call addresses initial values
|
// update all indirect call addresses initial values
|
||||||
for func_index in segment.members_mut() {
|
for func_index in segment.members_mut() {
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
//!
|
//!
|
||||||
//! All values are treated equally, as they have the same size.
|
//! All values are treated equally, as they have the same size.
|
||||||
//!
|
//!
|
||||||
//! The rationale for this it makes it possible to use this very naive wasm executor, that is:
|
//! The rationale is that this makes it possible to use the following very naive wasm executor:
|
||||||
//!
|
//!
|
||||||
//! - values are implemented by a union, so each value takes a size equal to
|
//! - values are implemented by a union, so each value takes a size equal to
|
||||||
//! the size of the largest possible value type this union can hold. (In MVP it is 8 bytes)
|
//! the size of the largest possible value type this union can hold. (In MVP it is 8 bytes)
|
||||||
@ -93,35 +93,20 @@ mod thunk;
|
|||||||
pub struct Error(String);
|
pub struct Error(String);
|
||||||
|
|
||||||
pub(crate) struct Context {
|
pub(crate) struct Context {
|
||||||
stack_height_global_idx: Option<u32>,
|
stack_height_global_idx: u32,
|
||||||
func_stack_costs: Option<Vec<u32>>,
|
func_stack_costs: Vec<u32>,
|
||||||
stack_limit: u32,
|
stack_limit: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
/// Returns index in a global index space of a stack_height global variable.
|
/// Returns index in a global index space of a stack_height global variable.
|
||||||
///
|
|
||||||
/// Panics if it haven't generated yet.
|
|
||||||
fn stack_height_global_idx(&self) -> u32 {
|
fn stack_height_global_idx(&self) -> u32 {
|
||||||
self.stack_height_global_idx.expect(
|
self.stack_height_global_idx
|
||||||
"stack_height_global_idx isn't yet generated;
|
|
||||||
Did you call `inject_stack_counter_global`",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `stack_cost` for `func_idx`.
|
/// Returns `stack_cost` for `func_idx`.
|
||||||
///
|
|
||||||
/// Panics if stack costs haven't computed yet or `func_idx` is greater
|
|
||||||
/// than the last function index.
|
|
||||||
fn stack_cost(&self, func_idx: u32) -> Option<u32> {
|
fn stack_cost(&self, func_idx: u32) -> Option<u32> {
|
||||||
self.func_stack_costs
|
self.func_stack_costs.get(func_idx as usize).cloned()
|
||||||
.as_ref()
|
|
||||||
.expect(
|
|
||||||
"func_stack_costs isn't yet computed;
|
|
||||||
Did you call `compute_stack_costs`?",
|
|
||||||
)
|
|
||||||
.get(func_idx as usize)
|
|
||||||
.cloned()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns stack limit specified by the rules.
|
/// Returns stack limit specified by the rules.
|
||||||
@ -142,13 +127,11 @@ pub fn inject_limiter(
|
|||||||
stack_limit: u32,
|
stack_limit: u32,
|
||||||
) -> Result<elements::Module, Error> {
|
) -> Result<elements::Module, Error> {
|
||||||
let mut ctx = Context {
|
let mut ctx = Context {
|
||||||
stack_height_global_idx: None,
|
stack_height_global_idx: generate_stack_height_global(&mut module),
|
||||||
func_stack_costs: None,
|
func_stack_costs: compute_stack_costs(&module)?,
|
||||||
stack_limit,
|
stack_limit,
|
||||||
};
|
};
|
||||||
|
|
||||||
generate_stack_height_global(&mut ctx, &mut module);
|
|
||||||
compute_stack_costs(&mut ctx, &module)?;
|
|
||||||
instrument_functions(&mut ctx, &mut module)?;
|
instrument_functions(&mut ctx, &mut module)?;
|
||||||
let module = thunk::generate_thunks(&mut ctx, module)?;
|
let module = thunk::generate_thunks(&mut ctx, module)?;
|
||||||
|
|
||||||
@ -156,7 +139,7 @@ pub fn inject_limiter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a new global that will be used for tracking current stack height.
|
/// Generate a new global that will be used for tracking current stack height.
|
||||||
fn generate_stack_height_global(ctx: &mut Context, module: &mut elements::Module) {
|
fn generate_stack_height_global(module: &mut elements::Module) -> u32 {
|
||||||
let global_entry = builder::global()
|
let global_entry = builder::global()
|
||||||
.value_type()
|
.value_type()
|
||||||
.i32()
|
.i32()
|
||||||
@ -168,10 +151,7 @@ fn generate_stack_height_global(ctx: &mut Context, module: &mut elements::Module
|
|||||||
for section in module.sections_mut() {
|
for section in module.sections_mut() {
|
||||||
if let elements::Section::Global(ref mut gs) = *section {
|
if let elements::Section::Global(ref mut gs) = *section {
|
||||||
gs.entries_mut().push(global_entry);
|
gs.entries_mut().push(global_entry);
|
||||||
|
return (gs.entries().len() as u32) - 1;
|
||||||
let stack_height_global_idx = (gs.entries().len() as u32) - 1;
|
|
||||||
ctx.stack_height_global_idx = Some(stack_height_global_idx);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,25 +159,26 @@ fn generate_stack_height_global(ctx: &mut Context, module: &mut elements::Module
|
|||||||
module.sections_mut().push(elements::Section::Global(
|
module.sections_mut().push(elements::Section::Global(
|
||||||
elements::GlobalSection::with_entries(vec![global_entry]),
|
elements::GlobalSection::with_entries(vec![global_entry]),
|
||||||
));
|
));
|
||||||
ctx.stack_height_global_idx = Some(0);
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate stack costs for all functions.
|
/// Calculate stack costs for all functions.
|
||||||
///
|
///
|
||||||
/// Returns a vector with a stack cost for each function, including imports.
|
/// Returns a vector with a stack cost for each function, including imports.
|
||||||
fn compute_stack_costs(ctx: &mut Context, module: &elements::Module) -> Result<(), Error> {
|
fn compute_stack_costs(module: &elements::Module) -> Result<Vec<u32>, Error> {
|
||||||
let func_imports = module.import_count(elements::ImportCountType::Function);
|
let func_imports = module.import_count(elements::ImportCountType::Function);
|
||||||
let mut func_stack_costs = vec![0; module.functions_space()];
|
|
||||||
// TODO: optimize!
|
|
||||||
for (func_idx, func_stack_cost) in func_stack_costs.iter_mut().enumerate() {
|
|
||||||
// We can't calculate stack_cost of the import functions.
|
|
||||||
if func_idx >= func_imports {
|
|
||||||
*func_stack_cost = compute_stack_cost(func_idx as u32, &module)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.func_stack_costs = Some(func_stack_costs);
|
// TODO: optimize!
|
||||||
Ok(())
|
(0..module.functions_space())
|
||||||
|
.map(|func_idx| {
|
||||||
|
if func_idx < func_imports {
|
||||||
|
// We can't calculate stack_cost of the import functions.
|
||||||
|
Ok(0)
|
||||||
|
} else {
|
||||||
|
compute_stack_cost(func_idx as u32, &module)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stack cost of the given *defined* function is the sum of it's locals count (that is,
|
/// Stack cost of the given *defined* function is the sum of it's locals count (that is,
|
||||||
|
@ -21,9 +21,9 @@ pub(crate) fn generate_thunks(
|
|||||||
ctx: &mut Context,
|
ctx: &mut Context,
|
||||||
module: elements::Module,
|
module: elements::Module,
|
||||||
) -> Result<elements::Module, Error> {
|
) -> Result<elements::Module, Error> {
|
||||||
// First, we need to collect all function indicies that should be replaced by thunks
|
// First, we need to collect all function indices that should be replaced by thunks
|
||||||
|
|
||||||
// Function indicies which needs to generate thunks.
|
// Function indices which needs to generate thunks.
|
||||||
let mut need_thunks: Vec<u32> = Vec::new();
|
let mut need_thunks: Vec<u32> = Vec::new();
|
||||||
|
|
||||||
let mut replacement_map: Map<u32, Thunk> = {
|
let mut replacement_map: Map<u32, Thunk> = {
|
||||||
@ -38,11 +38,11 @@ pub(crate) fn generate_thunks(
|
|||||||
let start_func_idx = module
|
let start_func_idx = module
|
||||||
.start_section();
|
.start_section();
|
||||||
|
|
||||||
let exported_func_indicies = exports.iter().filter_map(|entry| match *entry.internal() {
|
let exported_func_indices = exports.iter().filter_map(|entry| match *entry.internal() {
|
||||||
Internal::Function(ref function_idx) => Some(*function_idx),
|
Internal::Function(ref function_idx) => Some(*function_idx),
|
||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
let table_func_indicies = elem_segments
|
let table_func_indices = elem_segments
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|segment| segment.members())
|
.flat_map(|segment| segment.members())
|
||||||
.cloned();
|
.cloned();
|
||||||
@ -50,7 +50,7 @@ pub(crate) fn generate_thunks(
|
|||||||
// Replacement map is at least export section size.
|
// Replacement map is at least export section size.
|
||||||
let mut replacement_map: Map<u32, Thunk> = Map::new();
|
let mut replacement_map: Map<u32, Thunk> = Map::new();
|
||||||
|
|
||||||
for func_idx in exported_func_indicies.chain(table_func_indicies).chain(start_func_idx.into_iter()) {
|
for func_idx in exported_func_indices.chain(table_func_indices).chain(start_func_idx.into_iter()) {
|
||||||
let callee_stack_cost = ctx.stack_cost(func_idx).ok_or_else(|| {
|
let callee_stack_cost = ctx.stack_cost(func_idx).ok_or_else(|| {
|
||||||
Error(format!("function with idx {} isn't found", func_idx))
|
Error(format!("function with idx {} isn't found", func_idx))
|
||||||
})?;
|
})?;
|
||||||
|
@ -122,4 +122,5 @@ mod gas {
|
|||||||
def_gas_test!(simple);
|
def_gas_test!(simple);
|
||||||
def_gas_test!(start);
|
def_gas_test!(start);
|
||||||
def_gas_test!(call);
|
def_gas_test!(call);
|
||||||
|
def_gas_test!(branch);
|
||||||
}
|
}
|
||||||
|
29
tests/expectations/gas/branch.wat
Normal file
29
tests/expectations/gas/branch.wat
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
(module
|
||||||
|
(type (;0;) (func (result i32)))
|
||||||
|
(type (;1;) (func (param i32)))
|
||||||
|
(import "env" "gas" (func (;0;) (type 1)))
|
||||||
|
(func (;1;) (type 0) (result i32)
|
||||||
|
(local i32 i32)
|
||||||
|
i32.const 3
|
||||||
|
call 0
|
||||||
|
block ;; label = @1
|
||||||
|
i32.const 17
|
||||||
|
call 0
|
||||||
|
i32.const 0
|
||||||
|
set_local 0
|
||||||
|
i32.const 1
|
||||||
|
set_local 1
|
||||||
|
get_local 0
|
||||||
|
get_local 1
|
||||||
|
tee_local 0
|
||||||
|
i32.add
|
||||||
|
set_local 1
|
||||||
|
i32.const 1
|
||||||
|
br_if 0 (;@1;)
|
||||||
|
get_local 0
|
||||||
|
get_local 1
|
||||||
|
tee_local 0
|
||||||
|
i32.add
|
||||||
|
set_local 1
|
||||||
|
end
|
||||||
|
get_local 1))
|
27
tests/fixtures/gas/branch.wat
vendored
Normal file
27
tests/fixtures/gas/branch.wat
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
(module
|
||||||
|
(func $fibonacci_with_break (result i32)
|
||||||
|
(local $x i32) (local $y i32)
|
||||||
|
|
||||||
|
(block $unrolled_loop
|
||||||
|
(set_local $x (i32.const 0))
|
||||||
|
(set_local $y (i32.const 1))
|
||||||
|
|
||||||
|
get_local $x
|
||||||
|
get_local $y
|
||||||
|
tee_local $x
|
||||||
|
i32.add
|
||||||
|
set_local $y
|
||||||
|
|
||||||
|
i32.const 1
|
||||||
|
br_if $unrolled_loop
|
||||||
|
|
||||||
|
get_local $x
|
||||||
|
get_local $y
|
||||||
|
tee_local $x
|
||||||
|
i32.add
|
||||||
|
set_local $y
|
||||||
|
)
|
||||||
|
|
||||||
|
get_local $y
|
||||||
|
)
|
||||||
|
)
|
1
tests/fixtures/gas/call.wat
vendored
1
tests/fixtures/gas/call.wat
vendored
@ -2,7 +2,6 @@
|
|||||||
(func $add_locals (param $x i32) (param $y i32) (result i32)
|
(func $add_locals (param $x i32) (param $y i32) (result i32)
|
||||||
(local $t i32)
|
(local $t i32)
|
||||||
|
|
||||||
;; This looks
|
|
||||||
get_local $x
|
get_local $x
|
||||||
get_local $y
|
get_local $y
|
||||||
call $add
|
call $add
|
||||||
|
6
tests/fixtures/gas/ifs.wat
vendored
6
tests/fixtures/gas/ifs.wat
vendored
@ -1,9 +1,9 @@
|
|||||||
(module
|
(module
|
||||||
(func (param $x i32) (result i32)
|
(func (param $x i32) (result i32)
|
||||||
(if (result i32)
|
(if (result i32)
|
||||||
(i32.const 1)
|
(i32.const 1)
|
||||||
(i32.add (get_local $x) (i32.const 1))
|
(then (i32.add (get_local $x) (i32.const 1)))
|
||||||
(i32.popcnt (get_local $x))
|
(else (i32.popcnt (get_local $x)))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
8
tests/fixtures/gas/simple.wat
vendored
8
tests/fixtures/gas/simple.wat
vendored
@ -1,9 +1,11 @@
|
|||||||
(module
|
(module
|
||||||
(func (export "simple")
|
(func (export "simple")
|
||||||
(if (i32.const 1)
|
(if (i32.const 1)
|
||||||
(loop
|
(then
|
||||||
i32.const 123
|
(loop
|
||||||
drop
|
i32.const 123
|
||||||
|
drop
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user