substrate icon indicating copy to clipboard operation
substrate copied to clipboard

benchmarking macro overhaul

Open sam0x17 opened this issue 1 year ago • 37 comments

Description

This PR introduces a new, simplified, and easier-to-use syntax for defining benchmarks that is based on a proc macro instead of a macro_rules! macro like the current system.

Right now the syntax looks like this:

#[benchmarks]
mod benchmarks {
    use super::*;
    ...
    #[benchmark]
    fn transfer_increasing_users(u: Linear<0, 1_000>) {
        let existential_deposit = T::ExistentialDeposit::get();
        let caller = whitelisted_caller();
        let balance = existential_deposit.saturating_mul(ED_MULTIPLIER.into());
        let _ = <Balances<T, I> as Currency<_>>::make_free_balance_be(&caller, balance);
        let recipient: T::AccountId = account("recipient", 0, SEED);
        let recipient_lookup = T::Lookup::unlookup(recipient.clone());
        let transfer_amount = existential_deposit.saturating_mul((ED_MULTIPLIER - 1).into()) + 1u32.into();
        for i in 0..u {
            let new_user: T::AccountId = account("new_user", i, SEED);
            let _ = <Balances<T, I> as Currency<_>>::make_free_balance_be(&new_user, balance);
        }

        #[extrinsic_call]
        transfer(RawOrigin::Signed(caller.clone()), recipient_lookup, transfer_amount);

        assert_eq!(Balances::<T, I>::free_balance(&caller), Zero::zero());
        assert_eq!(Balances::<T, I>::free_balance(&recipient), transfer_amount);
    }
    ...
}

Syntax

The #[benchmarks] macro expects a module containing any number of benchmark puesdo function definitions such as the one shown above to be provided in a block. These function definitions should be annotated with #[benchmark]. A where clause can be provided as an optional argument i.e. #[benchmarks(where <clause here>)].

Each benchmark function definition should be zero arg, or should specify one or more named parameters (that meets the requirements of frame_benchmarking::BenchmarkParameter with a type that implements frame_support::benchmarking::ParamRange. Right now the only struct implementing this trait is Linear, which has two generic u32 parameters`:

pub struct Linear<const A: u32, const B: u32>;

These generic parameters represent the (inclusive) start and end range of the benchmark parameter. So if you want to specify a range of 1 to 100,000 (inclusive) for the variable x, you would use:

#[benchmark]
fn my_benchmark(x: Linear<1, 100_000>) {

As mentioned before, you can also specify multiple parameters if necessary:

#[benchmark]
fn my_benchmark(x: Linear<100, 500>, y: Linear<200, 600>) {

These benchmark function definitions can be optionally annotated with the extra and skip_meta i.e. #[benchmark(skip_meta, extra)] and these function the same way they used to function when they were attributes in the old benchmarking syntax

Note that while these benchmark function definitions appear to be function definitions, currently they are not compiled as such (mainly because the signature would be incorrect with respect to the benchmark parameters). If generating a function that resembles the benchmark function definition is desirable, I could easily add this by changing the signature at compile time to transform things like x: Linear<0, 10> to x: u32. If this would be of practical use beyond the structs and trait impls already created by the macro then let me know!

The #[extrinsic_call] acts as a bisector between the setup code, the extrinsic call, and any verification code that needs to go at the end of the benchmark. The extrinsic call can be either block style (pass a block to #[extrinsic_call]), or must be a function call do an extrinsic where the first argument is the origin.

UPDATE: now we use #[block] for the block-style call, and #[extrinsic_call] for an actual extrinsic call.

Instance Benchmarks

You can specify #[instance_benchmarks] in place of #[benchmarks] if your pallet requires this.

Outer Macro Pattern

The new setup uses proc macros and the outer macro pattern, similar to the way pallets themselves are defined, with the caveat that individual benchmark function definitions can technically be parsed and expanded on their own -- the only reason we need the outer macro pattern is once all benchmarks within a benchmarks! {} block have been processed, we need to iterate over them and implement some traits such as run_benchmarks that require knowledge of all of the benchmarks, so this is why we need the outer macro pattern.

Status

  • [X] parsing for new #[benchmark] attribute
  • [X] parsing "bisection" technique for picking out the #[extrinsic_call] and using this to separate the setup code from the verification code
  • [X] new ParamRange trait and a generic Linear struct that implements it
  • [X] parsing support for ParamRange
  • [X] parsing for new benchmarks! {} outer macro
  • [X] basic expansion for #[benchmark] functions
  • [X] expansion of benchmarks! {} macro
  • [X] conversion of the Balances pallet to the new syntax
  • [X] decide on syntax for specifying that we want to code-gen benchmark tests (previously was impl_benchmark_test_suite!, ~~maybe we want to keep this?~~ Update: yes, we need this
  • [x] parsing + expansion for test suite syntax, if applicable
  • [x] proper handling of where clauses
  • [x] docs
  • [X] UI tests

Related Issues

  • closes #10848

sam0x17 avatar Dec 13 '22 20:12 sam0x17