diff --git a/.gitignore b/.gitignore index 6fc8f03..06b362b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ target .DS_Store .idea .vscode +*~ \ No newline at end of file diff --git a/BENCHMARKS.md b/BENCHMARKS.md new file mode 100644 index 0000000..76aa6d1 --- /dev/null +++ b/BENCHMARKS.md @@ -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) + diff --git a/CHANGELOG.md b/CHANGELOG.md index ba3236c..d9ec4ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.toml b/Cargo.toml index 4319bc2..053486f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 "] @@ -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 diff --git a/benches/benches.rs b/benches/benches.rs index 09e04e6..155bb18 100644 --- a/benches/benches.rs +++ b/benches/benches.rs @@ -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(backend: P, input: &[u8]) -> (wasmi::Module, Store) { + 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, store: &mut Store) { + // 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) -> Store { + 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 = >::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 = >::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 { + 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 = >::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 = >::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 = >::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 = >::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 = >::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 = >::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 = >::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 = >::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( + b: &mut Bencher, + vec_add: Func, + mut store: &mut Store, + mem: Memory, + len: usize, + vec_a: A, + vec_b: B, + ) where + A: IntoIterator, + B: IntoIterator, + { + use core::mem::size_of; + + let ptr_result = 10; + let len_result = len * size_of::(); + let ptr_a = ptr_result + len_result; + let len_a = len * size_of::(); + let ptr_b = ptr_a + len_a; + + // Reset `result` buffer to zeros: + mem.data_mut(&mut store)[ptr_result..ptr_result + (len * size_of::())].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::()), &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::()), &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, ¶ms, &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::()), &mut buffer4).unwrap(); + i32::from_le_bytes(buffer4) + }; + let b = { + mem.read(&store, ptr_b + (n * size_of::()), &mut buffer4).unwrap(); + i32::from_le_bytes(buffer4) + }; + let actual_result = { + mem.read(&store, ptr_result + (n * size_of::()), &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 = >::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 = >::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 = >::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 = >::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 = >::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 = >::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); diff --git a/benches/fixtures/contract_terminate.wasm b/benches/fixtures/wasm/contract_terminate.wasm similarity index 100% rename from benches/fixtures/contract_terminate.wasm rename to benches/fixtures/wasm/contract_terminate.wasm diff --git a/benches/fixtures/contract_transfer.wasm b/benches/fixtures/wasm/contract_transfer.wasm similarity index 100% rename from benches/fixtures/contract_transfer.wasm rename to benches/fixtures/wasm/contract_transfer.wasm diff --git a/benches/fixtures/wasm/coremark_minimal.wasm b/benches/fixtures/wasm/coremark_minimal.wasm new file mode 100755 index 0000000..c5d6b87 Binary files /dev/null and b/benches/fixtures/wasm/coremark_minimal.wasm differ diff --git a/benches/fixtures/dns.wasm b/benches/fixtures/wasm/dns.wasm similarity index 100% rename from benches/fixtures/dns.wasm rename to benches/fixtures/wasm/dns.wasm diff --git a/benches/fixtures/erc1155.wasm b/benches/fixtures/wasm/erc1155.wasm similarity index 100% rename from benches/fixtures/erc1155.wasm rename to benches/fixtures/wasm/erc1155.wasm diff --git a/benches/fixtures/erc20.wasm b/benches/fixtures/wasm/erc20.wasm similarity index 100% rename from benches/fixtures/erc20.wasm rename to benches/fixtures/wasm/erc20.wasm diff --git a/benches/fixtures/erc721.wasm b/benches/fixtures/wasm/erc721.wasm similarity index 100% rename from benches/fixtures/erc721.wasm rename to benches/fixtures/wasm/erc721.wasm diff --git a/benches/fixtures/many_blocks.wasm b/benches/fixtures/wasm/many_blocks.wasm similarity index 100% rename from benches/fixtures/many_blocks.wasm rename to benches/fixtures/wasm/many_blocks.wasm diff --git a/benches/fixtures/multisig.wasm b/benches/fixtures/wasm/multisig.wasm similarity index 100% rename from benches/fixtures/multisig.wasm rename to benches/fixtures/wasm/multisig.wasm diff --git a/benches/fixtures/proxy.wasm b/benches/fixtures/wasm/proxy.wasm similarity index 100% rename from benches/fixtures/proxy.wasm rename to benches/fixtures/wasm/proxy.wasm diff --git a/benches/fixtures/rand_extension.wasm b/benches/fixtures/wasm/rand_extension.wasm similarity index 100% rename from benches/fixtures/rand_extension.wasm rename to benches/fixtures/wasm/rand_extension.wasm diff --git a/benches/fixtures/trait_erc20.wasm b/benches/fixtures/wasm/trait_erc20.wasm similarity index 100% rename from benches/fixtures/trait_erc20.wasm rename to benches/fixtures/wasm/trait_erc20.wasm diff --git a/benches/fixtures/wasm/wasm_kernel.wasm b/benches/fixtures/wasm/wasm_kernel.wasm new file mode 100644 index 0000000..33e55ad Binary files /dev/null and b/benches/fixtures/wasm/wasm_kernel.wasm differ diff --git a/benches/fixtures/wat/count_until.wat b/benches/fixtures/wat/count_until.wat new file mode 100644 index 0000000..d5350e0 --- /dev/null +++ b/benches/fixtures/wat/count_until.wat @@ -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)) + ) +) diff --git a/benches/fixtures/wat/factorial.wat b/benches/fixtures/wat/factorial.wat new file mode 100644 index 0000000..c2e7c2d --- /dev/null +++ b/benches/fixtures/wat/factorial.wat @@ -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) + ) + ) + ) + ) + ) + ) +) diff --git a/benches/fixtures/wat/fibonacci.wat b/benches/fixtures/wat/fibonacci.wat new file mode 100644 index 0000000..4b15519 --- /dev/null +++ b/benches/fixtures/wat/fibonacci.wat @@ -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) + ) +) diff --git a/benches/fixtures/wat/global_bump.wat b/benches/fixtures/wat/global_bump.wat new file mode 100644 index 0000000..5feb835 --- /dev/null +++ b/benches/fixtures/wat/global_bump.wat @@ -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)) + ) +) diff --git a/benches/fixtures/wat/memory-vec-add.wat b/benches/fixtures/wat/memory-vec-add.wat new file mode 100644 index 0000000..81a5242 --- /dev/null +++ b/benches/fixtures/wat/memory-vec-add.wat @@ -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) + ) +) diff --git a/benches/fixtures/wat/recursive_ok.wat b/benches/fixtures/wat/recursive_ok.wat new file mode 100644 index 0000000..19b1827 --- /dev/null +++ b/benches/fixtures/wat/recursive_ok.wat @@ -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)) + ) + ) + ) +) diff --git a/src/gas_metering/backend.rs b/src/gas_metering/backend.rs new file mode 100644 index 0000000..b9d6001 --- /dev/null +++ b/src/gas_metering/backend.rs @@ -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(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(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(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, + } + } + } +} diff --git a/src/gas_metering/mod.rs b/src/gas_metering/mod.rs index 97a7c35..b1facdd 100644 --- a/src/gas_metering/mod.rs +++ b/src/gas_metering/mod.rs @@ -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( +/// the original module as an `Err`. +pub fn inject( module: elements::Module, + backend: B, rules: &R, - gas_module_name: &str, ) -> Result { - // 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( { 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( } 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( fn inject_counter( 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, 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 diff --git a/tests/diff.rs b/tests/diff.rs index ebd097e..60f48ce 100644 --- a/tests/diff.rs +++ b/tests/diff.rs @@ -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>(path: P) -> io::Result> { @@ -20,18 +19,23 @@ fn dump>(path: P, buf: &[u8]) -> io::Result<()> { Ok(()) } -fn run_diff_test Vec>(test_dir: &str, name: &str, test: F) { +fn run_diff_test Vec>( + 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 Vec>(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)); } diff --git a/tests/expectations/gas/branch.wat b/tests/expectations/gas/branch_host_fn.wat similarity index 100% rename from tests/expectations/gas/branch.wat rename to tests/expectations/gas/branch_host_fn.wat diff --git a/tests/expectations/gas/branch_mut_global.wat b/tests/expectations/gas/branch_mut_global.wat new file mode 100644 index 0000000..9cf9650 --- /dev/null +++ b/tests/expectations/gas/branch_mut_global.wat @@ -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)) +) \ No newline at end of file diff --git a/tests/expectations/gas/call.wat b/tests/expectations/gas/call_host_fn.wat similarity index 100% rename from tests/expectations/gas/call.wat rename to tests/expectations/gas/call_host_fn.wat diff --git a/tests/expectations/gas/call_mut_global.wat b/tests/expectations/gas/call_mut_global.wat new file mode 100644 index 0000000..b730b0a --- /dev/null +++ b/tests/expectations/gas/call_mut_global.wat @@ -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)) +) \ No newline at end of file diff --git a/tests/expectations/gas/ifs.wat b/tests/expectations/gas/ifs_host_fn.wat similarity index 100% rename from tests/expectations/gas/ifs.wat rename to tests/expectations/gas/ifs_host_fn.wat diff --git a/tests/expectations/gas/ifs_mut_global.wat b/tests/expectations/gas/ifs_mut_global.wat new file mode 100644 index 0000000..f79aee9 --- /dev/null +++ b/tests/expectations/gas/ifs_mut_global.wat @@ -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)) +) \ No newline at end of file diff --git a/tests/expectations/gas/simple.wat b/tests/expectations/gas/simple_host_fn.wat similarity index 100% rename from tests/expectations/gas/simple.wat rename to tests/expectations/gas/simple_host_fn.wat diff --git a/tests/expectations/gas/simple_mut_global.wat b/tests/expectations/gas/simple_mut_global.wat new file mode 100644 index 0000000..9b75798 --- /dev/null +++ b/tests/expectations/gas/simple_mut_global.wat @@ -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)) +) \ No newline at end of file diff --git a/tests/expectations/gas/start.wat b/tests/expectations/gas/start_host_fn.wat similarity index 100% rename from tests/expectations/gas/start.wat rename to tests/expectations/gas/start_host_fn.wat diff --git a/tests/expectations/gas/start_mut_global.wat b/tests/expectations/gas/start_mut_global.wat new file mode 100644 index 0000000..5a23e4b --- /dev/null +++ b/tests/expectations/gas/start_mut_global.wat @@ -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") +) \ No newline at end of file diff --git a/tests/overhead.rs b/tests/overhead.rs index 066f34c..c629275 100644 --- a/tests/overhead.rs +++ b/tests/overhead.rs @@ -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(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 { + 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 { + 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::>(); + 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}% |" + ); } }