mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-12 15:51:12 +00:00
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:
committed by
GitHub
parent
9704c204e6
commit
51c67fe881
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user