Add new gas metering method: mutable global + local gas function (#34)

* fix misprints in doc comments

* added global gas tracker variable and local gas fn

* all exported functions of the module to accept a new param and to set the gas_left global to its value at their very start

* make module support both gas metering methods

* tests fixed for the old metering method

* better naming

* MutableGlobal metering method implemented, tests for the old method pass

* gas_metering::tests updated and pass

* all tests udpdated and pass

* emacs backup files to .gitignore

* docs updated

* clippy fix

* iff = if and only if

* more clippy

* docs misprints fixes

* refactored to have Backend trait and two implementations in separate sub-modules

* docs updated

* fixed old benches (updating them is coming next)

* added bench for an instrumented wasm-coremark

* updated benches: added them for both gas_metering instrumentations

* benches contest first ver

* added debug prints to the bench

* refactored to better fit frontend-backend pattern

* docs update

* updated benches

* design updated on feedback

* re-structured sub-modules

re-structured sub-modules & updated docs

* docs improved

* addressed latest feedback comments

* re-writed the local gas function

* coremark benches show ~20% performance improvement

* fix ci: test + clippy

* save before re-factoring prepare_in_wasm()

* bare_call_16 shows 16% worse perf

* + fibonacci recursive bench

* refactored benchmarks

* + factorial recursive bench

* benches on wasmi fixtures show no perf improvement, coremark runs ~20% faster being instrumented with mutable_global gas metering

* charge gas for local gas func isntructions execution

* replaced benchmark which requires multi_value feature

* save: optimized gas func a bit (benches work, fixture tests fail)

* 1033% overhead on many_blocks.wasm when mut_global gas_metering together with stack_height

* size overhead test for both gas metering methods + stack limiter

* added more benches

* improved print_size_overhead test

* test for comparing size overheads of two gas_metering injectors

* before optimization: benches + size overhead

* optimization try-1: inline part of gas func instructions: +benches +size overheads

* optimization try-2: inline hot path of gas fn:  +benches +size overheads

* opt try-3: count for gas fn cost on the caller side: +benches +size overhead

* revert to initial version but with static gas fn cost on the caller side: +benches +sizes

* tests fixed

* use newest wasmi 0.20: +benches +docs updated

* use if-else block instead of Return: +benches

* fix tests

* clippy fix

* addressed review comments

* Update changelog

Co-authored-by: Alexander Theißen <alex.theissen@me.com>
This commit is contained in:
Sasha Gryaznov
2022-11-20 16:00:10 +02:00
committed by GitHub
parent 90cb67d5d7
commit a4dde28607
38 changed files with 1775 additions and 183 deletions
+1
View File
@@ -4,3 +4,4 @@ target
.DS_Store
.idea
.vscode
*~
+91
View File
@@ -0,0 +1,91 @@
# Benchmarks
## Table of Contents
- [Benchmark Results](#benchmark-results)
- [coremark, instrumented](#coremark,-instrumented)
- [recursive_ok, instrumented](#recursive_ok,-instrumented)
- [fibonacci_recursive, instrumented](#fibonacci_recursive,-instrumented)
- [factorial_recursive, instrumented](#factorial_recursive,-instrumented)
- [count_until, instrumented](#count_until,-instrumented)
- [memory_vec_add, instrumented](#memory_vec_add,-instrumented)
- [wasm_kernel::tiny_keccak, instrumented](#wasm_kernel::tiny_keccak,-instrumented)
- [global_bump, instrumented](#global_bump,-instrumented)
## Instrumented Modules sizes
| fixture | original size | gas metered/host fn | gas metered/mut global | size diff |
|------------------------------|------------------|---------------------|------------------------|-----------|
| recursive_ok.wat | 0 kb | 0 kb (137%) | 0 kb (177%) | +29% |
| count_until.wat | 0 kb | 0 kb (125%) | 0 kb (153%) | +21% |
| global_bump.wat | 0 kb | 0 kb (123%) | 0 kb (145%) | +18% |
| memory-vec-add.wat | 0 kb | 0 kb (116%) | 0 kb (134%) | +15% |
| factorial.wat | 0 kb | 0 kb (125%) | 0 kb (145%) | +15% |
| fibonacci.wat | 0 kb | 0 kb (121%) | 0 kb (134%) | +10% |
| contract_terminate.wasm | 1 kb | 1 kb (110%) | 1 kb (112%) | +2% |
| coremark_minimal.wasm | 7 kb | 8 kb (114%) | 8 kb (115%) | +0% |
| trait_erc20.wasm | 10 kb | 11 kb (108%) | 11 kb (108%) | +0% |
| rand_extension.wasm | 4 kb | 5 kb (109%) | 5 kb (109%) | +0% |
| multisig.wasm | 27 kb | 30 kb (110%) | 30 kb (110%) | +0% |
| wasm_kernel.wasm | 779 kb | 787 kb (100%) | 795 kb (101%) | +0% |
| many_blocks.wasm | 1023 kb | 2389 kb (233%) | 2389 kb (233%) | +0% |
| contract_transfer.wasm | 7 kb | 8 kb (113%) | 8 kb (113%) | +0% |
| erc1155.wasm | 26 kb | 29 kb (111%) | 29 kb (111%) | +0% |
| erc20.wasm | 9 kb | 10 kb (108%) | 10 kb (109%) | +0% |
| dns.wasm | 10 kb | 11 kb (108%) | 11 kb (108%) | +0% |
| proxy.wasm | 3 kb | 4 kb (108%) | 4 kb (109%) | +0% |
| erc721.wasm | 13 kb | 14 kb (108%) | 14 kb (108%) | +0% |
## Benchmark Results
### coremark, instrumented
| | `with host_function::Injector` | `with mutable_global::Injector` |
|:-------|:----------------------------------------|:----------------------------------------- |
| | `20.81 s` (✅ **1.00x**) | `20.20 s` (✅ **1.03x faster**) |
### recursive_ok, instrumented
| | `with host_function::Injector` | `with mutable_global::Injector` |
|:-------|:----------------------------------------|:----------------------------------------- |
| | `367.11 us` (✅ **1.00x**) | `585.39 us` (❌ *1.59x slower*) |
### fibonacci_recursive, instrumented
| | `with host_function::Injector` | `with mutable_global::Injector` |
|:-------|:----------------------------------------|:----------------------------------------- |
| | `9.15 us` (✅ **1.00x**) | `13.56 us` (❌ *1.48x slower*) |
### factorial_recursive, instrumented
| | `with host_function::Injector` | `with mutable_global::Injector` |
|:-------|:----------------------------------------|:----------------------------------------- |
| | `1.50 us` (✅ **1.00x**) | `1.98 us` (❌ *1.32x slower*) |
### count_until, instrumented
| | `with host_function::Injector` | `with mutable_global::Injector` |
|:-------|:----------------------------------------|:----------------------------------------- |
| | `5.03 ms` (✅ **1.00x**) | `8.13 ms` (❌ *1.62x slower*) |
### memory_vec_add, instrumented
| | `with host_function::Injector` | `with mutable_global::Injector` |
|:-------|:----------------------------------------|:----------------------------------------- |
| | `6.21 ms` (✅ **1.00x**) | `8.45 ms` (❌ *1.36x slower*) |
### wasm_kernel::tiny_keccak, instrumented
| | `with host_function::Injector` | `with mutable_global::Injector` |
|:-------|:----------------------------------------|:----------------------------------------- |
| | `925.22 us` (✅ **1.00x**) | `1.08 ms` (❌ *1.17x slower*) |
### global_bump, instrumented
| | `with host_function::Injector` | `with mutable_global::Injector` |
|:-------|:----------------------------------------|:----------------------------------------- |
| | `3.79 ms` (✅ **1.00x**) | `7.03 ms` (❌ *1.86x slower*) |
---
Made with [criterion-table](https://github.com/nu11ptr/criterion-table)
+7
View File
@@ -16,6 +16,13 @@ The interface provided to smart contracts will adhere to semver with one excepti
major version bumps will be backwards compatible with regard to already deployed contracts.
In other words: Upgrading this pallet will not break pre-existing contracts.
## [Unreleased]
### New
- Add new gas metering method: mutable global + local gas function
[#34](https://github.com/paritytech/wasm-instrument/pull/34)
## [v0.3.0]
### Changed
+5 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "wasm-instrument"
version = "0.3.0"
version = "0.4.0"
edition = "2021"
rust-version = "1.56.1"
authors = ["Parity Technologies <admin@parity.io>"]
@@ -32,8 +32,12 @@ rand = "0.8"
wat = "1"
wasmparser = "0.94"
wasmprinter = "0.2"
wasmi = "0.20"
[features]
default = ["std"]
std = ["parity-wasm/std"]
sign_ext = ["parity-wasm/sign_ext"]
[lib]
bench = false
+528 -5
View File
@@ -1,20 +1,23 @@
use criterion::{
criterion_group, criterion_main, measurement::Measurement, BenchmarkGroup, Criterion,
criterion_group, criterion_main, measurement::Measurement, Bencher, BenchmarkGroup, Criterion,
Throughput,
};
use std::{
fs::{read, read_dir},
path::PathBuf,
slice,
};
use wasm_instrument::{
gas_metering, inject_stack_limiter,
parity_wasm::{deserialize_buffer, elements::Module},
gas_metering::{self, host_function, mutable_global, Backend, ConstantCostRules},
inject_stack_limiter,
parity_wasm::{deserialize_buffer, elements::Module, serialize},
};
fn fixture_dir() -> PathBuf {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("benches");
path.push("fixtures");
path.push("wasm");
path
}
@@ -36,7 +39,12 @@ where
fn gas_metering(c: &mut Criterion) {
let mut group = c.benchmark_group("Gas Metering");
any_fixture(&mut group, |module| {
gas_metering::inject(module, &gas_metering::ConstantCostRules::default(), "env").unwrap();
gas_metering::inject(
module,
host_function::Injector::new("env", "gas"),
&ConstantCostRules::default(),
)
.unwrap();
});
}
@@ -47,5 +55,520 @@ fn stack_height_limiter(c: &mut Criterion) {
});
}
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use wasmi::{
self,
core::{Pages, Value, F32},
Caller, Config, Engine, Extern, Func, Instance, Linker, Memory, StackLimits, Store,
};
fn prepare_module<P: Backend>(backend: P, input: &[u8]) -> (wasmi::Module, Store<u64>) {
let module = deserialize_buffer(input).unwrap();
let instrumented_module =
gas_metering::inject(module, backend, &ConstantCostRules::default()).unwrap();
let input = serialize(instrumented_module).unwrap();
// Prepare wasmi
let engine = Engine::new(&bench_config());
let module = wasmi::Module::new(&engine, &mut &input[..]).unwrap();
// Init host state with maximum gas_left
let store = Store::new(&engine, u64::MAX);
(module, store)
}
fn add_gas_host_func(linker: &mut Linker<u64>, store: &mut Store<u64>) {
// Create gas host function
let host_gas = Func::wrap(store, |mut caller: Caller<'_, u64>, param: u64| {
*caller.host_data_mut() -= param;
});
// Link the gas host function
linker.define("env", "gas", host_gas).unwrap();
}
fn add_gas_left_global(instance: &Instance, mut store: Store<u64>) -> Store<u64> {
instance
.get_export(&mut store, "gas_left")
.and_then(Extern::into_global)
.unwrap()
.set(&mut store, Value::I64(-1i64)) // the same as u64::MAX
.unwrap();
store
}
fn gas_metered_coremark(c: &mut Criterion) {
let mut group = c.benchmark_group("coremark, instrumented");
// Benchmark host_function::Injector
let wasm_filename = "coremark_minimal.wasm";
let bytes = read(fixture_dir().join(wasm_filename)).unwrap();
group.bench_function("with host_function::Injector", |bench| {
let backend = host_function::Injector::new("env", "gas");
let (module, mut store) = prepare_module(backend, &bytes);
// Link the host functions with the imported ones
let mut linker = <Linker<u64>>::new();
add_gas_host_func(&mut linker, &mut store);
// Create clock_ms host function.
let host_clock_ms = Func::wrap(&mut store, || {
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64
});
// Link the time measurer for the coremark wasm
linker.define("env", "clock_ms", host_clock_ms).unwrap();
let instance = linker.instantiate(&mut store, &module).unwrap().start(&mut store).unwrap();
bench.iter(|| {
let run = instance
.get_export(&mut store, "run")
.and_then(Extern::into_func)
.unwrap()
.typed::<(), F32>(&mut store)
.unwrap();
// Call the wasm!
run.call(&mut store, ()).unwrap();
})
});
group.bench_function("with mutable_global::Injector", |bench| {
let backend = mutable_global::Injector::new("gas_left");
let (module, mut store) = prepare_module(backend, &bytes);
// Add the gas_left mutable global
let mut linker = <Linker<u64>>::new();
// Create clock_ms host function.
let host_clock_ms = Func::wrap(&mut store, || {
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64
});
// Link the time measurer for the coremark wasm
linker.define("env", "clock_ms", host_clock_ms).unwrap();
let instance = linker.instantiate(&mut store, &module).unwrap().start(&mut store).unwrap();
let mut store = add_gas_left_global(&instance, store);
bench.iter(|| {
let run = instance
.get_export(&mut store, "run")
.and_then(Extern::into_func)
.unwrap()
.typed::<(), F32>(&mut store)
.unwrap();
// Call the wasm!
run.call(&mut store, ()).unwrap();
})
});
}
/// Converts the `.wat` encoded `bytes` into `.wasm` encoded bytes.
pub fn wat2wasm(bytes: &[u8]) -> Vec<u8> {
wat::parse_bytes(bytes).unwrap().into_owned()
}
/// Returns a [`Config`] useful for benchmarking.
fn bench_config() -> Config {
let mut config = Config::default();
config.set_stack_limits(StackLimits::new(1024, 1024 * 1024, 64 * 1024).unwrap());
config
}
fn gas_metered_recursive_ok(c: &mut Criterion) {
let mut group = c.benchmark_group("recursive_ok, instrumented");
const RECURSIVE_DEPTH: i32 = 8000;
let wasm_bytes = wat2wasm(include_bytes!("fixtures/wat/recursive_ok.wat"));
group.bench_function("with host_function::Injector", |bench| {
let backend = host_function::Injector::new("env", "gas");
let (module, mut store) = prepare_module(backend, &wasm_bytes);
// Link the host function with the imported one
let mut linker = <Linker<u64>>::new();
add_gas_host_func(&mut linker, &mut store);
let instance = linker.instantiate(&mut store, &module).unwrap().start(&mut store).unwrap();
let bench_call = instance.get_export(&store, "call").and_then(Extern::into_func).unwrap();
let mut result = [Value::I32(0)];
bench.iter(|| {
bench_call
.call(&mut store, &[Value::I32(RECURSIVE_DEPTH)], &mut result)
.unwrap();
assert_eq!(result, [Value::I32(0)]);
})
});
group.bench_function("with mutable_global::Injector", |bench| {
let backend = mutable_global::Injector::new("gas_left");
let (module, mut store) = prepare_module(backend, &wasm_bytes);
// Add the gas_left mutable global
let linker = <Linker<u64>>::new();
let instance = linker.instantiate(&mut store, &module).unwrap().start(&mut store).unwrap();
let mut store = add_gas_left_global(&instance, store);
let bench_call = instance.get_export(&store, "call").and_then(Extern::into_func).unwrap();
let mut result = [Value::I32(0)];
bench.iter(|| {
bench_call
.call(&mut store, &[Value::I32(RECURSIVE_DEPTH)], &mut result)
.unwrap();
assert_eq!(result, [Value::I32(0)]);
})
});
}
fn gas_metered_fibonacci_recursive(c: &mut Criterion) {
let mut group = c.benchmark_group("fibonacci_recursive, instrumented");
const FIBONACCI_REC_N: i64 = 10;
let wasm_bytes = wat2wasm(include_bytes!("fixtures/wat/fibonacci.wat"));
group.bench_function("with host_function::Injector", |bench| {
let backend = host_function::Injector::new("env", "gas");
let (module, mut store) = prepare_module(backend, &wasm_bytes);
// Link the host function with the imported one
let mut linker = <Linker<u64>>::new();
add_gas_host_func(&mut linker, &mut store);
let instance = linker.instantiate(&mut store, &module).unwrap().start(&mut store).unwrap();
let bench_call = instance
.get_export(&store, "fib_recursive")
.and_then(Extern::into_func)
.unwrap();
let mut result = [Value::I32(0)];
bench.iter(|| {
bench_call
.call(&mut store, &[Value::I64(FIBONACCI_REC_N)], &mut result)
.unwrap();
});
});
group.bench_function("with mutable_global::Injector", |bench| {
let backend = mutable_global::Injector::new("gas_left");
let (module, mut store) = prepare_module(backend, &wasm_bytes);
// Add the gas_left mutable global
let linker = <Linker<u64>>::new();
let instance = linker.instantiate(&mut store, &module).unwrap().start(&mut store).unwrap();
let mut store = add_gas_left_global(&instance, store);
let bench_call = instance
.get_export(&store, "fib_recursive")
.and_then(Extern::into_func)
.unwrap();
let mut result = [Value::I32(0)];
bench.iter(|| {
bench_call
.call(&mut store, &[Value::I64(FIBONACCI_REC_N)], &mut result)
.unwrap();
});
});
}
fn gas_metered_fac_recursive(c: &mut Criterion) {
let mut group = c.benchmark_group("factorial_recursive, instrumented");
let wasm_bytes = wat2wasm(include_bytes!("fixtures/wat/factorial.wat"));
group.bench_function("with host_function::Injector", |b| {
let backend = host_function::Injector::new("env", "gas");
let (module, mut store) = prepare_module(backend, &wasm_bytes);
// Link the host function with the imported one
let mut linker = <Linker<u64>>::new();
add_gas_host_func(&mut linker, &mut store);
let instance = linker.instantiate(&mut store, &module).unwrap().start(&mut store).unwrap();
let fac = instance
.get_export(&store, "recursive_factorial")
.and_then(Extern::into_func)
.unwrap();
let mut result = [Value::I64(0)];
b.iter(|| {
fac.call(&mut store, &[Value::I64(25)], &mut result).unwrap();
assert_eq!(result, [Value::I64(7034535277573963776)]);
})
});
group.bench_function("with mutable_global::Injector", |b| {
let backend = mutable_global::Injector::new("gas_left");
let (module, mut store) = prepare_module(backend, &wasm_bytes);
// Add the gas_left mutable global
let linker = <Linker<u64>>::new();
let instance = linker.instantiate(&mut store, &module).unwrap().start(&mut store).unwrap();
let mut store = add_gas_left_global(&instance, store);
let fac = instance
.get_export(&store, "recursive_factorial")
.and_then(Extern::into_func)
.unwrap();
let mut result = [Value::I64(0)];
b.iter(|| {
fac.call(&mut store, &[Value::I64(25)], &mut result).unwrap();
assert_eq!(result, [Value::I64(7034535277573963776)]);
})
});
}
fn gas_metered_count_until(c: &mut Criterion) {
const COUNT_UNTIL: i32 = 100_000;
let mut group = c.benchmark_group("count_until, instrumented");
let wasm_bytes = wat2wasm(include_bytes!("fixtures/wat/count_until.wat"));
group.bench_function("with host_function::Injector", |b| {
let backend = host_function::Injector::new("env", "gas");
let (module, mut store) = prepare_module(backend, &wasm_bytes);
// Link the host function with the imported one
let mut linker = <Linker<u64>>::new();
add_gas_host_func(&mut linker, &mut store);
let instance = linker.instantiate(&mut store, &module).unwrap().start(&mut store).unwrap();
let count_until =
instance.get_export(&store, "count_until").and_then(Extern::into_func).unwrap();
let mut result = [Value::I32(0)];
b.iter(|| {
count_until.call(&mut store, &[Value::I32(COUNT_UNTIL)], &mut result).unwrap();
assert_eq!(result, [Value::I32(COUNT_UNTIL)]);
})
});
group.bench_function("with mutable_global::Injector", |b| {
let backend = mutable_global::Injector::new("gas_left");
let (module, mut store) = prepare_module(backend, &wasm_bytes);
// Add the gas_left mutable global
let linker = <Linker<u64>>::new();
let instance = linker.instantiate(&mut store, &module).unwrap().start(&mut store).unwrap();
let mut store = add_gas_left_global(&instance, store);
let count_until =
instance.get_export(&store, "count_until").and_then(Extern::into_func).unwrap();
let mut result = [Value::I32(0)];
b.iter(|| {
count_until.call(&mut store, &[Value::I32(COUNT_UNTIL)], &mut result).unwrap();
assert_eq!(result, [Value::I32(COUNT_UNTIL)]);
})
});
}
fn gas_metered_vec_add(c: &mut Criterion) {
fn test_for<A, B>(
b: &mut Bencher,
vec_add: Func,
mut store: &mut Store<u64>,
mem: Memory,
len: usize,
vec_a: A,
vec_b: B,
) where
A: IntoIterator<Item = i32>,
B: IntoIterator<Item = i32>,
{
use core::mem::size_of;
let ptr_result = 10;
let len_result = len * size_of::<i64>();
let ptr_a = ptr_result + len_result;
let len_a = len * size_of::<i32>();
let ptr_b = ptr_a + len_a;
// Reset `result` buffer to zeros:
mem.data_mut(&mut store)[ptr_result..ptr_result + (len * size_of::<i32>())].fill(0);
// Initialize `a` buffer:
for (n, a) in vec_a.into_iter().take(len).enumerate() {
mem.write(&mut store, ptr_a + (n * size_of::<i32>()), &a.to_le_bytes()).unwrap();
}
// Initialize `b` buffer:
for (n, b) in vec_b.into_iter().take(len).enumerate() {
mem.write(&mut store, ptr_b + (n * size_of::<i32>()), &b.to_le_bytes()).unwrap();
}
// Prepare parameters and all Wasm `vec_add`:
let params = [
Value::I32(ptr_result as i32),
Value::I32(ptr_a as i32),
Value::I32(ptr_b as i32),
Value::I32(len as i32),
];
b.iter(|| {
vec_add.call(&mut store, &params, &mut []).unwrap();
});
// Validate the result buffer:
for n in 0..len {
let mut buffer4 = [0x00; 4];
let mut buffer8 = [0x00; 8];
let a = {
mem.read(&store, ptr_a + (n * size_of::<i32>()), &mut buffer4).unwrap();
i32::from_le_bytes(buffer4)
};
let b = {
mem.read(&store, ptr_b + (n * size_of::<i32>()), &mut buffer4).unwrap();
i32::from_le_bytes(buffer4)
};
let actual_result = {
mem.read(&store, ptr_result + (n * size_of::<i64>()), &mut buffer8).unwrap();
i64::from_le_bytes(buffer8)
};
let expected_result = (a as i64) + (b as i64);
assert_eq!(
expected_result, actual_result,
"given a = {a} and b = {b}, results diverge at index {n}"
);
}
}
let mut group = c.benchmark_group("memory_vec_add, instrumented");
let wasm_bytes = wat2wasm(include_bytes!("fixtures/wat/memory-vec-add.wat"));
const LEN: usize = 100_000;
group.bench_function("with host_function::Injector", |b| {
let backend = host_function::Injector::new("env", "gas");
let (module, mut store) = prepare_module(backend, &wasm_bytes);
// Link the host function with the imported one
let mut linker = <Linker<u64>>::new();
add_gas_host_func(&mut linker, &mut store);
let instance = linker.instantiate(&mut store, &module).unwrap().start(&mut store).unwrap();
let vec_add = instance.get_export(&store, "vec_add").and_then(Extern::into_func).unwrap();
let mem = instance.get_export(&store, "mem").and_then(Extern::into_memory).unwrap();
mem.grow(&mut store, Pages::new(25).unwrap()).unwrap();
test_for(
b,
vec_add,
&mut store,
mem,
LEN,
(0..LEN).map(|i| (i * i) as i32),
(0..LEN).map(|i| (i * 10) as i32),
)
});
group.bench_function("with mutable_global::Injector", |b| {
let backend = mutable_global::Injector::new("gas_left");
let (module, mut store) = prepare_module(backend, &wasm_bytes);
// Add the gas_left mutable global
let linker = <Linker<u64>>::new();
let instance = linker.instantiate(&mut store, &module).unwrap().start(&mut store).unwrap();
let mut store = add_gas_left_global(&instance, store);
let vec_add = instance.get_export(&store, "vec_add").and_then(Extern::into_func).unwrap();
let mem = instance.get_export(&store, "mem").and_then(Extern::into_memory).unwrap();
mem.grow(&mut store, Pages::new(25).unwrap()).unwrap();
test_for(
b,
vec_add,
&mut store,
mem,
LEN,
(0..LEN).map(|i| (i * i) as i32),
(0..LEN).map(|i| (i * 10) as i32),
)
});
}
fn gas_metered_tiny_keccak(c: &mut Criterion) {
let mut group = c.benchmark_group("wasm_kernel::tiny_keccak, instrumented");
let wasm_filename = "wasm_kernel.wasm";
let wasm_bytes = read(fixture_dir().join(wasm_filename)).unwrap();
group.bench_function("with host_function::Injector", |b| {
let backend = host_function::Injector::new("env", "gas");
let (module, mut store) = prepare_module(backend, &wasm_bytes);
// Link the host function with the imported one
let mut linker = <Linker<u64>>::new();
add_gas_host_func(&mut linker, &mut store);
let instance = linker.instantiate(&mut store, &module).unwrap().start(&mut store).unwrap();
let prepare = instance
.get_export(&store, "prepare_tiny_keccak")
.and_then(Extern::into_func)
.unwrap();
let keccak = instance
.get_export(&store, "bench_tiny_keccak")
.and_then(Extern::into_func)
.unwrap();
let mut test_data_ptr = Value::I32(0);
prepare.call(&mut store, &[], slice::from_mut(&mut test_data_ptr)).unwrap();
b.iter(|| {
keccak.call(&mut store, slice::from_ref(&test_data_ptr), &mut []).unwrap();
})
});
group.bench_function("with mutable_global::Injector", |b| {
let backend = mutable_global::Injector::new("gas_left");
let (module, mut store) = prepare_module(backend, &wasm_bytes);
// Add the gas_left mutable global
let linker = <Linker<u64>>::new();
let instance = linker.instantiate(&mut store, &module).unwrap().start(&mut store).unwrap();
let mut store = add_gas_left_global(&instance, store);
let prepare = instance
.get_export(&store, "prepare_tiny_keccak")
.and_then(Extern::into_func)
.unwrap();
let keccak = instance
.get_export(&store, "bench_tiny_keccak")
.and_then(Extern::into_func)
.unwrap();
let mut test_data_ptr = Value::I32(0);
prepare.call(&mut store, &[], slice::from_mut(&mut test_data_ptr)).unwrap();
b.iter(|| {
keccak.call(&mut store, slice::from_ref(&test_data_ptr), &mut []).unwrap();
})
});
}
fn gas_metered_global_bump(c: &mut Criterion) {
const BUMP_AMOUNT: i32 = 100_000;
let mut group = c.benchmark_group("global_bump, instrumented");
let wasm_bytes = wat2wasm(include_bytes!("fixtures/wat/global_bump.wat"));
group.bench_function("with host_function::Injector", |b| {
let backend = host_function::Injector::new("env", "gas");
let (module, mut store) = prepare_module(backend, &wasm_bytes);
// Link the host function with the imported one
let mut linker = <Linker<u64>>::new();
add_gas_host_func(&mut linker, &mut store);
let instance = linker.instantiate(&mut store, &module).unwrap().start(&mut store).unwrap();
let bump = instance.get_export(&store, "bump").and_then(Extern::into_func).unwrap();
let mut result = [Value::I32(0)];
b.iter(|| {
bump.call(&mut store, &[Value::I32(BUMP_AMOUNT)], &mut result).unwrap();
assert_eq!(result, [Value::I32(BUMP_AMOUNT)]);
})
});
group.bench_function("with mutable_global::Injector", |b| {
let backend = mutable_global::Injector::new("gas_left");
let (module, mut store) = prepare_module(backend, &wasm_bytes);
// Add the gas_left mutable global
let linker = <Linker<u64>>::new();
let instance = linker.instantiate(&mut store, &module).unwrap().start(&mut store).unwrap();
let mut store = add_gas_left_global(&instance, store);
let bump = instance.get_export(&store, "bump").and_then(Extern::into_func).unwrap();
let mut result = [Value::I32(0)];
b.iter(|| {
bump.call(&mut store, &[Value::I32(BUMP_AMOUNT)], &mut result).unwrap();
assert_eq!(result, [Value::I32(BUMP_AMOUNT)]);
})
});
}
criterion_group!(benches, gas_metering, stack_height_limiter);
criterion_main!(benches);
criterion_group!(
name = coremark;
config = Criterion::default()
.sample_size(10)
.measurement_time(Duration::from_millis(275000))
.warm_up_time(Duration::from_millis(1000));
targets =
gas_metered_coremark,
);
criterion_group!(
name = wasmi_fixtures;
config = Criterion::default()
.sample_size(10)
.measurement_time(Duration::from_millis(250000))
.warm_up_time(Duration::from_millis(1000));
targets =
gas_metered_recursive_ok,
gas_metered_fibonacci_recursive,
gas_metered_fac_recursive,
gas_metered_count_until,
gas_metered_vec_add,
gas_metered_tiny_keccak,
gas_metered_global_bump,
);
criterion_main!(coremark, wasmi_fixtures);
Binary file not shown.
Binary file not shown.
+25
View File
@@ -0,0 +1,25 @@
;; Exports a function `count_until` that takes an input `n`.
;; The exported function counts an integer `n` times and then returns `n`.
(module
(func $count_until (export "count_until") (param $limit i32) (result i32)
(local $counter i32)
(block
(loop
(br_if
1
(i32.eq
(local.tee $counter
(i32.add
(local.get $counter)
(i32.const 1)
)
)
(local.get $limit)
)
)
(br 0)
)
)
(return (local.get $counter))
)
)
+35
View File
@@ -0,0 +1,35 @@
(module
;; Iterative factorial function, does not use recursion.
(func (export "iterative_factorial") (param i64) (result i64)
(local i64)
(local.set 1 (i64.const 1))
(block
(br_if 0 (i64.lt_s (local.get 0) (i64.const 2)))
(loop
(local.set 1 (i64.mul (local.get 1) (local.get 0)))
(local.set 0 (i64.add (local.get 0) (i64.const -1)))
(br_if 0 (i64.gt_s (local.get 0) (i64.const 1)))
)
)
(local.get 1)
)
;; Recursive trivial factorial function.
(func $rec_fac (export "recursive_factorial") (param i64) (result i64)
(if (result i64)
(i64.eq (local.get 0) (i64.const 0))
(then (i64.const 1))
(else
(i64.mul
(local.get 0)
(call $rec_fac
(i64.sub
(local.get 0)
(i64.const 1)
)
)
)
)
)
)
)
+47
View File
@@ -0,0 +1,47 @@
(module
(func $fib_recursive (export "fib_recursive") (param $N i64) (result i64)
(if
(i64.le_s (local.get $N) (i64.const 1))
(then (return (local.get $N)))
)
(return
(i64.add
(call $fib_recursive
(i64.sub (local.get $N) (i64.const 1))
)
(call $fib_recursive
(i64.sub (local.get $N) (i64.const 2))
)
)
)
)
(func $fib_iterative (export "fib_iterative") (param $N i64) (result i64)
(local $n1 i64)
(local $n2 i64)
(local $tmp i64)
(local $i i64)
;; return $N for N <= 1
(if
(i64.le_s (local.get $N) (i64.const 1))
(then (return (local.get $N)))
)
(local.set $n1 (i64.const 1))
(local.set $n2 (i64.const 1))
(local.set $i (i64.const 2))
;;since we normally return n2, handle n=1 case specially
(loop $again
(if
(i64.lt_s (local.get $i) (local.get $N))
(then
(local.set $tmp (i64.add (local.get $n1) (local.get $n2)))
(local.set $n1 (local.get $n2))
(local.set $n2 (local.get $tmp))
(local.set $i (i64.add (local.get $i) (i64.const 1)))
(br $again)
)
)
)
(local.get $n2)
)
)
+27
View File
@@ -0,0 +1,27 @@
;; Exports a function `bump` that takes an input `n`.
;; The exported function bumps a global variable `n` times and then returns it.
(module
(global $g (mut i32) (i32.const 0))
(func $bump (export "bump") (param $n i32) (result i32)
(global.set $g (i32.const 0))
(block $break
(loop $continue
(br_if ;; if $g == $n then break
$break
(i32.eq
(global.get $g)
(local.get $n)
)
)
(global.set $g ;; $g += 1
(i32.add
(global.get $g)
(i32.const 1)
)
)
(br $continue)
)
)
(return (global.get $g))
)
)
+58
View File
@@ -0,0 +1,58 @@
;; Exports a function `vec_add` that computes the addition of 2 vectors
;; of length `len` starting at `ptr_a` and `ptr_b` and stores the result
;; into a buffer of the same length starting at `ptr_result`.
(module
(memory (export "mem") 1)
(func (export "vec_add")
(param $ptr_result i32)
(param $ptr_a i32)
(param $ptr_b i32)
(param $len i32)
(local $n i32)
(block $exit
(loop $loop
(br_if ;; exit loop if $n == $len
$exit
(i32.eq
(local.get $n)
(local.get $len)
)
)
(i64.store offset=0 ;; ptr_result[n] = ptr_a[n] + ptr_b[n]
(i32.add
(local.get $ptr_result)
(i32.mul
(local.get $n)
(i32.const 8)
)
)
(i64.add
(i64.load32_s offset=0 ;; load ptr_a[n]
(i32.add
(local.get $ptr_a)
(i32.mul
(local.get $n)
(i32.const 4)
)
)
)
(i64.load32_s offset=0 ;; load ptr_b[n]
(i32.add
(local.get $ptr_b)
(i32.mul
(local.get $n)
(i32.const 4)
)
)
)
)
)
(local.set $n ;; increment n
(i32.add (local.get $n) (i32.const 1))
)
(br $loop) ;; continue loop
)
)
(return)
)
)
+22
View File
@@ -0,0 +1,22 @@
;; Exports a function `call` that takes an input `n`.
;; The exported function calls itself `n` times.
(module
(func $call (export "call") (param $n i32) (result i32)
(if (result i32)
(local.get $n)
(then
(return
(call $call
(i32.sub
(local.get $n)
(i32.const 1)
)
)
)
)
(else
(return (local.get $n))
)
)
)
)
+138
View File
@@ -0,0 +1,138 @@
//! Provides backends for the gas metering instrumentation
use parity_wasm::elements;
/// Implementation details of the specific method of the gas metering.
#[derive(Clone)]
pub enum GasMeter {
/// Gas metering with an external function.
External {
/// Name of the module to import the gas function from.
module: &'static str,
/// Name of the external gas function to be imported.
function: &'static str,
},
/// Gas metering with a local function and a mutable global.
Internal {
/// Name of the mutable global to be exported.
global: &'static str,
/// Body of the local gas counting function to be injected.
func_instructions: elements::Instructions,
/// Cost of the gas function execution.
cost: u64,
},
}
use super::Rules;
/// Under the hood part of the gas metering mechanics.
pub trait Backend {
/// Provides the gas metering implementation details.
fn gas_meter<R: Rules>(self, module: &elements::Module, rules: &R) -> GasMeter;
}
/// Gas metering with an external host function.
pub mod host_function {
use super::{Backend, GasMeter, Rules};
use parity_wasm::elements::Module;
/// Injects invocations of the gas charging host function into each metering block.
pub struct Injector {
/// The name of the module to import the gas function from.
module: &'static str,
/// The name of the gas function to import.
name: &'static str,
}
impl Injector {
pub fn new(module: &'static str, name: &'static str) -> Self {
Self { module, name }
}
}
impl Backend for Injector {
fn gas_meter<R: Rules>(self, _module: &Module, _rules: &R) -> GasMeter {
GasMeter::External { module: self.module, function: self.name }
}
}
}
/// Gas metering with a mutable global.
///
/// # Note
///
/// Not for all execution engines this method gives performance wins compared to using an [external
/// host function](host_function). See benchmarks and size overhead tests for examples of how to
/// make measurements needed to decide which gas metering method is better for your particular case.
///
/// # Warning
///
/// It is not recommended to apply [stack limiter](crate::inject_stack_limiter) instrumentation to a
/// module instrumented with this type of gas metering. This could lead to a massive module size
/// bloat. This is a known issue to be fixed in upcoming versions.
pub mod mutable_global {
use super::{Backend, GasMeter, Rules};
use alloc::vec;
use parity_wasm::elements::{self, Instruction, Module};
/// Injects a mutable global variable and a local function to the module to track
/// current gas left.
///
/// The function is called in every metering block. In case of falling out of gas, the global is
/// set to the sentinel value `U64::MAX` and `unreachable` instruction is called. The execution
/// engine should take care of getting the current global value and setting it back in order to
/// sync the gas left value during an execution.
pub struct Injector {
/// The export name of the gas tracking global.
pub global_name: &'static str,
}
impl Injector {
pub fn new(global_name: &'static str) -> Self {
Self { global_name }
}
}
impl Backend for Injector {
fn gas_meter<R: Rules>(self, module: &Module, rules: &R) -> GasMeter {
let gas_global_idx = module.globals_space() as u32;
let func_instructions = vec![
Instruction::GetGlobal(gas_global_idx),
Instruction::GetLocal(0),
Instruction::I64GeU,
Instruction::If(elements::BlockType::NoResult),
Instruction::GetGlobal(gas_global_idx),
Instruction::GetLocal(0),
Instruction::I64Sub,
Instruction::SetGlobal(gas_global_idx),
Instruction::Else,
// sentinel val u64::MAX
Instruction::I64Const(-1i64), // non-charged instruction
Instruction::SetGlobal(gas_global_idx), // non-charged instruction
Instruction::Unreachable, // non-charged instruction
Instruction::End,
Instruction::End,
];
// calculate gas used for the gas charging func execution itself
let mut gas_fn_cost = func_instructions.iter().fold(0, |cost, instruction| {
cost + (rules.instruction_cost(instruction).unwrap_or(0) as u64)
});
// don't charge for the instructions used to fail when out of gas
let fail_cost = vec![
Instruction::I64Const(-1i64), // non-charged instruction
Instruction::SetGlobal(gas_global_idx), // non-charged instruction
Instruction::Unreachable, // non-charged instruction
]
.iter()
.fold(0, |cost, instruction| {
cost + (rules.instruction_cost(instruction).unwrap_or(0) as u64)
});
gas_fn_cost -= fail_cost;
GasMeter::Internal {
global: self.global_name,
func_instructions: elements::Instructions::new(func_instructions),
cost: gas_fn_cost,
}
}
}
}
+379 -100
View File
@@ -1,9 +1,13 @@
//! This module is used to instrument a Wasm module with gas metering code.
//! This module is used to instrument a Wasm module with the gas metering code.
//!
//! The primary public interface is the [`inject`] function which transforms a given
//! module into one that charges gas for code to be executed. See function documentation for usage
//! and details.
mod backend;
pub use backend::{host_function, mutable_global, Backend, GasMeter};
#[cfg(test)]
mod validation;
@@ -67,7 +71,7 @@ impl MemoryGrowCost {
/// # Note
///
/// In a production environment it usually makes no sense to assign every instruction
/// the same cost. A proper implemention of [`Rules`] should be prived that is probably
/// the same cost. A proper implemention of [`Rules`] should be provided that is probably
/// created by benchmarking.
pub struct ConstantCostRules {
instruction_cost: u32,
@@ -101,84 +105,153 @@ impl Rules for ConstantCostRules {
}
}
/// Transforms a given module into one that charges gas for code to be executed by proxy of an
/// imported gas metering function.
/// Transforms a given module into one that tracks the gas charged during its execution.
///
/// The output module imports a function "gas" from the specified module with type signature
/// [i64] -> []. 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 output module uses the `gas` function to track the gas spent. The function could be either
/// an imported or a local one modifying a mutable global. The argument is the amount of gas
/// required to continue execution. The execution engine 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 body of each function is divided into metered blocks, and the calls to charge gas are
/// inserted at the beginning of every such block of code. A metered block is defined so that,
/// unless there is a trap, either all of the instructions are executed or none are. These are
/// similar to basic blocks in a control flow graph, except that in some cases multiple basic
/// blocks can be merged into a single metered block. This is the case if any path through the
/// control flow graph containing one basic block also contains another.
/// The body of each function of the original module is divided into metered blocks, and the calls
/// to charge gas are inserted at the beginning of every such block of code. A metered block is
/// defined so that, unless there is a trap, either all of the instructions are executed or none
/// are. These are similar to basic blocks in a control flow graph, except that in some cases
/// multiple basic blocks can be merged into a single metered block. This is the case if any path
/// through the control flow graph containing one basic block also contains another.
///
/// Charging gas is at the beginning of each metered block ensures that 1) all instructions
/// Charging gas at the beginning of each metered block ensures that 1) all instructions
/// executed are already paid for, 2) instructions that will not be executed are not charged for
/// unless execution traps, and 3) the number of calls to "gas" is minimized. The corollary is that
/// modules instrumented with this metering code may charge gas for instructions not executed in
/// the event of a trap.
/// unless execution traps, and 3) the number of calls to `gas` is minimized. The corollary is
/// that modules instrumented with this metering code may charge gas for instructions not
/// executed in the event of a trap.
///
/// 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`.
/// 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. If the
/// the module has a NameSection, added by calling `parse_names`, the indices will also be updated.
/// the addition of an imported functions changes the indices of module-defined functions. If
/// the module has a `NameSection`, added by calling `parse_names`, the indices will also be
/// updated.
///
/// Syncronizing the amount of gas charged with the execution engine can be done in two ways. The
/// first way is by calling the imported `gas` host function, see [`host_function`] for details. The
/// second way is by using a local `gas` function together with a mutable global, see
/// [`mutable_global`] for details.
///
/// This routine runs in time linear in the size of the input module.
///
/// The function fails if the module contains any operation forbidden by gas rule set, returning
/// the original module as an Err.
pub fn inject<R: Rules>(
/// the original module as an `Err`.
pub fn inject<R: Rules, B: Backend>(
module: elements::Module,
backend: B,
rules: &R,
gas_module_name: &str,
) -> Result<elements::Module, elements::Module> {
// Injecting gas counting external
// Prepare module and return the gas function
let gas_meter = backend.gas_meter(&module, rules);
let import_count = module.import_count(elements::ImportCountType::Function) as u32;
let functions_space = module.functions_space() as u32;
let gas_global_idx = module.globals_space() as u32;
let mut mbuilder = builder::from_module(module);
let import_sig =
mbuilder.push_signature(builder::signature().with_param(ValueType::I64).build_sig());
mbuilder.push_import(
builder::import()
.module(gas_module_name)
.field("gas")
.external()
.func(import_sig)
.build(),
);
// Calculate the indexes and gas function cost,
// for external gas function the cost is counted on the host side
let (gas_func_idx, total_func, gas_fn_cost) = match gas_meter {
GasMeter::External { module: gas_module, function } => {
// Inject the import of the gas function
let import_sig = mbuilder
.push_signature(builder::signature().with_param(ValueType::I64).build_sig());
mbuilder.push_import(
builder::import()
.module(gas_module)
.field(function)
.external()
.func(import_sig)
.build(),
);
// back to plain module
(import_count, functions_space + 1, 0)
},
GasMeter::Internal { global, ref func_instructions, cost } => {
// Inject the gas counting global
mbuilder.push_global(
builder::global()
.with_type(ValueType::I64)
.mutable()
.init_expr(Instruction::I64Const(0))
.build(),
);
// Inject the export entry for the gas counting global
let ebuilder = builder::ExportBuilder::new();
let global_export = ebuilder
.field(global)
.with_internal(elements::Internal::Global(gas_global_idx))
.build();
mbuilder.push_export(global_export);
let func_idx = functions_space;
// Build local gas function
let gas_func_sig =
builder::SignatureBuilder::new().with_param(ValueType::I64).build_sig();
let function = builder::FunctionBuilder::new()
.with_signature(gas_func_sig)
.body()
.with_instructions(func_instructions.clone())
.build()
.build();
// Inject local gas function
mbuilder.push_function(function);
(func_idx, func_idx + 1, cost)
},
};
// We need the built the module for making injections to its blocks
let mut module = mbuilder.build();
// calculate actual function index of the imported definition
// (subtract all imports that are NOT functions)
let gas_func = module.import_count(elements::ImportCountType::Function) as u32 - 1;
let total_func = module.functions_space() as u32;
let mut need_grow_counter = false;
let mut error = false;
// Updating calling addresses (all calls to function index >= `gas_func` should be incremented)
// Iterate over module sections and perform needed transformations.
// Indexes are needed to be fixed up in `GasMeter::External` case, as it adds an imported
// function, which goes to the beginning of the module's functions space.
for section in module.sections_mut() {
match section {
elements::Section::Code(code_section) =>
for func_body in code_section.bodies_mut() {
for instruction in func_body.code_mut().elements_mut().iter_mut() {
if let Instruction::Call(call_index) = instruction {
if *call_index >= gas_func {
*call_index += 1
elements::Section::Code(code_section) => {
let injection_targets = match gas_meter {
GasMeter::External { .. } => code_section.bodies_mut().as_mut_slice(),
// Don't inject counters to the local gas function, which is the last one as
// it's just added. Cost for its execution is added statically before each
// invocation (see `inject_counter()`).
GasMeter::Internal { .. } => {
let len = code_section.bodies().len();
&mut code_section.bodies_mut()[..len - 1]
},
};
for func_body in injection_targets {
// Increment calling addresses if needed
if let GasMeter::External { .. } = gas_meter {
for instruction in func_body.code_mut().elements_mut().iter_mut() {
if let Instruction::Call(call_index) = instruction {
if *call_index >= gas_func_idx {
*call_index += 1
}
}
}
}
if inject_counter(func_body.code_mut(), rules, gas_func).is_err() {
if inject_counter(func_body.code_mut(), gas_fn_cost, rules, gas_func_idx)
.is_err()
{
error = true;
break
}
@@ -187,42 +260,50 @@ pub fn inject<R: Rules>(
{
need_grow_counter = true;
}
},
elements::Section::Export(export_section) => {
for export in export_section.entries_mut() {
if let elements::Internal::Function(func_index) = export.internal_mut() {
if *func_index >= gas_func {
*func_index += 1
}
}
}
},
elements::Section::Export(export_section) =>
if let GasMeter::External { module: _, function: _ } = gas_meter {
for export in export_section.entries_mut() {
if let elements::Internal::Function(func_index) = export.internal_mut() {
if *func_index >= gas_func_idx {
*func_index += 1
}
}
}
},
elements::Section::Element(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 segment in elements_section.entries_mut() {
// update all indirect call addresses initial values
for func_index in segment.members_mut() {
if *func_index >= gas_func {
*func_index += 1
if let GasMeter::External { .. } = gas_meter {
for segment in elements_section.entries_mut() {
// update all indirect call addresses initial values
for func_index in segment.members_mut() {
if *func_index >= gas_func_idx {
*func_index += 1
}
}
}
}
},
elements::Section::Start(start_idx) =>
if *start_idx >= gas_func {
*start_idx += 1
if let GasMeter::External { .. } = gas_meter {
if *start_idx >= gas_func_idx {
*start_idx += 1
}
},
elements::Section::Name(s) =>
for functions in s.functions_mut() {
*functions.names_mut() =
IndexMap::from_iter(functions.names().iter().map(|(mut idx, name)| {
if idx >= gas_func {
idx += 1;
}
if let GasMeter::External { .. } = gas_meter {
for functions in s.functions_mut() {
*functions.names_mut() =
IndexMap::from_iter(functions.names().iter().map(|(mut idx, name)| {
if idx >= gas_func_idx {
idx += 1;
}
(idx, name.clone())
}));
(idx, name.clone())
}));
}
},
_ => {},
}
@@ -233,7 +314,7 @@ pub fn inject<R: Rules>(
}
if need_grow_counter {
Ok(add_grow_counter(module, rules, gas_func))
Ok(add_grow_counter(module, rules, gas_func_idx))
} else {
Ok(module)
}
@@ -558,16 +639,18 @@ fn determine_metered_blocks<R: Rules>(
fn inject_counter<R: Rules>(
instructions: &mut elements::Instructions,
gas_function_cost: u64,
rules: &R,
gas_func: u32,
) -> Result<(), ()> {
let blocks = determine_metered_blocks(instructions, rules)?;
insert_metering_calls(instructions, blocks, gas_func)
insert_metering_calls(instructions, gas_function_cost, blocks, gas_func)
}
// Then insert metering calls into a sequence of instructions given the block locations and costs.
fn insert_metering_calls(
instructions: &mut elements::Instructions,
gas_function_cost: u64,
blocks: Vec<MeteredBlock>,
gas_func: u32,
) -> Result<(), ()> {
@@ -585,7 +668,7 @@ fn insert_metering_calls(
// If there the next block starts at this position, inject metering instructions.
let used_block = if let Some(block) = block_iter.peek() {
if block.start_pos == original_pos {
new_instrs.push(I64Const(block.cost as i64));
new_instrs.push(I64Const((block.cost + gas_function_cost) as i64));
new_instrs.push(Call(gas_func));
true
} else {
@@ -627,7 +710,7 @@ mod tests {
}
#[test]
fn simple_grow() {
fn simple_grow_host_fn() {
let module = parse_wat(
r#"(module
(func (result i32)
@@ -637,8 +720,9 @@ mod tests {
(memory 0 1)
)"#,
);
let injected_module = inject(module, &ConstantCostRules::new(1, 10_000), "env").unwrap();
let backend = host_function::Injector::new("env", "gas");
let injected_module =
super::inject(module, backend, &ConstantCostRules::new(1, 10_000)).unwrap();
assert_eq!(
get_function_body(&injected_module, 0).unwrap(),
@@ -663,7 +747,64 @@ mod tests {
}
#[test]
fn grow_no_gas_no_track() {
fn simple_grow_mut_global() {
let module = parse_wat(
r#"(module
(func (result i32)
global.get 0
memory.grow)
(global i32 (i32.const 42))
(memory 0 1)
)"#,
);
let backend = mutable_global::Injector::new("gas_left");
let injected_module =
super::inject(module, backend, &ConstantCostRules::new(1, 10_000)).unwrap();
assert_eq!(
get_function_body(&injected_module, 0).unwrap(),
&vec![I64Const(13), Call(1), GetGlobal(0), Call(2), End][..]
);
assert_eq!(
get_function_body(&injected_module, 1).unwrap(),
&vec![
Instruction::GetGlobal(1),
Instruction::GetLocal(0),
Instruction::I64GeU,
Instruction::If(elements::BlockType::NoResult),
Instruction::GetGlobal(1),
Instruction::GetLocal(0),
Instruction::I64Sub,
Instruction::SetGlobal(1),
Instruction::Else,
// sentinel val u64::MAX
Instruction::I64Const(-1i64), // non-charged instruction
Instruction::SetGlobal(1), // non-charged instruction
Instruction::Unreachable, // non-charged instruction
Instruction::End,
Instruction::End,
][..]
);
assert_eq!(
get_function_body(&injected_module, 2).unwrap(),
&vec![
GetLocal(0),
GetLocal(0),
I64ExtendUI32,
I64Const(10000),
I64Mul,
Call(1),
GrowMemory(0),
End,
][..]
);
let binary = serialize(injected_module).expect("serialization failed");
wasmparser::validate(&binary).unwrap();
}
#[test]
fn grow_no_gas_no_track_host_fn() {
let module = parse_wat(
r"(module
(func (result i32)
@@ -673,8 +814,9 @@ mod tests {
(memory 0 1)
)",
);
let injected_module = inject(module, &ConstantCostRules::default(), "env").unwrap();
let backend = host_function::Injector::new("env", "gas");
let injected_module =
super::inject(module, backend, &ConstantCostRules::default()).unwrap();
assert_eq!(
get_function_body(&injected_module, 0).unwrap(),
@@ -688,7 +830,33 @@ mod tests {
}
#[test]
fn call_index() {
fn grow_no_gas_no_track_mut_global() {
let module = parse_wat(
r"(module
(func (result i32)
global.get 0
memory.grow)
(global i32 (i32.const 42))
(memory 0 1)
)",
);
let backend = mutable_global::Injector::new("gas_left");
let injected_module =
super::inject(module, backend, &ConstantCostRules::default()).unwrap();
assert_eq!(
get_function_body(&injected_module, 0).unwrap(),
&vec![I64Const(13), Call(1), GetGlobal(0), GrowMemory(0), End][..]
);
assert_eq!(injected_module.functions_space(), 2);
let binary = serialize(injected_module).expect("serialization failed");
wasmparser::validate(&binary).unwrap();
}
#[test]
fn call_index_host_fn() {
let module = builder::module()
.global()
.value_type()
@@ -725,7 +893,9 @@ mod tests {
.build()
.build();
let injected_module = inject(module, &ConstantCostRules::default(), "env").unwrap();
let backend = host_function::Injector::new("env", "gas");
let injected_module =
super::inject(module, backend, &ConstantCostRules::default()).unwrap();
assert_eq!(
get_function_body(&injected_module, 1).unwrap(),
@@ -751,20 +921,89 @@ mod tests {
);
}
#[test]
fn call_index_mut_global() {
let module = builder::module()
.global()
.value_type()
.i32()
.build()
.function()
.signature()
.param()
.i32()
.build()
.body()
.build()
.build()
.function()
.signature()
.param()
.i32()
.build()
.body()
.with_instructions(elements::Instructions::new(vec![
Call(0),
If(elements::BlockType::NoResult),
Call(0),
Call(0),
Call(0),
Else,
Call(0),
Call(0),
End,
Call(0),
End,
]))
.build()
.build()
.build();
let backend = mutable_global::Injector::new("gas_left");
let injected_module =
super::inject(module, backend, &ConstantCostRules::default()).unwrap();
assert_eq!(
get_function_body(&injected_module, 1).unwrap(),
&vec![
I64Const(14),
Call(2),
Call(0),
If(elements::BlockType::NoResult),
I64Const(14),
Call(2),
Call(0),
Call(0),
Call(0),
Else,
I64Const(13),
Call(2),
Call(0),
Call(0),
End,
Call(0),
End
][..]
);
}
fn parse_wat(source: &str) -> elements::Module {
let module_bytes = wat::parse_str(source).unwrap();
elements::deserialize_buffer(module_bytes.as_ref()).unwrap()
}
macro_rules! test_gas_counter_injection {
(name = $name:ident; input = $input:expr; expected = $expected:expr) => {
(names = ($name1:ident, $name2:ident); input = $input:expr; expected = $expected:expr) => {
#[test]
fn $name() {
fn $name1() {
let input_module = parse_wat($input);
let expected_module = parse_wat($expected);
let injected_module = inject(input_module, &ConstantCostRules::default(), "env")
.expect("inject_gas_counter call failed");
let injected_module = super::inject(
input_module,
host_function::Injector::new("env", "gas"),
&ConstantCostRules::default(),
)
.expect("inject_gas_counter call failed");
let actual_func_body = get_function_body(&injected_module, 0)
.expect("injected module must have a function body");
@@ -773,11 +1012,51 @@ mod tests {
assert_eq!(actual_func_body, expected_func_body);
}
#[test]
fn $name2() {
let input_module = parse_wat($input);
let draft_module = parse_wat($expected);
let gas_fun_cost = match mutable_global::Injector::new("gas_left")
.gas_meter(&input_module, &ConstantCostRules::default())
{
GasMeter::Internal { cost, .. } => cost as i64,
_ => 0i64,
};
let injected_module = super::inject(
input_module,
mutable_global::Injector::new("gas_left"),
&ConstantCostRules::default(),
)
.expect("inject_gas_counter call failed");
let actual_func_body = get_function_body(&injected_module, 0)
.expect("injected module must have a function body");
let mut expected_func_body = get_function_body(&draft_module, 0)
.expect("post-module must have a function body")
.to_vec();
// modify expected instructions set for gas_metering::mutable_global
let mut iter = expected_func_body.iter_mut();
while let Some(ins) = iter.next() {
if let I64Const(cost) = ins {
if let Some(ins_next) = iter.next() {
if let Call(0) = ins_next {
*cost += gas_fun_cost;
*ins_next = Call(1);
}
}
}
}
assert_eq!(actual_func_body, &expected_func_body);
}
};
}
test_gas_counter_injection! {
name = simple;
names = (simple_host_fn, simple_mut_global);
input = r#"
(module
(func (result i32)
@@ -792,7 +1071,7 @@ mod tests {
}
test_gas_counter_injection! {
name = nested;
names = (nested_host_fn, nested_mut_global);
input = r#"
(module
(func (result i32)
@@ -817,7 +1096,7 @@ mod tests {
}
test_gas_counter_injection! {
name = ifelse;
names = (ifelse_host_fn, ifelse_mut_global);
input = r#"
(module
(func (result i32)
@@ -852,7 +1131,7 @@ mod tests {
}
test_gas_counter_injection! {
name = branch_innermost;
names = (branch_innermost_host_fn, branch_innermost_mut_global);
input = r#"
(module
(func (result i32)
@@ -882,7 +1161,7 @@ mod tests {
}
test_gas_counter_injection! {
name = branch_outer_block;
names = (branch_outer_block_host_fn, branch_outer_block_mut_global);
input = r#"
(module
(func (result i32)
@@ -921,7 +1200,7 @@ mod tests {
}
test_gas_counter_injection! {
name = branch_outer_loop;
names = (branch_outer_loop_host_fn, branch_outer_loop_mut_global);
input = r#"
(module
(func (result i32)
@@ -967,7 +1246,7 @@ mod tests {
}
test_gas_counter_injection! {
name = return_from_func;
names = (return_from_func_host_fn, return_from_func_mut_global);
input = r#"
(module
(func (result i32)
@@ -992,7 +1271,7 @@ mod tests {
}
test_gas_counter_injection! {
name = branch_from_if_not_else;
names = (branch_from_if_not_else_host_fn, branch_from_if_not_else_mut_global);
input = r#"
(module
(func (result i32)
@@ -1028,7 +1307,7 @@ mod tests {
}
test_gas_counter_injection! {
name = empty_loop;
names = (empty_loop_host_fn, empty_loop_mut_global);
input = r#"
(module
(func
+64 -29
View File
@@ -1,10 +1,9 @@
use parity_wasm::elements::Module;
use std::{
fs,
io::{self, Read, Write},
path::{Path, PathBuf},
};
use wasm_instrument::{self as instrument, parity_wasm::elements};
use wasm_instrument::{self as instrument, gas_metering, parity_wasm::elements};
use wasmparser::validate;
fn slurp<P: AsRef<Path>>(path: P) -> io::Result<Vec<u8>> {
@@ -20,18 +19,23 @@ fn dump<P: AsRef<Path>>(path: P, buf: &[u8]) -> io::Result<()> {
Ok(())
}
fn run_diff_test<F: FnOnce(&[u8]) -> Vec<u8>>(test_dir: &str, name: &str, test: F) {
fn run_diff_test<F: FnOnce(&[u8]) -> Vec<u8>>(
test_dir: &str,
in_name: &str,
out_name: &str,
test: F,
) {
let mut fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
fixture_path.push("tests");
fixture_path.push("fixtures");
fixture_path.push(test_dir);
fixture_path.push(name);
fixture_path.push(in_name);
let mut expected_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
expected_path.push("tests");
expected_path.push("expectations");
expected_path.push(test_dir);
expected_path.push(name);
expected_path.push(out_name);
let fixture_wasm = wat::parse_file(&fixture_path).expect("Failed to read fixture");
validate(&fixture_wasm).expect("Fixture is invalid");
@@ -48,7 +52,7 @@ fn run_diff_test<F: FnOnce(&[u8]) -> Vec<u8>>(test_dir: &str, name: &str, test:
if actual_wat != expected_wat {
println!("difference!");
println!("--- {}", expected_path.display());
println!("+++ {} test {}", test_dir, name);
println!("+++ {} test {}", test_dir, out_name);
for diff in diff::lines(expected_wat, &actual_wat) {
match diff {
diff::Result::Left(l) => println!("-{}", l),
@@ -72,13 +76,18 @@ mod stack_height {
( $name:ident ) => {
#[test]
fn $name() {
run_diff_test("stack-height", concat!(stringify!($name), ".wat"), |input| {
let module =
elements::deserialize_buffer(input).expect("Failed to deserialize");
let instrumented = instrument::inject_stack_limiter(module, 1024)
.expect("Failed to instrument with stack counter");
elements::serialize(instrumented).expect("Failed to serialize")
});
run_diff_test(
"stack-height",
concat!(stringify!($name), ".wat"),
concat!(stringify!($name), ".wat"),
|input| {
let module =
elements::deserialize_buffer(input).expect("Failed to deserialize");
let instrumented = instrument::inject_stack_limiter(module, 1024)
.expect("Failed to instrument with stack counter");
elements::serialize(instrumented).expect("Failed to serialize")
},
);
}
};
}
@@ -96,27 +105,53 @@ mod gas {
use super::*;
macro_rules! def_gas_test {
( $name:ident ) => {
( ($input:ident, $name1:ident, $name2:ident) ) => {
#[test]
fn $name() {
run_diff_test("gas", concat!(stringify!($name), ".wat"), |input| {
let rules = instrument::gas_metering::ConstantCostRules::default();
fn $name1() {
run_diff_test(
"gas",
concat!(stringify!($input), ".wat"),
concat!(stringify!($name1), ".wat"),
|input| {
let rules = gas_metering::ConstantCostRules::default();
let module: Module =
elements::deserialize_buffer(input).expect("Failed to deserialize");
let module = module.parse_names().expect("Failed to parse names");
let module: elements::Module =
elements::deserialize_buffer(input).expect("Failed to deserialize");
let module = module.parse_names().expect("Failed to parse names");
let backend = gas_metering::host_function::Injector::new("env", "gas");
let instrumented = instrument::gas_metering::inject(module, &rules, "env")
.expect("Failed to instrument with gas metering");
elements::serialize(instrumented).expect("Failed to serialize")
});
let instrumented = gas_metering::inject(module, backend, &rules)
.expect("Failed to instrument with gas metering");
elements::serialize(instrumented).expect("Failed to serialize")
},
);
}
#[test]
fn $name2() {
run_diff_test(
"gas",
concat!(stringify!($input), ".wat"),
concat!(stringify!($name2), ".wat"),
|input| {
let rules = gas_metering::ConstantCostRules::default();
let module: elements::Module =
elements::deserialize_buffer(input).expect("Failed to deserialize");
let module = module.parse_names().expect("Failed to parse names");
let backend = gas_metering::mutable_global::Injector::new("gas_left");
let instrumented = gas_metering::inject(module, backend, &rules)
.expect("Failed to instrument with gas metering");
elements::serialize(instrumented).expect("Failed to serialize")
},
);
}
};
}
def_gas_test!(ifs);
def_gas_test!(simple);
def_gas_test!(start);
def_gas_test!(call);
def_gas_test!(branch);
def_gas_test!((ifs, ifs_host_fn, ifs_mut_global));
def_gas_test!((simple, simple_host_fn, simple_mut_global));
def_gas_test!((start, start_host_fn, start_mut_global));
def_gas_test!((call, call_host_fn, call_mut_global));
def_gas_test!((branch, branch_host_fn, branch_mut_global));
}
@@ -0,0 +1,47 @@
(module
(type (;0;) (func (result i32)))
(type (;1;) (func (param i64)))
(func $fibonacci_with_break (;0;) (type 0) (result i32)
(local $x i32) (local $y i32)
i64.const 24
call 1
block ;; label = @1
i32.const 0
local.set $x
i32.const 1
local.set $y
local.get $x
local.get $y
local.tee $x
i32.add
local.set $y
i32.const 1
br_if 0 (;@1;)
i64.const 16
call 1
local.get $x
local.get $y
local.tee $x
i32.add
local.set $y
end
local.get $y
)
(func (;1;) (type 1) (param i64)
global.get 0
local.get 0
i64.ge_u
if ;; label = @1
global.get 0
local.get 0
i64.sub
global.set 0
else
i64.const -1
global.set 0
unreachable
end
)
(global (;0;) (mut i64) i64.const 0)
(export "gas_left" (global 0))
)
@@ -0,0 +1,38 @@
(module
(type (;0;) (func (param i32 i32) (result i32)))
(type (;1;) (func (param i64)))
(func $add_locals (;0;) (type 0) (param $x i32) (param $y i32) (result i32)
(local $t i32)
i64.const 16
call 2
local.get $x
local.get $y
call $add
local.set $t
local.get $t
)
(func $add (;1;) (type 0) (param $x i32) (param $y i32) (result i32)
i64.const 14
call 2
local.get $x
local.get $y
i32.add
)
(func (;2;) (type 1) (param i64)
global.get 0
local.get 0
i64.ge_u
if ;; label = @1
global.get 0
local.get 0
i64.sub
global.set 0
else
i64.const -1
global.set 0
unreachable
end
)
(global (;0;) (mut i64) i64.const 0)
(export "gas_left" (global 0))
)
+38
View File
@@ -0,0 +1,38 @@
(module
(type (;0;) (func (param i32) (result i32)))
(type (;1;) (func (param i64)))
(func (;0;) (type 0) (param $x i32) (result i32)
i64.const 13
call 1
i32.const 1
if (result i32) ;; label = @1
i64.const 14
call 1
local.get $x
i32.const 1
i32.add
else
i64.const 13
call 1
local.get $x
i32.popcnt
end
)
(func (;1;) (type 1) (param i64)
global.get 0
local.get 0
i64.ge_u
if ;; label = @1
global.get 0
local.get 0
i64.sub
global.set 0
else
i64.const -1
global.set 0
unreachable
end
)
(global (;0;) (mut i64) i64.const 0)
(export "gas_left" (global 0))
)
@@ -0,0 +1,43 @@
(module
(type (;0;) (func))
(type (;1;) (func (param i64)))
(func (;0;) (type 0)
i64.const 13
call 2
i32.const 1
if ;; label = @1
i64.const 12
call 2
loop ;; label = @2
i64.const 13
call 2
i32.const 123
drop
end
end
)
(func (;1;) (type 0)
i64.const 12
call 2
block ;; label = @1
end
)
(func (;2;) (type 1) (param i64)
global.get 0
local.get 0
i64.ge_u
if ;; label = @1
global.get 0
local.get 0
i64.sub
global.set 0
else
i64.const -1
global.set 0
unreachable
end
)
(global (;0;) (mut i64) i64.const 0)
(export "simple" (func 0))
(export "gas_left" (global 0))
)
@@ -0,0 +1,36 @@
(module
(type (;0;) (func (param i32 i32)))
(type (;1;) (func))
(type (;2;) (func (param i64)))
(import "env" "ext_return" (func $ext_return (;0;) (type 0)))
(import "env" "memory" (memory (;0;) 1 1))
(func $start (;1;) (type 1)
i64.const 15
call 3
i32.const 8
i32.const 4
call $ext_return
unreachable
)
(func (;2;) (type 1))
(func (;3;) (type 2) (param i64)
global.get 0
local.get 0
i64.ge_u
if ;; label = @1
global.get 0
local.get 0
i64.sub
global.set 0
else
i64.const -1
global.set 0
unreachable
end
)
(global (;0;) (mut i64) i64.const 0)
(export "call" (func 2))
(export "gas_left" (global 0))
(start $start)
(data (;0;) (i32.const 8) "\01\02\03\04")
)
+146 -48
View File
@@ -1,9 +1,10 @@
use std::{
fs::{read, read_dir},
fs::{read, read_dir, ReadDir},
path::PathBuf,
};
use wasm_instrument::{
gas_metering, inject_stack_limiter,
gas_metering::{self, host_function, mutable_global, ConstantCostRules},
inject_stack_limiter,
parity_wasm::{deserialize_buffer, elements::Module, serialize},
};
@@ -14,60 +15,157 @@ fn fixture_dir() -> PathBuf {
path
}
/// Print the overhead of applying gas metering, stack height limiting or both.
///
/// Use `cargo test print_overhead -- --nocapture`.
#[test]
fn print_size_overhead() {
let mut results: Vec<_> = read_dir(fixture_dir())
.unwrap()
use gas_metering::Backend;
fn gas_metered_mod_len<B: Backend>(orig_module: Module, backend: B) -> (Module, usize) {
let module = gas_metering::inject(orig_module, backend, &ConstantCostRules::default()).unwrap();
let bytes = serialize(module.clone()).unwrap();
let len = bytes.len();
(module, len)
}
fn stack_limited_mod_len(module: Module) -> (Module, usize) {
let module = inject_stack_limiter(module, 128).unwrap();
let bytes = serialize(module.clone()).unwrap();
let len = bytes.len();
(module, len)
}
struct InstrumentedWasmResults {
filename: String,
original_module_len: usize,
stack_limited_len: usize,
gas_metered_host_fn_len: usize,
gas_metered_mut_glob_len: usize,
gas_metered_host_fn_then_stack_limited_len: usize,
gas_metered_mut_glob_then_stack_limited_len: usize,
}
fn size_overheads_all(files: ReadDir) -> Vec<InstrumentedWasmResults> {
files
.map(|entry| {
let entry = entry.unwrap();
let (orig_len, orig_module) = {
let bytes = read(&entry.path()).unwrap();
let filename = entry.file_name().into_string().unwrap();
let (original_module_len, orig_module) = {
let bytes = match entry.path().extension().unwrap().to_str() {
Some("wasm") => read(&entry.path()).unwrap(),
Some("wat") =>
wat::parse_bytes(&read(&entry.path()).unwrap()).unwrap().into_owned(),
_ => panic!("expected fixture_dir containing .wasm or .wat files only"),
};
let len = bytes.len();
let module: Module = deserialize_buffer(&bytes).unwrap();
(len, module)
};
let (gas_metering_len, gas_module) = {
let module = gas_metering::inject(
orig_module.clone(),
&gas_metering::ConstantCostRules::default(),
"env",
)
.unwrap();
let bytes = serialize(module.clone()).unwrap();
let len = bytes.len();
(len, module)
};
let stack_height_len = {
let module = inject_stack_limiter(orig_module, 128).unwrap();
let bytes = serialize(module).unwrap();
bytes.len()
};
let both_len = {
let module = inject_stack_limiter(gas_module, 128).unwrap();
let bytes = serialize(module).unwrap();
bytes.len()
};
let overhead = both_len * 100 / orig_len;
let (gm_host_fn_module, gas_metered_host_fn_len) = gas_metered_mod_len(
orig_module.clone(),
host_function::Injector::new("env", "gas"),
);
(
overhead,
format!(
"{:30}: orig = {:4} kb, gas_metering = {} %, stack_limiter = {} %, both = {} %",
entry.file_name().to_str().unwrap(),
orig_len / 1024,
gas_metering_len * 100 / orig_len,
stack_height_len * 100 / orig_len,
overhead,
),
)
let (gm_mut_global_module, gas_metered_mut_glob_len) =
gas_metered_mod_len(orig_module.clone(), mutable_global::Injector::new("gas_left"));
let stack_limited_len = stack_limited_mod_len(orig_module).1;
let (_gm_hf_sl_mod, gas_metered_host_fn_then_stack_limited_len) =
stack_limited_mod_len(gm_host_fn_module);
let (_gm_mg_sl_module, gas_metered_mut_glob_then_stack_limited_len) =
stack_limited_mod_len(gm_mut_global_module);
InstrumentedWasmResults {
filename,
original_module_len,
stack_limited_len,
gas_metered_host_fn_len,
gas_metered_mut_glob_len,
gas_metered_host_fn_then_stack_limited_len,
gas_metered_mut_glob_then_stack_limited_len,
}
})
.collect();
results.sort_unstable_by(|a, b| b.0.cmp(&a.0));
for entry in results {
println!("{}", entry.1);
.collect()
}
fn calc_size_overheads() -> Vec<InstrumentedWasmResults> {
let mut wasm_path = fixture_dir();
wasm_path.push("wasm");
let mut wat_path = fixture_dir();
wat_path.push("wat");
let mut results = size_overheads_all(read_dir(wasm_path).unwrap());
let results_wat = size_overheads_all(read_dir(wat_path).unwrap());
results.extend(results_wat);
results
}
/// Print the overhead of applying gas metering, stack
/// height limiting or both.
///
/// Use `cargo test print_size_overhead -- --nocapture`.
#[test]
fn print_size_overhead() {
let mut results = calc_size_overheads();
results.sort_unstable_by(|a, b| {
b.gas_metered_mut_glob_then_stack_limited_len
.cmp(&a.gas_metered_mut_glob_then_stack_limited_len)
});
for r in results {
let filename = r.filename;
let original_size = r.original_module_len / 1024;
let stack_limit = r.stack_limited_len * 100 / r.original_module_len;
let host_fn = r.gas_metered_host_fn_len * 100 / r.original_module_len;
let mut_glob = r.gas_metered_mut_glob_len * 100 / r.original_module_len;
let host_fn_sl = r.gas_metered_host_fn_then_stack_limited_len * 100 / r.original_module_len;
let mut_glob_sl =
r.gas_metered_mut_glob_then_stack_limited_len * 100 / r.original_module_len;
println!(
"{filename:30}: orig = {original_size:4} kb, stack_limiter = {stack_limit} %, \
gas_metered_host_fn = {host_fn} %, both = {host_fn_sl} %,\n \
{:69} gas_metered_mut_global = {mut_glob} %, both = {mut_glob_sl} %",
""
);
}
}
/// Compare module size overhead of applying gas metering with two methods.
///
/// Use `cargo test print_gas_metered_sizes -- --nocapture`.
#[test]
fn print_gas_metered_sizes() {
let overheads = calc_size_overheads();
let mut results = overheads
.iter()
.map(|r| {
let diff = (r.gas_metered_mut_glob_len * 100 / r.gas_metered_host_fn_len) as i32 - 100;
(diff, r)
})
.collect::<Vec<(i32, &InstrumentedWasmResults)>>();
results.sort_unstable_by(|a, b| b.0.cmp(&a.0));
println!(
"| {:28} | {:^16} | gas metered/host fn | gas metered/mut global | size diff |",
"fixture", "original size",
);
println!("|{:-^30}|{:-^18}|{:-^21}|{:-^24}|{:-^11}|", "", "", "", "", "",);
for r in results {
let filename = &r.1.filename;
let original_size = &r.1.original_module_len / 1024;
let host_fn = &r.1.gas_metered_host_fn_len / 1024;
let mut_glob = &r.1.gas_metered_mut_glob_len / 1024;
let host_fn_percent = &r.1.gas_metered_host_fn_len * 100 / r.1.original_module_len;
let mut_glob_percent = &r.1.gas_metered_mut_glob_len * 100 / r.1.original_module_len;
let host_fn = format!("{host_fn} kb ({host_fn_percent:}%)");
let mut_glob = format!("{mut_glob} kb ({mut_glob_percent:}%)");
let diff = &r.0;
println!(
"| {filename:28} | {original_size:13} kb | {host_fn:>19} | {mut_glob:>22} | {diff:+8}% |"
);
}
}