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:
Sam Johnson
2023-01-23 02:07:48 -05:00
committed by GitHub
parent 82075c1d26
commit 42e5c27c84
38 changed files with 1761 additions and 135 deletions
@@ -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