Skip to content

Commit 7a34837

Browse files
pgherveouathei
authored andcommitted
Contracts: Rework host fn benchmarks (paritytech#4233)
fix paritytech#4163 This PR does the following: Update to pallet-contracts-proc-macro: - Parse #[cfg] so we can add a dummy noop host function for benchmark. - Generate BenchEnv::<host_fn> so we can call host functions directly in the benchmark. - Add the weight of the noop host function before calling the host function itself Update benchmarks: - Update all host function benchmark, a host function benchmark now simply call the host function, instead of invoking the function n times from within a contract. - Refactor RuntimeCosts & Schedule, for most host functions, we can now use the generated weight function directly instead of computing the diff with the cost! macro ```rust // Before #[benchmark(pov_mode = Measured)] fn seal_input(r: Linear<0, API_BENCHMARK_RUNS>) { let code = WasmModule::<T>::from(ModuleDefinition { memory: Some(ImportedMemory::max::<T>()), imported_functions: vec![ImportedFunction { module: "seal0", name: "seal_input", params: vec![ValueType::I32, ValueType::I32], return_type: None, }], data_segments: vec![DataSegment { offset: 0, value: 0u32.to_le_bytes().to_vec() }], call_body: Some(body::repeated( r, &[ Instruction::I32Const(4), // ptr where to store output Instruction::I32Const(0), // ptr to length Instruction::Call(0), ], )), ..Default::default() }); call_builder!(func, code); let res; #[block] { res = func.call(); } assert_eq!(res.did_revert(), false); } ``` ```rust // After fn seal_input(n: Linear<0, { code::max_pages::<T>() * 64 * 1024 - 4 }>) { let mut setup = CallSetup::<T>::default(); let (mut ext, _) = setup.ext(); let mut runtime = crate::wasm::Runtime::new(&mut ext, vec![42u8; n as usize]); let mut memory = memory!(n.to_le_bytes(), vec![0u8; n as usize],); let result; #[block] { result = BenchEnv::seal0_input(&mut runtime, &mut memory, 4, 0) } assert_ok!(result); assert_eq!(&memory[4..], &vec![42u8; n as usize]); } ``` [Weights compare](https://weights.tasty.limo/compare?unit=weight&ignore_errors=true&threshold=10&method=asymptotic&repo=polkadot-sdk&old=master&new=pg%2Frework-host-benchs&path_pattern=substrate%2Fframe%2Fcontracts%2Fsrc%2Fweights.rs%2Cpolkadot%2Fruntime%2F*%2Fsrc%2Fweights%2F**%2F*.rs%2Cpolkadot%2Fbridges%2Fmodules%2F*%2Fsrc%2Fweights.rs%2Ccumulus%2F**%2Fweights%2F*.rs%2Ccumulus%2F**%2Fweights%2Fxcm%2F*.rs%2Ccumulus%2F**%2Fsrc%2Fweights.rs) --------- Co-authored-by: command-bot <> Co-authored-by: Alexander Theißen <[email protected]>
1 parent f48097d commit 7a34837

File tree

13 files changed

+1613
-3980
lines changed

13 files changed

+1613
-3980
lines changed

prdoc/pr_4233.prdoc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
title: "[pallet_contracts] Update Host fn benchnmarks"
2+
3+
doc:
4+
- audience: Runtime Dev
5+
description: |
6+
Update how the host functions are benchmarked.
7+
Instead of benchnarking a contract that calls the host functions, we now benchmark the host functions directly.
8+
9+
crates:
10+
- name: pallet-contracts
11+
bump: minor
12+
- name: pallet-contracts-proc-macro
13+
bump: minor
14+

substrate/frame/contracts/README.md

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,6 @@ calls are reverted. Assuming correct error handling by contract A, A's other cal
3434

3535
One `ref_time` `Weight` is defined as one picosecond of execution time on the runtime's reference machine.
3636

37-
#### Schedule
38-
39-
The `Schedule` is where, among other things, the cost of every action a contract can do is defined. These costs are derived
40-
from the benchmarks of this pallet. Instead of looking at the raw benchmark results it is advised to look at the `Schedule`
41-
if one wants to manually inspect the performance characteristics. The `Schedule` can be printed like this:
42-
43-
```sh
44-
RUST_LOG=runtime::contracts=info cargo run --features runtime-benchmarks --bin substrate-node -- benchmark pallet --extra -p pallet_contracts -e print_schedule
45-
```
46-
47-
Please note that the `Schedule` will be printed multiple times. This is because we are (ab)using a benchmark to print
48-
the struct.
49-
5037
### Revert Behaviour
5138

5239
Contract call failures are not cascading. When failures occur in a sub-call, they do not "bubble up", and the call will

substrate/frame/contracts/proc-macro/src/lib.rs

Lines changed: 127 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ struct HostFn {
132132
alias_to: Option<String>,
133133
/// Formulating the predicate inverted makes the expression using it simpler.
134134
not_deprecated: bool,
135+
cfg: Option<syn::Attribute>,
135136
}
136137

137138
enum HostFnReturn {
@@ -163,20 +164,21 @@ impl ToTokens for HostFn {
163164
impl HostFn {
164165
pub fn try_from(mut item: syn::ItemFn) -> syn::Result<Self> {
165166
let err = |span, msg| {
166-
let msg = format!("Invalid host function definition. {}", msg);
167+
let msg = format!("Invalid host function definition.\n{}", msg);
167168
syn::Error::new(span, msg)
168169
};
169170

170171
// process attributes
171172
let msg =
172-
"only #[version(<u8>)], #[unstable], #[prefixed_alias] and #[deprecated] attributes are allowed.";
173+
"Only #[version(<u8>)], #[unstable], #[prefixed_alias], #[cfg] and #[deprecated] attributes are allowed.";
173174
let span = item.span();
174175
let mut attrs = item.attrs.clone();
175176
attrs.retain(|a| !a.path().is_ident("doc"));
176177
let mut maybe_version = None;
177178
let mut is_stable = true;
178179
let mut alias_to = None;
179180
let mut not_deprecated = true;
181+
let mut cfg = None;
180182
while let Some(attr) = attrs.pop() {
181183
let ident = attr.path().get_ident().ok_or(err(span, msg))?.to_string();
182184
match ident.as_str() {
@@ -206,7 +208,13 @@ impl HostFn {
206208
}
207209
not_deprecated = false;
208210
},
209-
_ => return Err(err(span, msg)),
211+
"cfg" => {
212+
if cfg.is_some() {
213+
return Err(err(span, "#[cfg] can only be specified once"))
214+
}
215+
cfg = Some(attr);
216+
},
217+
id => return Err(err(span, &format!("Unsupported attribute \"{id}\". {msg}"))),
210218
}
211219
}
212220
let name = item.sig.ident.to_string();
@@ -311,6 +319,7 @@ impl HostFn {
311319
is_stable,
312320
alias_to,
313321
not_deprecated,
322+
cfg,
314323
})
315324
},
316325
_ => Err(err(span, &msg)),
@@ -528,8 +537,9 @@ fn expand_env(def: &EnvDef, docs: bool) -> TokenStream2 {
528537
/// - real implementation, to register it in the contract execution environment;
529538
/// - dummy implementation, to be used as mocks for contract validation step.
530539
fn expand_impls(def: &EnvDef) -> TokenStream2 {
531-
let impls = expand_functions(def, true, quote! { crate::wasm::Runtime<E> });
532-
let dummy_impls = expand_functions(def, false, quote! { () });
540+
let impls = expand_functions(def, ExpandMode::Impl);
541+
let dummy_impls = expand_functions(def, ExpandMode::MockImpl);
542+
let bench_impls = expand_functions(def, ExpandMode::BenchImpl);
533543

534544
quote! {
535545
impl<'a, E: Ext> crate::wasm::Environment<crate::wasm::runtime::Runtime<'a, E>> for Env
@@ -545,6 +555,14 @@ fn expand_impls(def: &EnvDef) -> TokenStream2 {
545555
}
546556
}
547557

558+
#[cfg(feature = "runtime-benchmarks")]
559+
pub struct BenchEnv<E>(::core::marker::PhantomData<E>);
560+
561+
#[cfg(feature = "runtime-benchmarks")]
562+
impl<E: Ext> BenchEnv<E> {
563+
#bench_impls
564+
}
565+
548566
impl crate::wasm::Environment<()> for Env
549567
{
550568
fn define(
@@ -560,18 +578,38 @@ fn expand_impls(def: &EnvDef) -> TokenStream2 {
560578
}
561579
}
562580

563-
fn expand_functions(def: &EnvDef, expand_blocks: bool, host_state: TokenStream2) -> TokenStream2 {
581+
enum ExpandMode {
582+
Impl,
583+
BenchImpl,
584+
MockImpl,
585+
}
586+
587+
impl ExpandMode {
588+
fn expand_blocks(&self) -> bool {
589+
match *self {
590+
ExpandMode::Impl | ExpandMode::BenchImpl => true,
591+
ExpandMode::MockImpl => false,
592+
}
593+
}
594+
595+
fn host_state(&self) -> TokenStream2 {
596+
match *self {
597+
ExpandMode::Impl | ExpandMode::BenchImpl => quote! { crate::wasm::runtime::Runtime<E> },
598+
ExpandMode::MockImpl => quote! { () },
599+
}
600+
}
601+
}
602+
603+
fn expand_functions(def: &EnvDef, expand_mode: ExpandMode) -> TokenStream2 {
564604
let impls = def.host_funcs.iter().map(|f| {
565605
// skip the context and memory argument
566606
let params = f.item.sig.inputs.iter().skip(2);
567-
568-
let (module, name, body, wasm_output, output) = (
569-
f.module(),
570-
&f.name,
571-
&f.item.block,
572-
f.returns.to_wasm_sig(),
573-
&f.item.sig.output
574-
);
607+
let module = f.module();
608+
let cfg = &f.cfg;
609+
let name = &f.name;
610+
let body = &f.item.block;
611+
let wasm_output = f.returns.to_wasm_sig();
612+
let output = &f.item.sig.output;
575613
let is_stable = f.is_stable;
576614
let not_deprecated = f.not_deprecated;
577615

@@ -608,23 +646,34 @@ fn expand_functions(def: &EnvDef, expand_blocks: bool, host_state: TokenStream2)
608646
// - We replace any code by unreachable!
609647
// - Allow unused variables as the code that uses is not expanded
610648
// - We don't need to map the error as we simply panic if they code would ever be executed
611-
let inner = if expand_blocks {
612-
quote! { || #output {
613-
let (memory, ctx) = __caller__
614-
.data()
615-
.memory()
616-
.expect("Memory must be set when setting up host data; qed")
617-
.data_and_store_mut(&mut __caller__);
618-
#wrapped_body_with_trace
619-
} }
620-
} else {
621-
quote! { || -> #wasm_output {
622-
// This is part of the implementation for `Environment<()>` which is not
623-
// meant to be actually executed. It is only for validation which will
624-
// never call host functions.
625-
::core::unreachable!()
626-
} }
649+
let expand_blocks = expand_mode.expand_blocks();
650+
let inner = match expand_mode {
651+
ExpandMode::Impl => {
652+
quote! { || #output {
653+
let (memory, ctx) = __caller__
654+
.data()
655+
.memory()
656+
.expect("Memory must be set when setting up host data; qed")
657+
.data_and_store_mut(&mut __caller__);
658+
#wrapped_body_with_trace
659+
} }
660+
},
661+
ExpandMode::BenchImpl => {
662+
let body = &body.stmts;
663+
quote!{
664+
#(#body)*
665+
}
666+
},
667+
ExpandMode::MockImpl => {
668+
quote! { || -> #wasm_output {
669+
// This is part of the implementation for `Environment<()>` which is not
670+
// meant to be actually executed. It is only for validation which will
671+
// never call host functions.
672+
::core::unreachable!()
673+
} }
674+
},
627675
};
676+
628677
let into_host = if expand_blocks {
629678
quote! {
630679
|reason| {
@@ -655,6 +704,11 @@ fn expand_functions(def: &EnvDef, expand_blocks: bool, host_state: TokenStream2)
655704
.map_err(TrapReason::from)
656705
.map_err(#into_host)?
657706
};
707+
708+
// Charge gas for host function execution.
709+
__caller__.data_mut().charge_gas(crate::wasm::RuntimeCosts::HostFn)
710+
.map_err(TrapReason::from)
711+
.map_err(#into_host)?;
658712
}
659713
} else {
660714
quote! { }
@@ -676,29 +730,51 @@ fn expand_functions(def: &EnvDef, expand_blocks: bool, host_state: TokenStream2)
676730
quote! { }
677731
};
678732

679-
quote! {
680-
// We need to allow all interfaces when runtime benchmarks are performed because
681-
// we generate the weights even when those interfaces are not enabled. This
682-
// is necessary as the decision whether we allow unstable or deprecated functions
683-
// is a decision made at runtime. Generation of the weights happens statically.
684-
if ::core::cfg!(feature = "runtime-benchmarks") ||
685-
((#is_stable || __allow_unstable__) && (#not_deprecated || __allow_deprecated__))
686-
{
687-
#allow_unused
688-
linker.define(#module, #name, ::wasmi::Func::wrap(&mut*store, |mut __caller__: ::wasmi::Caller<#host_state>, #( #params, )*| -> #wasm_output {
689-
#sync_gas_before
690-
let mut func = #inner;
691-
let result = func().map_err(#into_host).map(::core::convert::Into::into);
692-
#sync_gas_after
693-
result
694-
}))?;
695-
}
733+
match expand_mode {
734+
ExpandMode::BenchImpl => {
735+
let name = Ident::new(&format!("{module}_{name}"), Span::call_site());
736+
quote! {
737+
pub fn #name(ctx: &mut crate::wasm::Runtime<E>, memory: &mut [u8], #(#params),*) #output {
738+
#inner
739+
}
740+
}
741+
},
742+
_ => {
743+
let host_state = expand_mode.host_state();
744+
quote! {
745+
// We need to allow all interfaces when runtime benchmarks are performed because
746+
// we generate the weights even when those interfaces are not enabled. This
747+
// is necessary as the decision whether we allow unstable or deprecated functions
748+
// is a decision made at runtime. Generation of the weights happens statically.
749+
#cfg
750+
if ::core::cfg!(feature = "runtime-benchmarks") ||
751+
((#is_stable || __allow_unstable__) && (#not_deprecated || __allow_deprecated__))
752+
{
753+
#allow_unused
754+
linker.define(#module, #name, ::wasmi::Func::wrap(&mut*store, |mut __caller__: ::wasmi::Caller<#host_state>, #( #params, )*| -> #wasm_output {
755+
#sync_gas_before
756+
let mut func = #inner;
757+
let result = func().map_err(#into_host).map(::core::convert::Into::into);
758+
#sync_gas_after
759+
result
760+
}))?;
761+
}
762+
}
763+
},
696764
}
697765
});
698-
quote! {
699-
let __allow_unstable__ = matches!(allow_unstable, AllowUnstableInterface::Yes);
700-
let __allow_deprecated__ = matches!(allow_deprecated, AllowDeprecatedInterface::Yes);
701-
#( #impls )*
766+
767+
match expand_mode {
768+
ExpandMode::BenchImpl => {
769+
quote! {
770+
#( #impls )*
771+
}
772+
},
773+
_ => quote! {
774+
let __allow_unstable__ = matches!(allow_unstable, AllowUnstableInterface::Yes);
775+
let __allow_deprecated__ = matches!(allow_deprecated, AllowDeprecatedInterface::Yes);
776+
#( #impls )*
777+
},
702778
}
703779
}
704780

substrate/frame/contracts/src/benchmarking/call_builder.rs

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use crate::{
2525
};
2626
use codec::{Encode, HasCompact};
2727
use core::fmt::Debug;
28+
use frame_benchmarking::benchmarking;
2829
use sp_core::Get;
2930
use sp_std::prelude::*;
3031

@@ -57,6 +58,16 @@ pub struct CallSetup<T: Config> {
5758
data: Vec<u8>,
5859
}
5960

61+
impl<T> Default for CallSetup<T>
62+
where
63+
T: Config + pallet_balances::Config,
64+
<BalanceOf<T> as HasCompact>::Type: Clone + Eq + PartialEq + Debug + TypeInfo + Encode,
65+
{
66+
fn default() -> Self {
67+
Self::new(WasmModule::dummy())
68+
}
69+
}
70+
6071
impl<T> CallSetup<T>
6172
where
6273
T: Config + pallet_balances::Config,
@@ -70,6 +81,17 @@ where
7081

7182
let storage_meter = Meter::new(&origin, None, 0u32.into()).unwrap();
7283

84+
// Whitelist contract account, as it is already accounted for in the call benchmark
85+
benchmarking::add_to_whitelist(
86+
frame_system::Account::<T>::hashed_key_for(&contract.account_id).into(),
87+
);
88+
89+
// Whitelist the contract's contractInfo as it is already accounted for in the call
90+
// benchmark
91+
benchmarking::add_to_whitelist(
92+
crate::ContractInfoOf::<T>::hashed_key_for(&contract.account_id).into(),
93+
);
94+
7395
Self {
7496
contract,
7597
dest,
@@ -150,21 +172,29 @@ where
150172
}
151173

152174
#[macro_export]
153-
macro_rules! call_builder(
154-
($func: ident, $module:expr) => {
155-
$crate::call_builder!($func, _contract, $module);
175+
macro_rules! memory(
176+
($($bytes:expr,)*) => {
177+
vec![]
178+
.into_iter()
179+
$(.chain($bytes))*
180+
.collect::<Vec<_>>()
156181
};
157-
($func: ident, $contract: ident, $module:expr) => {
158-
let mut setup = CallSetup::<T>::new($module);
159-
$crate::call_builder!($func, $contract, setup: setup);
182+
);
183+
184+
#[macro_export]
185+
macro_rules! build_runtime(
186+
($runtime:ident, $memory:ident: [$($segment:expr,)*]) => {
187+
$crate::build_runtime!($runtime, _contract, $memory: [$($segment,)*]);
160188
};
161-
($func:ident, setup: $setup: ident) => {
162-
$crate::call_builder!($func, _contract, setup: $setup);
189+
($runtime:ident, $contract:ident, $memory:ident: [$($bytes:expr,)*]) => {
190+
$crate::build_runtime!($runtime, $contract);
191+
let mut $memory = $crate::memory!($($bytes,)*);
163192
};
164-
($func:ident, $contract: ident, setup: $setup: ident) => {
165-
let data = $setup.data();
166-
let $contract = $setup.contract();
167-
let (mut ext, module) = $setup.ext();
168-
let $func = CallSetup::<T>::prepare_call(&mut ext, module, data);
193+
($runtime:ident, $contract:ident) => {
194+
let mut setup = CallSetup::<T>::default();
195+
let $contract = setup.contract();
196+
let input = setup.data();
197+
let (mut ext, _) = setup.ext();
198+
let mut $runtime = crate::wasm::Runtime::new(&mut ext, input);
169199
};
170200
);

0 commit comments

Comments
 (0)