Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: Add benchmarks for estimate dynamic link gas cost #224

Merged
merged 5 commits into from
Aug 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/benchmarking.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
run: rustc --version; cargo --version; rustup --version; rustup target list --installed
- name: Run vm benchmarks (Singlepass)
working-directory: ${{env.working-directory}}/vm
run: cargo bench --no-default-features -- --color never --save-baseline singlepass
run: cargo bench --no-default-features --features bench -- --color never --save-baseline singlepass
- name: Run crypto benchmarks
working-directory: ${{env.working-directory}}/crypto
run: cargo bench -- --color never --save-baseline crypto
3 changes: 3 additions & 0 deletions packages/vm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ staking = ["cosmwasm-std/staking"]
stargate = ["cosmwasm-std/stargate"]
# Use cranelift backend instead of singlepass. This is required for development on Windows.
cranelift = ["wasmer/cranelift"]
# for benchmark
bench = []

[lib]
# See https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options
Expand Down Expand Up @@ -65,3 +67,4 @@ clap = "2.33.3"
[[bench]]
name = "main"
harness = false
required-features = ["bench"]
231 changes: 226 additions & 5 deletions packages/vm/benches/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@ use criterion::{criterion_group, criterion_main, Criterion, PlottingBackend};
use std::time::Duration;
use tempfile::TempDir;

use cosmwasm_std::{coins, Empty};
use cosmwasm_std::{coins, to_vec, Addr, Empty};
use cosmwasm_vm::testing::{
mock_backend, mock_env, mock_info, mock_instance_options, MockApi, MockQuerier, MockStorage,
mock_backend, mock_env, mock_info, mock_instance, mock_instance_options,
mock_instance_with_options, write_data_to_mock_env, MockApi, MockInstanceOptions, MockQuerier,
MockStorage, INSTANCE_CACHE,
};
use cosmwasm_vm::{
call_execute, call_instantiate, features_from_csv, Cache, CacheOptions, Checksum, Instance,
InstanceOptions, Size,
call_execute, call_instantiate, features_from_csv, native_dynamic_link_trampoline_for_bench,
read_region, ref_to_u32, to_u32, write_region, Backend, BackendApi, BackendError,
BackendResult, Cache, CacheOptions, Checksum, Environment, FunctionMetadata, GasInfo, Instance,
InstanceOptions, Querier, Size, Storage, WasmerVal,
};
use std::cell::RefCell;
use wasmer_types::Type;

// Instance
const DEFAULT_MEMORY_LIMIT: Size = Size::mebi(64);
Expand All @@ -18,11 +24,51 @@ const DEFAULT_INSTANCE_OPTIONS: InstanceOptions = InstanceOptions {
gas_limit: DEFAULT_GAS_LIMIT,
print_debug: false,
};

// Cache
const MEMORY_CACHE_SIZE: Size = Size::mebi(200);

static CONTRACT: &[u8] = include_bytes!("../testdata/hackatom.wasm");

// For Dynamic Call
const CALLEE_NAME_ADDR: &str = "callee";
const CALLER_NAME_ADDR: &str = "caller";

// DummyApi is Api with dummy `call_contract` which does nothing
#[derive(Copy, Clone)]
struct DummyApi {}

impl BackendApi for DummyApi {
fn canonical_address(&self, _human: &str) -> BackendResult<Vec<u8>> {
(
Err(BackendError::unknown("not implemented")),
GasInfo::with_cost(0),
)
}

fn human_address(&self, _canonical: &[u8]) -> BackendResult<String> {
(
Err(BackendError::unknown("not implemented")),
GasInfo::with_cost(0),
)
}

fn contract_call<A, S, Q>(
&self,
_caller_env: &Environment<A, S, Q>,
_contract_addr: &str,
_func_info: &FunctionMetadata,
_args: &[WasmerVal],
) -> BackendResult<Box<[WasmerVal]>>
where
A: BackendApi + 'static,
S: Storage + 'static,
Q: Querier + 'static,
{
(Ok(Box::from([])), GasInfo::with_cost(0))
}
}

fn bench_instance(c: &mut Criterion) {
let mut group = c.benchmark_group("Instance");

Expand Down Expand Up @@ -187,6 +233,171 @@ fn bench_cache(c: &mut Criterion) {
group.finish();
}

fn prepare_dynamic_call_data<A: BackendApi>(
callee_address: Addr,
func_info: FunctionMetadata,
caller_env: &mut Environment<A, MockStorage, MockQuerier>,
) -> u32 {
let data = to_vec(&callee_address).unwrap();
let region_ptr = write_data_to_mock_env(caller_env, &data).unwrap();

caller_env.set_callee_function_metadata(Some(func_info));

let serialized_env = to_vec(&mock_env()).unwrap();
caller_env.set_serialized_env(&serialized_env);

let storage = MockStorage::new();
let querier: MockQuerier<Empty> = MockQuerier::new(&[]);
caller_env.move_in(storage, querier);
region_ptr
}

fn bench_dynamic_link(c: &mut Criterion) {
let mut group = c.benchmark_group("DynamicLink");

group.bench_function(
"native_dynamic_link_trampoline with dummy contract_call",
|b| {
let backend = Backend {
api: DummyApi {},
storage: MockStorage::default(),
querier: MockQuerier::new(&[]),
};
let mock_options = MockInstanceOptions::default();
let options = InstanceOptions {
gas_limit: mock_options.gas_limit,
print_debug: mock_options.print_debug,
};
let instance =
Instance::from_code(CONTRACT, backend, options, mock_options.memory_limit).unwrap();
let mut dummy_env = instance.env;
let callee_address = Addr::unchecked(CALLEE_NAME_ADDR);
let target_func_info = FunctionMetadata {
module_name: CALLEE_NAME_ADDR.to_string(),
name: "foo".to_string(),
signature: ([Type::I32], []).into(),
};

let address_region =
prepare_dynamic_call_data(callee_address, target_func_info, &mut dummy_env);

b.iter(|| {
let _ = native_dynamic_link_trampoline_for_bench(
&dummy_env,
&[WasmerVal::I32(address_region as i32)],
)
.unwrap();
})
},
);

group.bench_function(
"native_dynamic_link_trampoline with mock cache environment",
|b| {
let callee_wasm = wat::parse_str(
r#"(module
(memory 3)
(export "memory" (memory 0))
(export "interface_version_5" (func 0))
(export "instantiate" (func 0))
(export "allocate" (func 0))
(export "deallocate" (func 0))
(type (func))
(func (type 0) nop)
(export "foo" (func 0))
)"#,
)
.unwrap();

INSTANCE_CACHE.with(|lock| {
let mut cache = lock.write().unwrap();
cache.insert(
CALLEE_NAME_ADDR.to_string(),
RefCell::new(mock_instance(&callee_wasm, &[])),
);
cache.insert(
CALLER_NAME_ADDR.to_string(),
RefCell::new(mock_instance_with_options(
&CONTRACT,
MockInstanceOptions {
// enough gas for bench iterations
gas_limit: 500_000_000_000_000_000,
..MockInstanceOptions::default()
},
)),
);
});

INSTANCE_CACHE.with(|lock| {
let cache = lock.read().unwrap();
let caller_instance = cache.get(CALLER_NAME_ADDR).unwrap();
let mut caller_env = &mut caller_instance.borrow_mut().env;
let target_func_info = FunctionMetadata {
module_name: CALLER_NAME_ADDR.to_string(),
name: "foo".to_string(),
signature: ([Type::I32], []).into(),
};
let address_region = prepare_dynamic_call_data(
Addr::unchecked(CALLEE_NAME_ADDR),
target_func_info,
&mut caller_env,
);

b.iter(|| {
let _ = native_dynamic_link_trampoline_for_bench(
&caller_env,
&[WasmerVal::I32(address_region as i32)],
)
.unwrap();
})
});
},
);

group.finish()
}

fn bench_copy_region(c: &mut Criterion) {
let mut group = c.benchmark_group("CopyRegion");

for i in 0..=3 {
let length = 10_i32.pow(i);
group.bench_function(format!("read region (length == {})", length), |b| {
let data: Vec<u8> = (0..length).map(|x| (x % 255) as u8).collect();
assert_eq!(data.len(), length as usize);
let instance = mock_instance(&CONTRACT, &[]);
let ret = instance
.env
.call_function1("allocate", &[to_u32(data.len()).unwrap().into()])
.unwrap();
let region_ptr = ref_to_u32(&ret).unwrap();
write_region(&instance.env.memory(), region_ptr, &data).unwrap();
let got_data =
read_region(&instance.env.memory(), region_ptr, u32::MAX as usize).unwrap();
assert_eq!(data, got_data);
b.iter(|| {
let _ = read_region(&instance.env.memory(), region_ptr, u32::MAX as usize);
})
});

group.bench_function(format!("write region (length == {})", length), |b| {
let data: Vec<u8> = (0..length).map(|x| (x % 255) as u8).collect();
assert_eq!(data.len(), length as usize);
let instance = mock_instance(&CONTRACT, &[]);
let ret = instance
.env
.call_function1("allocate", &[to_u32(data.len()).unwrap().into()])
.unwrap();
let region_ptr = ref_to_u32(&ret).unwrap();
b.iter(|| {
write_region(&instance.env.memory(), region_ptr, &data).unwrap();
})
});
}

group.finish();
}

fn make_config() -> Criterion {
Criterion::default()
.plotting_backend(PlottingBackend::Plotters)
Expand All @@ -205,4 +416,14 @@ criterion_group!(
config = make_config();
targets = bench_cache
);
criterion_main!(instance, cache);
criterion_group!(
name = dynamic_link;
config = make_config();
targets = bench_dynamic_link
);
criterion_group!(
name = copy_region;
config = make_config();
targets = bench_copy_region
);
criterion_main!(instance, cache, dynamic_link, copy_region);
13 changes: 13 additions & 0 deletions packages/vm/src/dynamic_link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,19 @@ where
})
}

#[cfg(feature = "bench")]
pub fn native_dynamic_link_trampoline_for_bench<A: BackendApi, S: Storage, Q: Querier>(
env: &Environment<A, S, Q>,
args: &[WasmerVal],
) -> Result<Vec<WasmerVal>, RuntimeError>
where
A: BackendApi + 'static,
S: Storage + 'static,
Q: Querier + 'static,
{
native_dynamic_link_trampoline(env, args)
}

pub fn dynamic_link<A: BackendApi, S: Storage, Q: Querier>(
module: &Module,
env: &Environment<A, S, Q>,
Expand Down
7 changes: 6 additions & 1 deletion packages/vm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ pub use crate::calls::{
call_sudo_raw,
};
pub use crate::checksum::Checksum;

#[cfg(feature = "bench")]
pub use crate::conversion::{ref_to_u32, to_u32};
#[cfg(feature = "bench")]
pub use crate::dynamic_link::native_dynamic_link_trampoline_for_bench;
pub use crate::dynamic_link::{
copy_region_vals_between_env, dynamic_link, FunctionMetadata, WasmerVal,
};
Expand All @@ -53,6 +56,8 @@ pub use crate::ibc_calls::{
call_ibc_packet_receive_raw, call_ibc_packet_timeout, call_ibc_packet_timeout_raw,
};
pub use crate::instance::{GasReport, Instance, InstanceOptions};
#[cfg(feature = "bench")]
pub use crate::memory::{read_region, write_region};
pub use crate::serde::{from_slice, to_vec};
pub use crate::size::Size;

Expand Down
13 changes: 7 additions & 6 deletions packages/vm/src/testing/environment.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
use super::{MockApi, MockQuerier, MockStorage};
use super::{MockQuerier, MockStorage};
use crate::conversion::ref_to_u32;
use crate::memory::{read_region, write_region};
use crate::BackendApi;
use crate::Environment;
use crate::VmResult;
use crate::WasmerVal;

pub fn write_data_to_mock_env(
env: &Environment<MockApi, MockStorage, MockQuerier>,
pub fn write_data_to_mock_env<A: BackendApi>(
env: &Environment<A, MockStorage, MockQuerier>,
data: &[u8],
) -> VmResult<u32> {
) -> VmResult<u32> where {
let result = env.call_function1("allocate", &[(data.len() as u32).into()])?;
let region_ptr = ref_to_u32(&result)?;
write_region(&env.memory(), region_ptr, data)?;
Ok(region_ptr)
}

pub fn read_data_from_mock_env(
env: &Environment<MockApi, MockStorage, MockQuerier>,
pub fn read_data_from_mock_env<A: BackendApi>(
env: &Environment<A, MockStorage, MockQuerier>,
wasm_ptr: &WasmerVal,
size: usize,
) -> VmResult<Vec<u8>> {
Expand Down
1 change: 1 addition & 0 deletions packages/vm/src/testing/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ impl BackendApi for MockApi {
};
(result, gas_info)
}

fn contract_call<A, S, Q>(
&self,
caller_env: &Environment<A, S, Q>,
Expand Down