contracts: Add automated weights for wasm instructions (#7361)

* pallet_contracts: Inline benchmark helper that is only used once

* Move all max_* Schedule items into a new struct

* Limit the number of globals a module can declare

* The current limits are too high for wasmi to even execute

* Limit the amount of parameters any wasm function is allowed to have

* Limit the size the BrTable's immediate value

* Add instruction benchmarks

* Add new benchmarks to the schedule and make use of it

* Add Benchmark Results generated by the bench bot

* Add proc macro that implements `Debug` for `Schedule`

* Add missing imports necessary for no_std build

* Make the WeightDebug macro available for no_std

In this case a dummy implementation is derived in order to not
blow up the code size akin to the RuntimeDebug macro.

* Rework instr_memory_grow benchmark to use only the maximum amount of pages allowed

* Add maximum amount of memory when benching (seal_)call/instantiate

* cargo run --release --features runtime-benchmarks --manifest-path bin/node/cli/Cargo.toml -- benchmark --chain dev --steps 50 --repeat 20 --extrinsic * --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output ./bin/node/runtime/src/weights --header ./HEADER --pallet pallet_contracts

* Added utility benchmark that allows pretty printing of the real schedule

* review: Add missing header to the proc-macro lib.rs

* review: Clarify why #[allow(dead_code)] attribute is there

* review: Fix pwasm-utils line

* review: Fixup rand usage

* review: Fix typo

* review: Imported -> Exported

* cargo run --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_contracts --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/contracts/src/weights.rs --template=./.maintain/frame-weight-template.hbs

* contracts: Adapt to new weight structure

* contracts: Fixup runtime WeightInfo

* contracts: Remove unneeded fullpath of WeightInfo type

* Apply suggestions from code review

Co-authored-by: Andrew Jones <ascjones@gmail.com>

* Fix typo in schedule.rs

Co-authored-by: Andrew Jones <ascjones@gmail.com>

* Fix docs in schedule.rs

* Apply suggestions from code review

Co-authored-by: Nikolay Volf <nikvolf@gmail.com>

* Don't publish proc-macro crate until 3.0.0 is ready

* Optimize imports for less repetition

* Break overlong line

Co-authored-by: Parity Benchmarking Bot <admin@parity.io>
Co-authored-by: Andrew Jones <ascjones@gmail.com>
Co-authored-by: Nikolay Volf <nikvolf@gmail.com>
This commit is contained in:
Alexander Theißen
2020-11-09 15:32:14 +01:00
committed by GitHub
parent 9704c204e6
commit 51c67fe881
17 changed files with 3152 additions and 843 deletions
@@ -28,28 +28,50 @@ use crate::Trait;
use crate::Module as Contracts;
use parity_wasm::elements::{Instruction, Instructions, FuncBody, ValueType, BlockType};
use pwasm_utils::stack_height::inject_limiter;
use sp_runtime::traits::Hash;
use sp_sandbox::{EnvironmentDefinitionBuilder, Memory};
use sp_std::{prelude::*, convert::TryFrom};
/// Pass to `create_code` in order to create a compiled `WasmModule`.
///
/// This exists to have a more declarative way to describe a wasm module than to use
/// parity-wasm directly. It is tailored to fit the structure of contracts that are
/// needed for benchmarking.
#[derive(Default)]
pub struct ModuleDefinition {
pub data_segments: Vec<DataSegment>,
/// Imported memory attached to the module. No memory is imported if `None`.
pub memory: Option<ImportedMemory>,
/// Initializers for the imported memory.
pub data_segments: Vec<DataSegment>,
/// Creates the supplied amount of i64 mutable globals initialized with random values.
pub num_globals: u32,
/// List of functions that the module should import. They start with index 0.
pub imported_functions: Vec<ImportedFunction>,
/// Function body of the exported `deploy` function. Body is empty if `None`.
/// Its index is `imported_functions.len()`.
pub deploy_body: Option<FuncBody>,
/// Function body of the exported `call` function. Body is empty if `None`.
/// Its index is `imported_functions.len() + 1`.
pub call_body: Option<FuncBody>,
/// Function body of a non-exported function with index `imported_functions.len() + 2`.
pub aux_body: Option<FuncBody>,
/// The amount of I64 arguments the aux function should have.
pub aux_arg_num: u32,
/// If set to true the stack height limiter is injected into the the module. This is
/// needed for instruction debugging because the cost of executing the stack height
/// instrumentation should be included in the costs for the individual instructions
/// that cause more metering code (only call).
pub inject_stack_metering: bool,
/// Create a table containing function pointers.
pub table: Option<TableSegment>,
}
impl Default for ModuleDefinition {
fn default() -> Self {
Self {
data_segments: vec![],
memory: None,
imported_functions: vec![],
deploy_body: None,
call_body: None,
}
}
pub struct TableSegment {
/// How many elements should be created inside the table.
pub num_elements: u32,
/// The function index with which all table elements should be initialized.
pub function_index: u32,
}
pub struct DataSegment {
@@ -57,6 +79,7 @@ pub struct DataSegment {
pub value: Vec<u8>,
}
#[derive(Clone)]
pub struct ImportedMemory {
pub min_pages: u32,
pub max_pages: u32,
@@ -80,6 +103,7 @@ pub struct ImportedFunction {
pub struct WasmModule<T:Trait> {
pub code: Vec<u8>,
pub hash: <T::Hashing as Hash>::Output,
memory: Option<ImportedMemory>,
}
impl<T: Trait> From<ModuleDefinition> for WasmModule<T> {
@@ -91,14 +115,14 @@ impl<T: Trait> From<ModuleDefinition> for WasmModule<T> {
let mut contract = parity_wasm::builder::module()
// deploy function (first internal function)
.function()
.signature().with_params(vec![]).with_return_type(None).build()
.signature().with_return_type(None).build()
.with_body(def.deploy_body.unwrap_or_else(||
FuncBody::new(Vec::new(), Instructions::empty())
))
.build()
// call function (second internal function)
.function()
.signature().with_params(vec![]).with_return_type(None).build()
.signature().with_return_type(None).build()
.with_body(def.call_body.unwrap_or_else(||
FuncBody::new(Vec::new(), Instructions::empty())
))
@@ -106,8 +130,19 @@ impl<T: Trait> From<ModuleDefinition> for WasmModule<T> {
.export().field("deploy").internal().func(func_offset).build()
.export().field("call").internal().func(func_offset + 1).build();
// If specified we add an additional internal function
if let Some(body) = def.aux_body {
let mut signature = contract
.function()
.signature().with_return_type(None);
for _ in 0 .. def.aux_arg_num {
signature = signature.with_param(ValueType::I64);
}
contract = signature.build().with_body(body).build();
}
// Grant access to linear memory.
if let Some(memory) = def.memory {
if let Some(memory) = &def.memory {
contract = contract.import()
.module("env").field("memory")
.external().memory(memory.min_pages, Some(memory.max_pages))
@@ -136,20 +171,69 @@ impl<T: Trait> From<ModuleDefinition> for WasmModule<T> {
.build()
}
let code = contract.build().to_bytes().unwrap();
// Add global variables
if def.num_globals > 0 {
use rand::{prelude::*, distributions::Standard};
let rng = rand_pcg::Pcg32::seed_from_u64(3112244599778833558);
for val in rng.sample_iter(Standard).take(def.num_globals as usize) {
contract = contract
.global()
.value_type().i64()
.mutable()
.init_expr(Instruction::I64Const(val))
.build()
}
}
// Add function pointer table
if let Some(table) = def.table {
contract = contract
.table()
.with_min(table.num_elements)
.with_max(Some(table.num_elements))
.with_element(0, vec![table.function_index; table.num_elements as usize])
.build();
}
let mut code = contract.build();
// Inject stack height metering
if def.inject_stack_metering {
code = inject_limiter(
code,
Contracts::<T>::current_schedule().limits.stack_height
)
.unwrap();
}
let code = code.to_bytes().unwrap();
let hash = T::Hashing::hash(&code);
Self {
code,
hash
hash,
memory: def.memory,
}
}
}
impl<T: Trait> WasmModule<T> {
/// Creates a wasm module with an empty `call` and `deploy` function and nothing else.
pub fn dummy() -> Self {
ModuleDefinition::default().into()
}
/// Same as `dummy` but with maximum sized linear memory.
pub fn dummy_with_mem() -> Self {
ModuleDefinition {
memory: Some(ImportedMemory::max::<T>()),
.. Default::default()
}
.into()
}
/// Creates a wasm module of `target_bytes` size. Used to benchmark the performance of
/// `put_code` for different sizes of wasm modules. The generated module maximizes
/// instrumentation runtime by nesting blocks as deeply as possible given the byte budget.
pub fn sized(target_bytes: u32) -> Self {
use parity_wasm::elements::Instruction::{If, I32Const, Return, End};
// Base size of a contract is 47 bytes and each expansion adds 6 bytes.
@@ -171,6 +255,9 @@ impl<T: Trait> WasmModule<T> {
.into()
}
/// Creates a wasm module that calls the imported function named `getter_name` `repeat`
/// times. The imported function is expected to have the "getter signature" of
/// (out_ptr: u32, len_ptr: u32) -> ().
pub fn getter(getter_name: &'static str, repeat: u32) -> Self {
let pages = max_pages::<T>();
ModuleDefinition {
@@ -198,11 +285,14 @@ impl<T: Trait> WasmModule<T> {
.into()
}
/// Creates a wasm module that calls the imported hash function named `name` `repeat` times
/// with an input of size `data_size`. Hash functions have the signature
/// (input_ptr: u32, input_len: u32, output_ptr: u32) -> ()
pub fn hasher(name: &'static str, repeat: u32, data_size: u32) -> Self {
ModuleDefinition {
memory: Some(ImportedMemory::max::<T>()),
imported_functions: vec![ImportedFunction {
name: name,
name,
params: vec![ValueType::I32, ValueType::I32, ValueType::I32],
return_type: None,
}],
@@ -216,16 +306,84 @@ impl<T: Trait> WasmModule<T> {
}
.into()
}
/// Creates a memory instance for use in a sandbox with dimensions declared in this module
/// and adds it to `env`. A reference to that memory is returned so that it can be used to
/// access the memory contents from the supervisor.
pub fn add_memory<S>(&self, env: &mut EnvironmentDefinitionBuilder<S>) -> Option<Memory> {
let memory = if let Some(memory) = &self.memory {
memory
} else {
return None;
};
let memory = Memory::new(memory.min_pages, Some(memory.max_pages)).unwrap();
env.add_memory("env", "memory", memory.clone());
Some(memory)
}
pub fn unary_instr(instr: Instruction, repeat: u32) -> Self {
use body::DynInstr::{RandomI64Repeated, Regular};
ModuleDefinition {
call_body: Some(body::repeated_dyn(repeat, vec![
RandomI64Repeated(1),
Regular(instr),
Regular(Instruction::Drop),
])),
.. Default::default()
}.into()
}
pub fn binary_instr(instr: Instruction, repeat: u32) -> Self {
use body::DynInstr::{RandomI64Repeated, Regular};
ModuleDefinition {
call_body: Some(body::repeated_dyn(repeat, vec![
RandomI64Repeated(2),
Regular(instr),
Regular(Instruction::Drop),
])),
.. Default::default()
}.into()
}
}
/// Mechanisms to create a function body that can be used inside a `ModuleDefinition`.
/// Mechanisms to generate a function body that can be used inside a `ModuleDefinition`.
pub mod body {
use super::*;
pub enum CountedInstruction {
// (offset, increment_by)
Counter(u32, u32),
/// When generating contract code by repeating a wasm sequence, it's sometimes necessary
/// to change those instructions on each repetition. The variants of this enum describe
/// various ways in which this can happen.
pub enum DynInstr {
/// Insert the associated instruction.
Regular(Instruction),
/// Insert a I32Const with incrementing value for each insertion.
/// (start_at, increment_by)
Counter(u32, u32),
/// Insert a I32Const with a random value in [low, high) not divisible by two.
/// (low, high)
RandomUnaligned(u32, u32),
/// Insert a I32Const with a random value in [low, high).
/// (low, high)
RandomI32(i32, i32),
/// Insert the specified amount of I32Const with a random value.
RandomI32Repeated(usize),
/// Insert the specified amount of I64Const with a random value.
RandomI64Repeated(usize),
/// Insert a GetLocal with a random offset in [low, high).
/// (low, high)
RandomGetLocal(u32, u32),
/// Insert a SetLocal with a random offset in [low, high).
/// (low, high)
RandomSetLocal(u32, u32),
/// Insert a TeeLocal with a random offset in [low, high).
/// (low, high)
RandomTeeLocal(u32, u32),
/// Insert a GetGlobal with a random offset in [low, high).
/// (low, high)
RandomGetGlobal(u32, u32),
/// Insert a SetGlobal with a random offset in [low, high).
/// (low, high)
RandomSetGlobal(u32, u32)
}
pub fn plain(instructions: Vec<Instruction>) -> FuncBody {
@@ -245,28 +403,73 @@ pub mod body {
FuncBody::new(Vec::new(), instructions)
}
pub fn counted(repetitions: u32, mut instructions: Vec<CountedInstruction>) -> FuncBody {
pub fn repeated_dyn(repetitions: u32, mut instructions: Vec<DynInstr>) -> FuncBody {
use rand::{prelude::*, distributions::Standard};
// We do not need to be secure here.
let mut rng = rand_pcg::Pcg32::seed_from_u64(8446744073709551615);
// We need to iterate over indices because we cannot cycle over mutable references
let body = (0..instructions.len())
.cycle()
.take(instructions.len() * usize::try_from(repetitions).unwrap())
.map(|idx| {
.flat_map(|idx|
match &mut instructions[idx] {
CountedInstruction::Counter(offset, increment_by) => {
DynInstr::Regular(instruction) => vec![instruction.clone()],
DynInstr::Counter(offset, increment_by) => {
let current = *offset;
*offset += *increment_by;
Instruction::I32Const(current as i32)
vec![Instruction::I32Const(current as i32)]
},
DynInstr::RandomUnaligned(low, high) => {
let unaligned = rng.gen_range(*low, *high) | 1;
vec![Instruction::I32Const(unaligned as i32)]
},
DynInstr::RandomI32(low, high) => {
vec![Instruction::I32Const(rng.gen_range(*low, *high))]
},
DynInstr::RandomI32Repeated(num) => {
(&mut rng).sample_iter(Standard).take(*num).map(|val|
Instruction::I32Const(val)
)
.collect()
},
DynInstr::RandomI64Repeated(num) => {
(&mut rng).sample_iter(Standard).take(*num).map(|val|
Instruction::I64Const(val)
)
.collect()
},
DynInstr::RandomGetLocal(low, high) => {
vec![Instruction::GetLocal(rng.gen_range(*low, *high))]
},
DynInstr::RandomSetLocal(low, high) => {
vec![Instruction::SetLocal(rng.gen_range(*low, *high))]
},
DynInstr::RandomTeeLocal(low, high) => {
vec![Instruction::TeeLocal(rng.gen_range(*low, *high))]
},
DynInstr::RandomGetGlobal(low, high) => {
vec![Instruction::GetGlobal(rng.gen_range(*low, *high))]
},
DynInstr::RandomSetGlobal(low, high) => {
vec![Instruction::SetGlobal(rng.gen_range(*low, *high))]
},
CountedInstruction::Regular(instruction) => instruction.clone(),
}
})
)
.chain(sp_std::iter::once(Instruction::End))
.collect();
FuncBody::new(Vec::new(), Instructions::new(body))
}
/// Replace the locals of the supplied `body` with `num` i64 locals.
pub fn inject_locals(body: &mut FuncBody, num: u32) {
use parity_wasm::elements::Local;
*body.locals_mut() = (0..num).map(|i| Local::new(i, ValueType::I64)).collect()
}
}
/// The maximum amount of pages any contract is allowed to have according to the current `Schedule`.
pub fn max_pages<T: Trait>() -> u32 {
Contracts::<T>::current_schedule().max_memory_pages
Contracts::<T>::current_schedule().limits.memory_pages
}