mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-27 12:48:00 +00:00
new proc-macro-based benchmarking syntax (#12924)
* add stub for new benchmark macro
* benchmark syntax
* add #[extrinsic call] separator
* parse #[benchmark] item as a function
* proper emission of error when #[extrinsic_call] annotation is missing
* clean up
* enclosing module via benchmarks! { } working
* use an attribute macro on the module instead of benchmarks! { }
* cargo fmt
* working component implementation
* WIP
* working
* add syntax for Linear<A, B>
* parsing of param ranges (still need to build tuple though)
* params parsing WIP
* clean up (don't need extrinsic call name)
* use proper Result syntax for BenchmarkDef parsing
* proper parsing of Linear<0, 1> style args
* successfully parse and make use of linear component ranges 💥
* rename support variable => home because eventually will be moved
* compile-time check that param range types implement ParamRange
* switch to using balances as example, failing on instance pallet
* successfully set up __origin and __call with balances 💥
* clean up
* use a module
* don't need a variable for transfer
* rename benchmark_transfer -> transfer because no longer conflicts
* clean up
* working with transfer_increasing_users as well 💥
* re-add BareBlock
* add comments for undocumented structs+functions+traits
* refactor in preparation for removing module requirements
* switch to a block instead of a module
* use the outer macro pattern to to enable #[benchmarks] aggregation
* successfully generate SelectedBenchmark 💥
* implement components for SelectedBenchmark
* implement instance for SelectedBenchmark
* properly track #[extra]
* working impl for fn benchmarks()
* run_benchmarks WIP
* finish run_benchmark! impl 💥
* import balances transfer_best_case benchmark
* import transfer_keep_alive balances pallet benchmark
* import set_balance_creating balances pallet benchmark
* import set_balance_killing balances pallet benchmark
* import force_transfer balances pallet benchmark
* add #[extra] annotation and docs to transfer_increasing_users
* import transfer_all balances pallet benchmark
* import force_unreserve balances pallet benchmark
* prepare to implement impl_benchmark_test_suite!
* ensure tests cover #[extra] before and after #[benchmark] tag
* refactor
* clean up
* fix
* move to outer
* switch to benchmarks/instance_benchmarks
* test impl almost done, strange compiler error
* benchmark test suites working 💥
* clean up
* add stub and basic parsing for where_clause
* working except where clause and extrinsic calls containing method chains
* assume option (2) for now wrt https://github.com/paritytech/substrate/pull/12924#issuecomment-1372938718
* clean up
* switch to attribute-style
* properly handle where clauses
* fix subtle missing where clause, now just MessageQueue issues
* fix block formatting in message-queue pallet
* switch to block vs non-block parsing of extrinsic call
* working now but some benchmark tests failing
* message-queue tests working (run order issue fixed) 🎉
* add comments and internal docs for fame_support_procedural::benchmark
* fix license years
* docs for lib.rs
* add docs to new support procedural macros
* don't allow #[benchmark] outside of benchmarking module
* add docs
* use benchmark(extra, skip_meta) style args
* update docs accordingly
* appease clippy
* bump ci
* add notes about `extra` and `skip_meta`
* fix doc tests
* re-run CI
* use `ignore` instead of `no_run` on doc examples
* bump CI
* replace some if-lets with if-elses
* more refactoring of if-let statements
* fix remaining if-lets in BenchmarkDef::from()
* fix if-lets in benchmarks()
* fix remaining if-lets, use nested find_map for extrinsic call
* switch to use #[extrinsic_call] or #[block] situationally
* refactor ExtrinsicCallDef => BenchmarkCallDef
* update docs with info about #[block]
* add macro stub for #[extrinsic_call]
* fix docs and add stub for #[block] as well
* remove unused extern crate line
* fix clippy nits
* Use V2 bench syntax in pallet-example-basic
Just testing the dev-ex...
Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
* carry over comment
* use curly-brace style for impl_benchmark_test_suite!
* remove unneeded parenthesis
* proper handling of _() extrinsic call style
* add docs for _() syntax
* fix crate access
* simplify keyword access
Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>
* simplify module content destructuring
Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>
* fix crate access "frame_benchmarking" => "frame-benchmarking", compiles
* use _() extrinsic call syntax where possible in balances
* simplify attr.path.segments.last()
Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>
* fix compile error being suppressed
* simplify extrinsic call keyword parsing
Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>
* use ? operator instead of return None
Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>
* rename generics => type_use_generics
rename full_generics => type_impl_generics
* simplify extrinsic call extraction with transpose
* bump CI
* nit
* proper handling of too many + too few block/extrinsic call annotations
* change to B >= A
Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
* remove unneeded ignore
Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
* remove another ignore
Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
* add ui tests
* use _() style extrinsic call on accumulate_dummy
Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
* add range check to ParamRange
* ui test for bad param ranges
* fix failing example
* add ignore back to other failing example
* tweak expr_call span
Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>
* fix typo
* eliminate a match
Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>
* change pub fn benchmarks to return Result<TokenStream>
* fix origin error span
* more informative error for invalid benchmark parameter name
* fix spans on a few benchmark errors
* remove unneeded clone
* refactor inner loop of benchmark function parsing
* preserve mod attributes
* refactor outer loop of benchmark def parsing code, greatly simplified
* simplify to use a ? operator when parsing benchmark attr path
* fix another ? operator
* further simplify benchmark function attr parsing with more ? ops
* refactor extrinsic call handling to use if let rather than match
* replace is_ok => is_err
Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>
* re-use name during expansion of benchmark def
* remove unneeded clone
* fix span for origin missing error
* fix missing semi
Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>
Co-authored-by: parity-processbot <>
This commit is contained in:
@@ -0,0 +1,860 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2023 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Home of the parsing and expansion code for the new pallet benchmarking syntax
|
||||
|
||||
use derive_syn_parse::Parse;
|
||||
use frame_support_procedural_tools::generate_crate_access_2018;
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
|
||||
use quote::{quote, quote_spanned, ToTokens};
|
||||
use syn::{
|
||||
parenthesized,
|
||||
parse::{Nothing, ParseStream},
|
||||
punctuated::Punctuated,
|
||||
spanned::Spanned,
|
||||
token::{Colon2, Comma, Gt, Lt, Paren},
|
||||
Attribute, Error, Expr, ExprBlock, ExprCall, ExprPath, FnArg, Item, ItemFn, ItemMod, LitInt,
|
||||
Pat, Path, PathArguments, PathSegment, Result, Stmt, Token, Type, WhereClause,
|
||||
};
|
||||
|
||||
mod keywords {
|
||||
use syn::custom_keyword;
|
||||
|
||||
custom_keyword!(benchmark);
|
||||
custom_keyword!(benchmarks);
|
||||
custom_keyword!(block);
|
||||
custom_keyword!(extra);
|
||||
custom_keyword!(extrinsic_call);
|
||||
custom_keyword!(skip_meta);
|
||||
}
|
||||
|
||||
/// This represents the raw parsed data for a param definition such as `x: Linear<10, 20>`.
|
||||
#[derive(Clone)]
|
||||
struct ParamDef {
|
||||
name: String,
|
||||
typ: Type,
|
||||
start: u32,
|
||||
end: u32,
|
||||
}
|
||||
|
||||
/// Allows easy parsing of the `<10, 20>` component of `x: Linear<10, 20>`.
|
||||
#[derive(Parse)]
|
||||
struct RangeArgs {
|
||||
_lt_token: Lt,
|
||||
start: LitInt,
|
||||
_comma: Comma,
|
||||
end: LitInt,
|
||||
_gt_token: Gt,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct BenchmarkAttrs {
|
||||
skip_meta: bool,
|
||||
extra: bool,
|
||||
}
|
||||
|
||||
/// Represents a single benchmark option
|
||||
enum BenchmarkAttrKeyword {
|
||||
Extra,
|
||||
SkipMeta,
|
||||
}
|
||||
|
||||
impl syn::parse::Parse for BenchmarkAttrKeyword {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let lookahead = input.lookahead1();
|
||||
if lookahead.peek(keywords::extra) {
|
||||
let _extra: keywords::extra = input.parse()?;
|
||||
return Ok(BenchmarkAttrKeyword::Extra)
|
||||
} else if lookahead.peek(keywords::skip_meta) {
|
||||
let _skip_meta: keywords::skip_meta = input.parse()?;
|
||||
return Ok(BenchmarkAttrKeyword::SkipMeta)
|
||||
} else {
|
||||
return Err(lookahead.error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl syn::parse::Parse for BenchmarkAttrs {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let lookahead = input.lookahead1();
|
||||
if !lookahead.peek(Paren) {
|
||||
let _nothing: Nothing = input.parse()?;
|
||||
return Ok(BenchmarkAttrs { skip_meta: false, extra: false })
|
||||
}
|
||||
let content;
|
||||
let _paren: Paren = parenthesized!(content in input);
|
||||
let mut extra = false;
|
||||
let mut skip_meta = false;
|
||||
let args = Punctuated::<BenchmarkAttrKeyword, Token![,]>::parse_terminated(&content)?;
|
||||
for arg in args.into_iter() {
|
||||
match arg {
|
||||
BenchmarkAttrKeyword::Extra => {
|
||||
if extra {
|
||||
return Err(content.error("`extra` can only be specified once"))
|
||||
}
|
||||
extra = true;
|
||||
},
|
||||
BenchmarkAttrKeyword::SkipMeta => {
|
||||
if skip_meta {
|
||||
return Err(content.error("`skip_meta` can only be specified once"))
|
||||
}
|
||||
skip_meta = true;
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(BenchmarkAttrs { extra, skip_meta })
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the parsed extrinsic call for a benchmark
|
||||
#[derive(Clone)]
|
||||
enum BenchmarkCallDef {
|
||||
ExtrinsicCall { origin: Expr, expr_call: ExprCall, attr_span: Span }, // #[extrinsic_call]
|
||||
Block { block: ExprBlock, attr_span: Span }, // #[block]
|
||||
}
|
||||
|
||||
impl BenchmarkCallDef {
|
||||
/// Returns the `span()` for attribute
|
||||
fn attr_span(&self) -> Span {
|
||||
match self {
|
||||
BenchmarkCallDef::ExtrinsicCall { origin: _, expr_call: _, attr_span } => *attr_span,
|
||||
BenchmarkCallDef::Block { block: _, attr_span } => *attr_span,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a parsed `#[benchmark]` or `#[instance_banchmark]` item.
|
||||
#[derive(Clone)]
|
||||
struct BenchmarkDef {
|
||||
params: Vec<ParamDef>,
|
||||
setup_stmts: Vec<Stmt>,
|
||||
call_def: BenchmarkCallDef,
|
||||
verify_stmts: Vec<Stmt>,
|
||||
extra: bool,
|
||||
skip_meta: bool,
|
||||
}
|
||||
|
||||
impl BenchmarkDef {
|
||||
/// Constructs a [`BenchmarkDef`] by traversing an existing [`ItemFn`] node.
|
||||
pub fn from(item_fn: &ItemFn, extra: bool, skip_meta: bool) -> Result<BenchmarkDef> {
|
||||
let mut params: Vec<ParamDef> = Vec::new();
|
||||
|
||||
// parse params such as "x: Linear<0, 1>"
|
||||
for arg in &item_fn.sig.inputs {
|
||||
let invalid_param = |span| {
|
||||
return Err(Error::new(span, "Invalid benchmark function param. A valid example would be `x: Linear<5, 10>`.", ))
|
||||
};
|
||||
|
||||
let FnArg::Typed(arg) = arg else { return invalid_param(arg.span()) };
|
||||
let Pat::Ident(ident) = &*arg.pat else { return invalid_param(arg.span()) };
|
||||
|
||||
// check param name
|
||||
let var_span = ident.span();
|
||||
let invalid_param_name = || {
|
||||
return Err(Error::new(
|
||||
var_span,
|
||||
"Benchmark parameter names must consist of a single lowercase letter (a-z) and no other characters.",
|
||||
))
|
||||
};
|
||||
let name = ident.ident.to_token_stream().to_string();
|
||||
if name.len() > 1 {
|
||||
return invalid_param_name()
|
||||
};
|
||||
let Some(name_char) = name.chars().next() else { return invalid_param_name() };
|
||||
if !name_char.is_alphabetic() || !name_char.is_lowercase() {
|
||||
return invalid_param_name()
|
||||
}
|
||||
|
||||
// parse type
|
||||
let typ = &*arg.ty;
|
||||
let Type::Path(tpath) = typ else { return invalid_param(typ.span()) };
|
||||
let Some(segment) = tpath.path.segments.last() else { return invalid_param(typ.span()) };
|
||||
let args = segment.arguments.to_token_stream().into();
|
||||
let Ok(args) = syn::parse::<RangeArgs>(args) else { return invalid_param(typ.span()) };
|
||||
let Ok(start) = args.start.base10_parse::<u32>() else { return invalid_param(args.start.span()) };
|
||||
let Ok(end) = args.end.base10_parse::<u32>() else { return invalid_param(args.end.span()) };
|
||||
|
||||
if end < start {
|
||||
return Err(Error::new(
|
||||
args.start.span(),
|
||||
"The start of a `ParamRange` must be less than or equal to the end",
|
||||
))
|
||||
}
|
||||
|
||||
params.push(ParamDef { name, typ: typ.clone(), start, end });
|
||||
}
|
||||
|
||||
// #[extrinsic_call] / #[block] handling
|
||||
let call_defs = item_fn.block.stmts.iter().enumerate().filter_map(|(i, child)| {
|
||||
if let Stmt::Semi(Expr::Call(expr_call), _semi) = child {
|
||||
// #[extrinsic_call] case
|
||||
expr_call.attrs.iter().enumerate().find_map(|(k, attr)| {
|
||||
let segment = attr.path.segments.last()?;
|
||||
let _: keywords::extrinsic_call = syn::parse(segment.ident.to_token_stream().into()).ok()?;
|
||||
let mut expr_call = expr_call.clone();
|
||||
|
||||
// consume #[extrinsic_call] tokens
|
||||
expr_call.attrs.remove(k);
|
||||
|
||||
// extract origin from expr_call
|
||||
let Some(origin) = expr_call.args.first().cloned() else {
|
||||
return Some(Err(Error::new(expr_call.span(), "Single-item extrinsic calls must specify their origin as the first argument.")))
|
||||
};
|
||||
|
||||
Some(Ok((i, BenchmarkCallDef::ExtrinsicCall { origin, expr_call, attr_span: attr.span() })))
|
||||
})
|
||||
} else if let Stmt::Expr(Expr::Block(block)) = child {
|
||||
// #[block] case
|
||||
block.attrs.iter().enumerate().find_map(|(k, attr)| {
|
||||
let segment = attr.path.segments.last()?;
|
||||
let _: keywords::block = syn::parse(segment.ident.to_token_stream().into()).ok()?;
|
||||
let mut block = block.clone();
|
||||
|
||||
// consume #[block] tokens
|
||||
block.attrs.remove(k);
|
||||
|
||||
Some(Ok((i, BenchmarkCallDef::Block { block, attr_span: attr.span() })))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}).collect::<Result<Vec<_>>>()?;
|
||||
let (i, call_def) = match &call_defs[..] {
|
||||
[(i, call_def)] => (*i, call_def.clone()), // = 1
|
||||
[] => return Err(Error::new( // = 0
|
||||
item_fn.block.brace_token.span,
|
||||
"No valid #[extrinsic_call] or #[block] annotation could be found in benchmark function body."
|
||||
)),
|
||||
_ => return Err(Error::new( // > 1
|
||||
call_defs[1].1.attr_span(),
|
||||
"Only one #[extrinsic_call] or #[block] attribute is allowed per benchmark."
|
||||
)),
|
||||
};
|
||||
|
||||
Ok(BenchmarkDef {
|
||||
params,
|
||||
setup_stmts: Vec::from(&item_fn.block.stmts[0..i]),
|
||||
call_def,
|
||||
verify_stmts: Vec::from(&item_fn.block.stmts[(i + 1)..item_fn.block.stmts.len()]),
|
||||
extra,
|
||||
skip_meta,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses and expands a `#[benchmarks]` or `#[instance_benchmarks]` invocation
|
||||
pub fn benchmarks(
|
||||
attrs: TokenStream,
|
||||
tokens: TokenStream,
|
||||
instance: bool,
|
||||
) -> syn::Result<TokenStream> {
|
||||
// gather module info
|
||||
let module: ItemMod = syn::parse(tokens)?;
|
||||
let mod_span = module.span();
|
||||
let where_clause = match syn::parse::<Nothing>(attrs.clone()) {
|
||||
Ok(_) => quote!(),
|
||||
Err(_) => syn::parse::<WhereClause>(attrs)?.predicates.to_token_stream(),
|
||||
};
|
||||
let mod_vis = module.vis;
|
||||
let mod_name = module.ident;
|
||||
|
||||
// consume #[benchmarks] attribute by exclusing it from mod_attrs
|
||||
let mod_attrs: Vec<&Attribute> = module
|
||||
.attrs
|
||||
.iter()
|
||||
.filter(|attr| syn::parse2::<keywords::benchmarks>(attr.to_token_stream()).is_err())
|
||||
.collect();
|
||||
|
||||
let mut benchmark_names: Vec<Ident> = Vec::new();
|
||||
let mut extra_benchmark_names: Vec<Ident> = Vec::new();
|
||||
let mut skip_meta_benchmark_names: Vec<Ident> = Vec::new();
|
||||
|
||||
let (_brace, mut content) =
|
||||
module.content.ok_or(syn::Error::new(mod_span, "Module cannot be empty!"))?;
|
||||
|
||||
// find all function defs marked with #[benchmark]
|
||||
let benchmark_fn_metas = content.iter_mut().filter_map(|stmt| {
|
||||
// parse as a function def first
|
||||
let Item::Fn(func) = stmt else { return None };
|
||||
|
||||
// find #[benchmark] attribute on function def
|
||||
let benchmark_attr = func.attrs.iter().find_map(|attr| {
|
||||
let seg = attr.path.segments.last()?;
|
||||
syn::parse::<keywords::benchmark>(seg.ident.to_token_stream().into()).ok()?;
|
||||
Some(attr)
|
||||
})?;
|
||||
|
||||
Some((benchmark_attr.clone(), func.clone(), stmt))
|
||||
});
|
||||
|
||||
// parse individual benchmark defs and args
|
||||
for (benchmark_attr, func, stmt) in benchmark_fn_metas {
|
||||
// parse any args provided to #[benchmark]
|
||||
let attr_tokens = benchmark_attr.tokens.to_token_stream().into();
|
||||
let benchmark_args: BenchmarkAttrs = syn::parse(attr_tokens)?;
|
||||
|
||||
// parse benchmark def
|
||||
let benchmark_def =
|
||||
BenchmarkDef::from(&func, benchmark_args.extra, benchmark_args.skip_meta)?;
|
||||
|
||||
// record benchmark name
|
||||
let name = &func.sig.ident;
|
||||
benchmark_names.push(name.clone());
|
||||
|
||||
// record name sets
|
||||
if benchmark_def.extra {
|
||||
extra_benchmark_names.push(name.clone());
|
||||
}
|
||||
if benchmark_def.skip_meta {
|
||||
skip_meta_benchmark_names.push(name.clone())
|
||||
}
|
||||
|
||||
// expand benchmark
|
||||
let expanded = expand_benchmark(benchmark_def, name, instance, where_clause.clone());
|
||||
|
||||
// replace original function def with expanded code
|
||||
*stmt = Item::Verbatim(expanded);
|
||||
}
|
||||
|
||||
// generics
|
||||
let type_use_generics = match instance {
|
||||
false => quote!(T),
|
||||
true => quote!(T, I),
|
||||
};
|
||||
let type_impl_generics = match instance {
|
||||
false => quote!(T: Config),
|
||||
true => quote!(T: Config<I>, I: 'static),
|
||||
};
|
||||
|
||||
let krate = generate_crate_access_2018("frame-benchmarking")?;
|
||||
let support = quote!(#krate::frame_support);
|
||||
|
||||
// benchmark name variables
|
||||
let benchmark_names_str: Vec<String> = benchmark_names.iter().map(|n| n.to_string()).collect();
|
||||
let extra_benchmark_names_str: Vec<String> =
|
||||
extra_benchmark_names.iter().map(|n| n.to_string()).collect();
|
||||
let skip_meta_benchmark_names_str: Vec<String> =
|
||||
skip_meta_benchmark_names.iter().map(|n| n.to_string()).collect();
|
||||
let mut selected_benchmark_mappings: Vec<TokenStream2> = Vec::new();
|
||||
let mut benchmarks_by_name_mappings: Vec<TokenStream2> = Vec::new();
|
||||
let test_idents: Vec<Ident> = benchmark_names_str
|
||||
.iter()
|
||||
.map(|n| Ident::new(format!("test_{}", n).as_str(), Span::call_site()))
|
||||
.collect();
|
||||
for i in 0..benchmark_names.len() {
|
||||
let name_ident = &benchmark_names[i];
|
||||
let name_str = &benchmark_names_str[i];
|
||||
let test_ident = &test_idents[i];
|
||||
selected_benchmark_mappings.push(quote!(#name_str => SelectedBenchmark::#name_ident));
|
||||
benchmarks_by_name_mappings.push(quote!(#name_str => Self::#test_ident()))
|
||||
}
|
||||
|
||||
// emit final quoted tokens
|
||||
let res = quote! {
|
||||
#(#mod_attrs)
|
||||
*
|
||||
#mod_vis mod #mod_name {
|
||||
#(#content)
|
||||
*
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
enum SelectedBenchmark {
|
||||
#(#benchmark_names),
|
||||
*
|
||||
}
|
||||
|
||||
impl<#type_impl_generics> #krate::BenchmarkingSetup<#type_use_generics> for SelectedBenchmark where #where_clause {
|
||||
fn components(&self) -> #krate::Vec<(#krate::BenchmarkParameter, u32, u32)> {
|
||||
match self {
|
||||
#(
|
||||
Self::#benchmark_names => {
|
||||
<#benchmark_names as #krate::BenchmarkingSetup<#type_use_generics>>::components(&#benchmark_names)
|
||||
}
|
||||
)
|
||||
*
|
||||
}
|
||||
}
|
||||
|
||||
fn instance(
|
||||
&self,
|
||||
components: &[(#krate::BenchmarkParameter, u32)],
|
||||
verify: bool,
|
||||
) -> Result<
|
||||
#krate::Box<dyn FnOnce() -> Result<(), #krate::BenchmarkError>>,
|
||||
#krate::BenchmarkError,
|
||||
> {
|
||||
match self {
|
||||
#(
|
||||
Self::#benchmark_names => {
|
||||
<#benchmark_names as #krate::BenchmarkingSetup<
|
||||
#type_use_generics
|
||||
>>::instance(&#benchmark_names, components, verify)
|
||||
}
|
||||
)
|
||||
*
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(any(feature = "runtime-benchmarks", test))]
|
||||
impl<#type_impl_generics> #krate::Benchmarking for Pallet<#type_use_generics>
|
||||
where T: frame_system::Config, #where_clause
|
||||
{
|
||||
fn benchmarks(
|
||||
extra: bool,
|
||||
) -> #krate::Vec<#krate::BenchmarkMetadata> {
|
||||
let mut all_names = #krate::vec![
|
||||
#(#benchmark_names_str),
|
||||
*
|
||||
];
|
||||
if !extra {
|
||||
let extra = [
|
||||
#(#extra_benchmark_names_str),
|
||||
*
|
||||
];
|
||||
all_names.retain(|x| !extra.contains(x));
|
||||
}
|
||||
all_names.into_iter().map(|benchmark| {
|
||||
let selected_benchmark = match benchmark {
|
||||
#(#selected_benchmark_mappings),
|
||||
*,
|
||||
_ => panic!("all benchmarks should be selectable")
|
||||
};
|
||||
let components = <SelectedBenchmark as #krate::BenchmarkingSetup<#type_use_generics>>::components(&selected_benchmark);
|
||||
#krate::BenchmarkMetadata {
|
||||
name: benchmark.as_bytes().to_vec(),
|
||||
components,
|
||||
}
|
||||
}).collect::<#krate::Vec<_>>()
|
||||
}
|
||||
|
||||
fn run_benchmark(
|
||||
extrinsic: &[u8],
|
||||
c: &[(#krate::BenchmarkParameter, u32)],
|
||||
whitelist: &[#krate::TrackedStorageKey],
|
||||
verify: bool,
|
||||
internal_repeats: u32,
|
||||
) -> Result<#krate::Vec<#krate::BenchmarkResult>, #krate::BenchmarkError> {
|
||||
let extrinsic = #krate::str::from_utf8(extrinsic).map_err(|_| "`extrinsic` is not a valid utf-8 string!")?;
|
||||
let selected_benchmark = match extrinsic {
|
||||
#(#selected_benchmark_mappings),
|
||||
*,
|
||||
_ => return Err("Could not find extrinsic.".into()),
|
||||
};
|
||||
let mut whitelist = whitelist.to_vec();
|
||||
let whitelisted_caller_key = <frame_system::Account<
|
||||
T,
|
||||
> as #support::storage::StorageMap<_, _,>>::hashed_key_for(
|
||||
#krate::whitelisted_caller::<T::AccountId>()
|
||||
);
|
||||
whitelist.push(whitelisted_caller_key.into());
|
||||
let transactional_layer_key = #krate::TrackedStorageKey::new(
|
||||
#support::storage::transactional::TRANSACTION_LEVEL_KEY.into(),
|
||||
);
|
||||
whitelist.push(transactional_layer_key);
|
||||
#krate::benchmarking::set_whitelist(whitelist);
|
||||
let mut results: #krate::Vec<#krate::BenchmarkResult> = #krate::Vec::new();
|
||||
|
||||
// Always do at least one internal repeat...
|
||||
for _ in 0 .. internal_repeats.max(1) {
|
||||
// Always reset the state after the benchmark.
|
||||
#krate::defer!(#krate::benchmarking::wipe_db());
|
||||
|
||||
// Set up the externalities environment for the setup we want to
|
||||
// benchmark.
|
||||
let closure_to_benchmark = <
|
||||
SelectedBenchmark as #krate::BenchmarkingSetup<#type_use_generics>
|
||||
>::instance(&selected_benchmark, c, verify)?;
|
||||
|
||||
// Set the block number to at least 1 so events are deposited.
|
||||
if #krate::Zero::is_zero(&frame_system::Pallet::<T>::block_number()) {
|
||||
frame_system::Pallet::<T>::set_block_number(1u32.into());
|
||||
}
|
||||
|
||||
// Commit the externalities to the database, flushing the DB cache.
|
||||
// This will enable worst case scenario for reading from the database.
|
||||
#krate::benchmarking::commit_db();
|
||||
|
||||
// Reset the read/write counter so we don't count operations in the setup process.
|
||||
#krate::benchmarking::reset_read_write_count();
|
||||
|
||||
// Time the extrinsic logic.
|
||||
#krate::log::trace!(
|
||||
target: "benchmark",
|
||||
"Start Benchmark: {} ({:?})",
|
||||
extrinsic,
|
||||
c
|
||||
);
|
||||
|
||||
let start_pov = #krate::benchmarking::proof_size();
|
||||
let start_extrinsic = #krate::benchmarking::current_time();
|
||||
|
||||
closure_to_benchmark()?;
|
||||
|
||||
let finish_extrinsic = #krate::benchmarking::current_time();
|
||||
let end_pov = #krate::benchmarking::proof_size();
|
||||
|
||||
// Calculate the diff caused by the benchmark.
|
||||
let elapsed_extrinsic = finish_extrinsic.saturating_sub(start_extrinsic);
|
||||
let diff_pov = match (start_pov, end_pov) {
|
||||
(Some(start), Some(end)) => end.saturating_sub(start),
|
||||
_ => Default::default(),
|
||||
};
|
||||
|
||||
// Commit the changes to get proper write count
|
||||
#krate::benchmarking::commit_db();
|
||||
#krate::log::trace!(
|
||||
target: "benchmark",
|
||||
"End Benchmark: {} ns", elapsed_extrinsic
|
||||
);
|
||||
let read_write_count = #krate::benchmarking::read_write_count();
|
||||
#krate::log::trace!(
|
||||
target: "benchmark",
|
||||
"Read/Write Count {:?}", read_write_count
|
||||
);
|
||||
|
||||
// Time the storage root recalculation.
|
||||
let start_storage_root = #krate::benchmarking::current_time();
|
||||
#krate::storage_root(#krate::StateVersion::V1);
|
||||
let finish_storage_root = #krate::benchmarking::current_time();
|
||||
let elapsed_storage_root = finish_storage_root - start_storage_root;
|
||||
|
||||
let skip_meta = [ #(#skip_meta_benchmark_names_str),* ];
|
||||
let read_and_written_keys = if skip_meta.contains(&extrinsic) {
|
||||
#krate::vec![(b"Skipped Metadata".to_vec(), 0, 0, false)]
|
||||
} else {
|
||||
#krate::benchmarking::get_read_and_written_keys()
|
||||
};
|
||||
|
||||
results.push(#krate::BenchmarkResult {
|
||||
components: c.to_vec(),
|
||||
extrinsic_time: elapsed_extrinsic,
|
||||
storage_root_time: elapsed_storage_root,
|
||||
reads: read_write_count.0,
|
||||
repeat_reads: read_write_count.1,
|
||||
writes: read_write_count.2,
|
||||
repeat_writes: read_write_count.3,
|
||||
proof_size: diff_pov,
|
||||
keys: read_and_written_keys,
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(results);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl<#type_impl_generics> Pallet<#type_use_generics> where T: ::frame_system::Config, #where_clause {
|
||||
/// Test a particular benchmark by name.
|
||||
///
|
||||
/// This isn't called `test_benchmark_by_name` just in case some end-user eventually
|
||||
/// writes a benchmark, itself called `by_name`; the function would be shadowed in
|
||||
/// that case.
|
||||
///
|
||||
/// This is generally intended to be used by child test modules such as those created
|
||||
/// by the `impl_benchmark_test_suite` macro. However, it is not an error if a pallet
|
||||
/// author chooses not to implement benchmarks.
|
||||
#[allow(unused)]
|
||||
fn test_bench_by_name(name: &[u8]) -> Result<(), #krate::BenchmarkError> {
|
||||
let name = #krate::str::from_utf8(name)
|
||||
.map_err(|_| -> #krate::BenchmarkError { "`name` is not a valid utf8 string!".into() })?;
|
||||
match name {
|
||||
#(#benchmarks_by_name_mappings),
|
||||
*,
|
||||
_ => Err("Could not find test for requested benchmark.".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#mod_vis use #mod_name::*;
|
||||
};
|
||||
Ok(res.into())
|
||||
}
|
||||
|
||||
/// Prepares a [`Vec<ParamDef>`] to be interpolated by [`quote!`] by creating easily-iterable
|
||||
/// arrays formatted in such a way that they can be interpolated directly.
|
||||
struct UnrolledParams {
|
||||
param_ranges: Vec<TokenStream2>,
|
||||
param_names: Vec<TokenStream2>,
|
||||
param_types: Vec<TokenStream2>,
|
||||
}
|
||||
|
||||
impl UnrolledParams {
|
||||
/// Constructs an [`UnrolledParams`] from a [`Vec<ParamDef>`]
|
||||
fn from(params: &Vec<ParamDef>) -> UnrolledParams {
|
||||
let param_ranges: Vec<TokenStream2> = params
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let name = Ident::new(&p.name, Span::call_site());
|
||||
let start = p.start;
|
||||
let end = p.end;
|
||||
quote!(#name, #start, #end)
|
||||
})
|
||||
.collect();
|
||||
let param_names: Vec<TokenStream2> = params
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let name = Ident::new(&p.name, Span::call_site());
|
||||
quote!(#name)
|
||||
})
|
||||
.collect();
|
||||
let param_types: Vec<TokenStream2> = params
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let typ = &p.typ;
|
||||
quote!(#typ)
|
||||
})
|
||||
.collect();
|
||||
UnrolledParams { param_ranges, param_names, param_types }
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs expansion of an already-parsed [`BenchmarkDef`].
|
||||
fn expand_benchmark(
|
||||
benchmark_def: BenchmarkDef,
|
||||
name: &Ident,
|
||||
is_instance: bool,
|
||||
where_clause: TokenStream2,
|
||||
) -> TokenStream2 {
|
||||
// set up variables needed during quoting
|
||||
let krate = match generate_crate_access_2018("frame-benchmarking") {
|
||||
Ok(ident) => ident,
|
||||
Err(err) => return err.to_compile_error().into(),
|
||||
};
|
||||
let home = quote!(#krate::frame_support::benchmarking);
|
||||
let codec = quote!(#krate::frame_support::codec);
|
||||
let traits = quote!(#krate::frame_support::traits);
|
||||
let setup_stmts = benchmark_def.setup_stmts;
|
||||
let verify_stmts = benchmark_def.verify_stmts;
|
||||
let test_ident = Ident::new(format!("test_{}", name.to_string()).as_str(), Span::call_site());
|
||||
|
||||
// unroll params (prepare for quoting)
|
||||
let unrolled = UnrolledParams::from(&benchmark_def.params);
|
||||
let param_names = unrolled.param_names;
|
||||
let param_ranges = unrolled.param_ranges;
|
||||
let param_types = unrolled.param_types;
|
||||
|
||||
let type_use_generics = match is_instance {
|
||||
false => quote!(T),
|
||||
true => quote!(T, I),
|
||||
};
|
||||
|
||||
let type_impl_generics = match is_instance {
|
||||
false => quote!(T: Config),
|
||||
true => quote!(T: Config<I>, I: 'static),
|
||||
};
|
||||
|
||||
let (pre_call, post_call) = match benchmark_def.call_def {
|
||||
BenchmarkCallDef::ExtrinsicCall { origin, expr_call, attr_span: _ } => {
|
||||
let mut expr_call = expr_call.clone();
|
||||
|
||||
// remove first arg from expr_call
|
||||
let mut final_args = Punctuated::<Expr, Comma>::new();
|
||||
let args: Vec<&Expr> = expr_call.args.iter().collect();
|
||||
for arg in &args[1..] {
|
||||
final_args.push((*(*arg)).clone());
|
||||
}
|
||||
expr_call.args = final_args;
|
||||
|
||||
// determine call name (handles `_` and normal call syntax)
|
||||
let expr_span = expr_call.span();
|
||||
let call_err = || {
|
||||
quote_spanned!(expr_span=> "Extrinsic call must be a function call or `_`".to_compile_error()).into()
|
||||
};
|
||||
let call_name = match *expr_call.func {
|
||||
Expr::Path(expr_path) => {
|
||||
// normal function call
|
||||
let Some(segment) = expr_path.path.segments.last() else { return call_err(); };
|
||||
segment.ident.to_string()
|
||||
},
|
||||
Expr::Verbatim(tokens) => {
|
||||
// `_` style
|
||||
// replace `_` with fn name
|
||||
let Ok(_) = syn::parse::<Token![_]>(tokens.to_token_stream().into()) else { return call_err(); };
|
||||
name.to_string()
|
||||
},
|
||||
_ => return call_err(),
|
||||
};
|
||||
|
||||
// modify extrinsic call to be prefixed with "new_call_variant"
|
||||
let call_name = format!("new_call_variant_{}", call_name);
|
||||
let mut punct: Punctuated<PathSegment, Colon2> = Punctuated::new();
|
||||
punct.push(PathSegment {
|
||||
arguments: PathArguments::None,
|
||||
ident: Ident::new(call_name.as_str(), Span::call_site()),
|
||||
});
|
||||
*expr_call.func = Expr::Path(ExprPath {
|
||||
attrs: vec![],
|
||||
qself: None,
|
||||
path: Path { leading_colon: None, segments: punct },
|
||||
});
|
||||
|
||||
(
|
||||
// (pre_call, post_call):
|
||||
quote! {
|
||||
let __call = Call::<#type_use_generics>::#expr_call;
|
||||
let __benchmarked_call_encoded = #codec::Encode::encode(&__call);
|
||||
},
|
||||
quote! {
|
||||
let __call_decoded = <Call<#type_use_generics> as #codec::Decode>
|
||||
::decode(&mut &__benchmarked_call_encoded[..])
|
||||
.expect("call is encoded above, encoding must be correct");
|
||||
let __origin = #origin.into();
|
||||
<Call<#type_use_generics> as #traits::UnfilteredDispatchable>::dispatch_bypass_filter(
|
||||
__call_decoded,
|
||||
__origin,
|
||||
)?;
|
||||
},
|
||||
)
|
||||
},
|
||||
BenchmarkCallDef::Block { block, attr_span: _ } => (quote!(), quote!(#block)),
|
||||
};
|
||||
|
||||
// generate final quoted tokens
|
||||
let res = quote! {
|
||||
// compile-time assertions that each referenced param type implements ParamRange
|
||||
#(
|
||||
#home::assert_impl_all!(#param_types: #home::ParamRange);
|
||||
)*
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
struct #name;
|
||||
|
||||
#[allow(unused_variables)]
|
||||
impl<#type_impl_generics> #krate::BenchmarkingSetup<#type_use_generics>
|
||||
for #name where #where_clause {
|
||||
fn components(&self) -> #krate::Vec<(#krate::BenchmarkParameter, u32, u32)> {
|
||||
#krate::vec! [
|
||||
#(
|
||||
(#krate::BenchmarkParameter::#param_ranges)
|
||||
),*
|
||||
]
|
||||
}
|
||||
|
||||
fn instance(
|
||||
&self,
|
||||
components: &[(#krate::BenchmarkParameter, u32)],
|
||||
verify: bool
|
||||
) -> Result<#krate::Box<dyn FnOnce() -> Result<(), #krate::BenchmarkError>>, #krate::BenchmarkError> {
|
||||
#(
|
||||
// prepare instance #param_names
|
||||
let #param_names = components.iter()
|
||||
.find(|&c| c.0 == #krate::BenchmarkParameter::#param_names)
|
||||
.ok_or("Could not find component during benchmark preparation.")?
|
||||
.1;
|
||||
)*
|
||||
|
||||
// benchmark setup code
|
||||
#(
|
||||
#setup_stmts
|
||||
)*
|
||||
#pre_call
|
||||
Ok(#krate::Box::new(move || -> Result<(), #krate::BenchmarkError> {
|
||||
#post_call
|
||||
if verify {
|
||||
#(
|
||||
#verify_stmts
|
||||
)*
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl<#type_impl_generics> Pallet<#type_use_generics> where T: ::frame_system::Config, #where_clause {
|
||||
#[allow(unused)]
|
||||
fn #test_ident() -> Result<(), #krate::BenchmarkError> {
|
||||
let selected_benchmark = SelectedBenchmark::#name;
|
||||
let components = <
|
||||
SelectedBenchmark as #krate::BenchmarkingSetup<T, _>
|
||||
>::components(&selected_benchmark);
|
||||
let execute_benchmark = |
|
||||
c: #krate::Vec<(#krate::BenchmarkParameter, u32)>
|
||||
| -> Result<(), #krate::BenchmarkError> {
|
||||
// Always reset the state after the benchmark.
|
||||
#krate::defer!(#krate::benchmarking::wipe_db());
|
||||
|
||||
// Set up the benchmark, return execution + verification function.
|
||||
let closure_to_verify = <
|
||||
SelectedBenchmark as #krate::BenchmarkingSetup<T, _>
|
||||
>::instance(&selected_benchmark, &c, true)?;
|
||||
|
||||
// Set the block number to at least 1 so events are deposited.
|
||||
if #krate::Zero::is_zero(&frame_system::Pallet::<T>::block_number()) {
|
||||
frame_system::Pallet::<T>::set_block_number(1u32.into());
|
||||
}
|
||||
|
||||
// Run execution + verification
|
||||
closure_to_verify()
|
||||
};
|
||||
|
||||
if components.is_empty() {
|
||||
execute_benchmark(Default::default())?;
|
||||
} else {
|
||||
let num_values: u32 = if let Ok(ev) = std::env::var("VALUES_PER_COMPONENT") {
|
||||
ev.parse().map_err(|_| {
|
||||
#krate::BenchmarkError::Stop(
|
||||
"Could not parse env var `VALUES_PER_COMPONENT` as u32."
|
||||
)
|
||||
})?
|
||||
} else {
|
||||
6
|
||||
};
|
||||
|
||||
if num_values < 2 {
|
||||
return Err("`VALUES_PER_COMPONENT` must be at least 2".into());
|
||||
}
|
||||
|
||||
for (name, low, high) in components.clone().into_iter() {
|
||||
// Test the lowest, highest (if its different from the lowest)
|
||||
// and up to num_values-2 more equidistant values in between.
|
||||
// For 0..10 and num_values=6 this would mean: [0, 2, 4, 6, 8, 10]
|
||||
|
||||
let mut values = #krate::vec![low];
|
||||
let diff = (high - low).min(num_values - 1);
|
||||
let slope = (high - low) as f32 / diff as f32;
|
||||
|
||||
for i in 1..=diff {
|
||||
let value = ((low as f32 + slope * i as f32) as u32)
|
||||
.clamp(low, high);
|
||||
values.push(value);
|
||||
}
|
||||
|
||||
for component_value in values {
|
||||
// Select the max value for all the other components.
|
||||
let c: #krate::Vec<(#krate::BenchmarkParameter, u32)> = components
|
||||
.iter()
|
||||
.map(|(n, _, h)|
|
||||
if *n == name {
|
||||
(*n, component_value)
|
||||
} else {
|
||||
(*n, *h)
|
||||
}
|
||||
)
|
||||
.collect();
|
||||
|
||||
execute_benchmark(c)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
res
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#![recursion_limit = "512"]
|
||||
|
||||
mod benchmark;
|
||||
mod clone_no_bound;
|
||||
mod construct_runtime;
|
||||
mod crate_version;
|
||||
@@ -479,6 +480,69 @@ pub fn pallet(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
pallet::pallet(attr, item)
|
||||
}
|
||||
|
||||
/// An attribute macro that can be attached to a (non-empty) module declaration. Doing so will
|
||||
/// designate that module as a benchmarking module.
|
||||
///
|
||||
/// See `frame_support::benchmarking` for more info.
|
||||
#[proc_macro_attribute]
|
||||
pub fn benchmarks(attr: TokenStream, tokens: TokenStream) -> TokenStream {
|
||||
match benchmark::benchmarks(attr, tokens, false) {
|
||||
Ok(tokens) => tokens,
|
||||
Err(err) => err.to_compile_error().into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// An attribute macro that can be attached to a (non-empty) module declaration. Doing so will
|
||||
/// designate that module as an instance benchmarking module.
|
||||
///
|
||||
/// See `frame_support::benchmarking` for more info.
|
||||
#[proc_macro_attribute]
|
||||
pub fn instance_benchmarks(attr: TokenStream, tokens: TokenStream) -> TokenStream {
|
||||
match benchmark::benchmarks(attr, tokens, true) {
|
||||
Ok(tokens) => tokens,
|
||||
Err(err) => err.to_compile_error().into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// An attribute macro used to declare a benchmark within a benchmarking module. Must be
|
||||
/// attached to a function definition containing an `#[extrinsic_call]` or `#[block]`
|
||||
/// attribute.
|
||||
///
|
||||
/// See `frame_support::benchmarking` for more info.
|
||||
#[proc_macro_attribute]
|
||||
pub fn benchmark(_attrs: TokenStream, _tokens: TokenStream) -> TokenStream {
|
||||
quote!(compile_error!(
|
||||
"`#[benchmark]` must be in a module labeled with #[benchmarks] or #[instance_benchmarks]."
|
||||
))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// An attribute macro used to specify the extrinsic call inside a benchmark function, and also
|
||||
/// used as a boundary designating where the benchmark setup code ends, and the benchmark
|
||||
/// verification code begins.
|
||||
///
|
||||
/// See `frame_support::benchmarking` for more info.
|
||||
#[proc_macro_attribute]
|
||||
pub fn extrinsic_call(_attrs: TokenStream, _tokens: TokenStream) -> TokenStream {
|
||||
quote!(compile_error!(
|
||||
"`#[extrinsic_call]` must be in a benchmark function definition labeled with `#[benchmark]`."
|
||||
);)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// An attribute macro used to specify that a block should be the measured portion of the
|
||||
/// enclosing benchmark function, This attribute is also used as a boundary designating where
|
||||
/// the benchmark setup code ends, and the benchmark verification code begins.
|
||||
///
|
||||
/// See `frame_support::benchmarking` for more info.
|
||||
#[proc_macro_attribute]
|
||||
pub fn block(_attrs: TokenStream, _tokens: TokenStream) -> TokenStream {
|
||||
quote!(compile_error!(
|
||||
"`#[block]` must be in a benchmark function definition labeled with `#[benchmark]`."
|
||||
))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Execute the annotated function in a new storage transaction.
|
||||
///
|
||||
/// The return type of the annotated function must be `Result`. All changes to storage performed
|
||||
|
||||
Reference in New Issue
Block a user