-
Notifications
You must be signed in to change notification settings - Fork 2.6k
generation of real benchmark functions for benchmarking v2 #13224
Conversation
Re-opening because of #13277 (comment) |
Interesting edge case @ggwpez: Let's consider the benchmark function def from the balances pallet mentioned above. I think we would expect to be able to write it like this: #[benchmark]
fn force_unreserve() -> BenchmarkResult {
let user: T::AccountId = account("user", 0, SEED);
let user_lookup = T::Lookup::unlookup(user.clone());
// Give some multiple of the existential deposit
let existential_deposit = T::ExistentialDeposit::get();
let balance = existential_deposit.saturating_mul(ED_MULTIPLIER.into());
let _ = <Balances<T, I> as Currency<_>>::make_free_balance_be(&user, balance);
// Reserve the balance
<Balances<T, I> as ReservableCurrency<_>>::reserve(&user, balance)?;
assert_eq!(Balances::<T, I>::reserved_balance(&user), balance);
assert!(Balances::<T, I>::free_balance(&user).is_zero());
#[extrinsic_call]
_(RawOrigin::Root, user_lookup, balance);
assert!(Balances::<T, I>::reserved_balance(&user).is_zero());
assert_eq!(Balances::<T, I>::free_balance(&user), balance);
Ok(())
} The problem is we actually can't do the Ok(#krate::Box::new(move || -> Result<(), #krate::BenchmarkError> {
#post_call
if verify {
#(
#verify_stmts
)*
}
Ok(())
})) So if the last line of There are several workarounds we could do:
Option 3 is the hardest to implement and has the most flexibility and best dev UX imo. Option 2 is more awkward and less flexible but would still be a good option. Option 1 is great other than the strange fact that the programmer doesn't need to (and in fact can't) add the Ultimately I like option 3 the most because it opens up the possibility for the programmer to return a custom type and actually do something with the benchmark function def, but also allows the programmer to be lazy and just have a blank return for simple benchmarks, so this is what I would recommend. Option 3 would look like this for #[benchmark]
fn force_unreserve() -> BenchmarkResult<()> {
let user: T::AccountId = account("user", 0, SEED);
let user_lookup = T::Lookup::unlookup(user.clone());
// Give some multiple of the existential deposit
let existential_deposit = T::ExistentialDeposit::get();
let balance = existential_deposit.saturating_mul(ED_MULTIPLIER.into());
let _ = <Balances<T, I> as Currency<_>>::make_free_balance_be(&user, balance);
// Reserve the balance
<Balances<T, I> as ReservableCurrency<_>>::reserve(&user, balance)?;
assert_eq!(Balances::<T, I>::reserved_balance(&user), balance);
assert!(Balances::<T, I>::free_balance(&user).is_zero());
#[extrinsic_call]
_(RawOrigin::Root, user_lookup, balance);
assert!(Balances::<T, I>::reserved_balance(&user).is_zero());
assert_eq!(Balances::<T, I>::free_balance(&user), balance);
Ok(())
} Other benchmark function defs would be able to remain the way they are since blank return would be supported. |
Yea I think having blank return or possible Result is nice as UX so you are not forced to write PS: Dont know how. My example code does not work. |
Ooh that’s a good idea I will try a closure
…On Mon, Feb 6, 2023 at 11:00 AM Oliver Tale-Yazdi ***@***.***> wrote:
Yea I think having blank return or possible Result is nice as UX so you
are not forced to write Ok(()).
Can you not wrap the verify block in a closure and use ? on it like:
#post_callif verify {
|| {
#(
#verify_stmts
)*
}()?;}Ok(())
or however the exact syntax would look like.
—
Reply to this email directly, view it on GitHub
<#13224 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAOE4LJ5NAOHIA3DNHUKB6DWWENZDANCNFSM6AAAAAAUEPDBMI>
.
You are receiving this because you modified the open/close state.Message
ID: ***@***.***>
|
Yeah I've tried a variety of things and this is actually quite hard to work around. Generally speaking I think I want to optimize for maximum flexibility here because I'm certain someone is going to come out of the woodwork someday who actually wants to call these benchmark function defs and return something other than So something like the following: Programmer would write this (using #[benchmark]
fn force_unreserve() -> BenchmarkResult<String> {
something()?;
let thing = "something";
#[block]
{}
assert_eq!(2 + 2, 4);
assert_eq!(4, 2 + 2);
Ok(thing)
} And we'd do this for the function def expansion: let function_def = quote! {
#vis #sig {
#(
#setup_stmts
)*
#extrinsic_call (or block)
if verify {
#(
#verify_stmts
)*
}
#last_item_of_function_def
}
}; By convention the last statement of the function def would have to be the return value, though custom early returns via the Now if someone tried to create a value in the verify block and return it on the last line, this would (rightly) create a compile error telling them they are returning a potentially uninitialized value. So if they want to return some custom value, they should be setting it in the setup or extrinsic call / block section of the function def. And this way I could still inject I think this should be the all-around best compromise UX-wise after going through all of the alternatives. Thoughts @ggwpez ? |
cfc4b86
to
84c7e86
Compare
What I lay out above is working on the balances pallet, and as a bonus I was able to get blank returns types Still doing some tweaks to get where clauses to work and then need to update/check over the ui tests and add a few. |
Yea could work by using the last line. I guess that would be a good solution - UX wise. |
Yeah will do. It seems to work well and since it's the last statement, we can again have more complex things there like a match etc if someone really needs to |
note the current code isn't producing the extrinsic call / block properly in the function def working on that now edit: should be good now |
This is fully working but doing one more thing to support paths that might lead to |
Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>
Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>
|
||
let vis = benchmark_def.fn_vis; | ||
|
||
// remove #[benchmark] attribute |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are not really functions, so having any kind of non-benchmarking attribute is actually an error, or?
Since FRAME does not interpret them accordingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well the function def we generate in the expansion is real, it's just the visibility only applies to it, not to any of the other stuff we generate, which I think is reasonable. The alternative would be ignoring it (which would be confusing to the programmer when they try to set it to be pub or not pub and the generated function doesn't track with that), or restricting it to be a certain thing (pub or non pub), which I think would be needlessly restrictive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm my comment here was about attribute macros on the benchmarking function. Like should_panic
or whatever would not make sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh right, sorry. I would make the same argument, just about attributes:
I would lean towards just letting all attributes through here. Sure in some cases it might not make sense, but I think it would be more confusing if the programmer tries to use an attribute and it is ignored or was specifically blocked.
Alternatively, we could issue a compiler error for any non-doc attributes.
Right now as far as I know we don't do any special blocking of non-pallet macros on pallet items, and I view this scenario as similar. We could open a new issue to fix this in a bunch of places if desirable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively, we could issue a compiler error for any non-doc attributes.
That is what i mean.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! We can discuss the open discussion afterwards.
bot merge |
…h#13224) * function generation with _name working, need to modify signature * WIP * support custom BenchmarkResult<T> type * full support for BenchmarkResult<T> on benchmark function defs * support () return type for benchmark function defs that don't use ? * uncomment * fix where clause handling * fix benchmark function call bodies * proper parsing of return type * add UI tests for bad return type * fix detection of missing last_stmt with defined return type * UI tests covering missing last_stmt * properly detect and complain about empty benchmark function defs * fix missing Comma in Result<T, BenchmarkError> parsing + test * add additional UI test * allow complex path for BenchmarkResult and BenchmarkError in fn defs * add UI tests covering complex path for BenchmarkResult, BenchmarkError * retain doc comments and attributes * also add attributes to struct * add docs for benchmark function definition support * fix imports on benchmark example * fix issue with unused variables in extrinsic call fn def * fix up docs * remove support for v2::BenchmarkResult because it was confusing * fix typo * remove ability to use custom T for Result<T, BenchmarkError> in v2 * use missing call error instead of empty_fn() * remove unneeded match statement * Add a proper QED Co-authored-by: Keith Yeung <kungfukeith11@gmail.com> * fix other QED Co-authored-by: Keith Yeung <kungfukeith11@gmail.com> * cargo fmt * add an explicit error for non TypePath as return type * tweak error warning and add a UI test for non TypePath return * remove comment * add docs about T and I generic params * improve docs referring to section "below" * pull out return type checking logic into its own function * pull out params parsing into its own function * pull out call_def parsing into its own function * add doc comment for missing_call() * replace spaces with tabs * add a result-based example to the benchmarking examples --------- Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>
* function generation with _name working, need to modify signature * WIP * support custom BenchmarkResult<T> type * full support for BenchmarkResult<T> on benchmark function defs * support () return type for benchmark function defs that don't use ? * uncomment * fix where clause handling * fix benchmark function call bodies * proper parsing of return type * add UI tests for bad return type * fix detection of missing last_stmt with defined return type * UI tests covering missing last_stmt * properly detect and complain about empty benchmark function defs * fix missing Comma in Result<T, BenchmarkError> parsing + test * add additional UI test * allow complex path for BenchmarkResult and BenchmarkError in fn defs * add UI tests covering complex path for BenchmarkResult, BenchmarkError * retain doc comments and attributes * also add attributes to struct * add docs for benchmark function definition support * fix imports on benchmark example * fix issue with unused variables in extrinsic call fn def * fix up docs * remove support for v2::BenchmarkResult because it was confusing * fix typo * remove ability to use custom T for Result<T, BenchmarkError> in v2 * use missing call error instead of empty_fn() * remove unneeded match statement * Add a proper QED Co-authored-by: Keith Yeung <kungfukeith11@gmail.com> * fix other QED Co-authored-by: Keith Yeung <kungfukeith11@gmail.com> * cargo fmt * add an explicit error for non TypePath as return type * tweak error warning and add a UI test for non TypePath return * remove comment * add docs about T and I generic params * improve docs referring to section "below" * pull out return type checking logic into its own function * pull out params parsing into its own function * pull out call_def parsing into its own function * add doc comment for missing_call() * replace spaces with tabs * add a result-based example to the benchmarking examples --------- Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>
…h#13224) * function generation with _name working, need to modify signature * WIP * support custom BenchmarkResult<T> type * full support for BenchmarkResult<T> on benchmark function defs * support () return type for benchmark function defs that don't use ? * uncomment * fix where clause handling * fix benchmark function call bodies * proper parsing of return type * add UI tests for bad return type * fix detection of missing last_stmt with defined return type * UI tests covering missing last_stmt * properly detect and complain about empty benchmark function defs * fix missing Comma in Result<T, BenchmarkError> parsing + test * add additional UI test * allow complex path for BenchmarkResult and BenchmarkError in fn defs * add UI tests covering complex path for BenchmarkResult, BenchmarkError * retain doc comments and attributes * also add attributes to struct * add docs for benchmark function definition support * fix imports on benchmark example * fix issue with unused variables in extrinsic call fn def * fix up docs * remove support for v2::BenchmarkResult because it was confusing * fix typo * remove ability to use custom T for Result<T, BenchmarkError> in v2 * use missing call error instead of empty_fn() * remove unneeded match statement * Add a proper QED Co-authored-by: Keith Yeung <kungfukeith11@gmail.com> * fix other QED Co-authored-by: Keith Yeung <kungfukeith11@gmail.com> * cargo fmt * add an explicit error for non TypePath as return type * tweak error warning and add a UI test for non TypePath return * remove comment * add docs about T and I generic params * improve docs referring to section "below" * pull out return type checking logic into its own function * pull out params parsing into its own function * pull out call_def parsing into its own function * add doc comment for missing_call() * replace spaces with tabs * add a result-based example to the benchmarking examples --------- Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>
…h#13224) * function generation with _name working, need to modify signature * WIP * support custom BenchmarkResult<T> type * full support for BenchmarkResult<T> on benchmark function defs * support () return type for benchmark function defs that don't use ? * uncomment * fix where clause handling * fix benchmark function call bodies * proper parsing of return type * add UI tests for bad return type * fix detection of missing last_stmt with defined return type * UI tests covering missing last_stmt * properly detect and complain about empty benchmark function defs * fix missing Comma in Result<T, BenchmarkError> parsing + test * add additional UI test * allow complex path for BenchmarkResult and BenchmarkError in fn defs * add UI tests covering complex path for BenchmarkResult, BenchmarkError * retain doc comments and attributes * also add attributes to struct * add docs for benchmark function definition support * fix imports on benchmark example * fix issue with unused variables in extrinsic call fn def * fix up docs * remove support for v2::BenchmarkResult because it was confusing * fix typo * remove ability to use custom T for Result<T, BenchmarkError> in v2 * use missing call error instead of empty_fn() * remove unneeded match statement * Add a proper QED Co-authored-by: Keith Yeung <kungfukeith11@gmail.com> * fix other QED Co-authored-by: Keith Yeung <kungfukeith11@gmail.com> * cargo fmt * add an explicit error for non TypePath as return type * tweak error warning and add a UI test for non TypePath return * remove comment * add docs about T and I generic params * improve docs referring to section "below" * pull out return type checking logic into its own function * pull out params parsing into its own function * pull out call_def parsing into its own function * add doc comment for missing_call() * replace spaces with tabs * add a result-based example to the benchmarking examples --------- Co-authored-by: Keith Yeung <kungfukeith11@gmail.com>
TLDR: the purpose of this is to get some useful compiler errors and warnings if people try to do bad things inside of benchmark function defs, and we accomplish this by actually generating real functions from the defs that they write. These functions aren't actually used for anything, though the programmer is free to call them if they wish. The underlying benchmarking impls take code that is essentially copy pasted from these function defs to fill in a number of impls i.e. the setup, call, and verification sections end up inside several closures in trait impls. See #13277 and the related issues below to understand why real functions are desirable.
This has been re-opened because several other issues, including #13277 could be resolved by generating benchmark function definitions.
Right now, we use function definitions like this to define benchmarks in the benchmarking v2 syntax:
Currently this compiles (even though there is a
?
operator but our function doesn't returnResult
) because the return types on the benchmark function defs are ignored by the benchmarking macros, because there isn't actually a real function that gets generated, instead the setup, extrinsic call, and verification sections of this code get put into trait impls that allow this to compile. In fact right now you can put any return type and it will compile as long as it is parsable, even if you use non-existent types, as noted by #13278.In #13277 we attempted to alleviate this by requiring certain return types in certain scenarios and manually adding a compile error if the
?
operator is used with an incompatible return-type, but this had edge cases that were difficult to handle (i.e. people returning Err manually, etc) that frankly should be handled by the compiler.Another issue that arose because of this is right now we do not get warnings if a benchmark parameter is not used in the function body, but we definitely want these warnings (#13303)
The solution for all of these issues is to actually generate real function definitions, which is what this PR sets out to do.
Progress
verify
param to generated function signatureBenchmarkResult<T>
when we want to return a result and/or use?
operator()
returns so existing benchmarks work unmodifiedBenchmarkResult
andBenchmarkError
instead of justBenchmarkResult
andBenchmarkError
v2::BenchmarkResult
and just stick withResult<T, BenchmarkError>
because this name otherwise collides with the existingBenchmarkResult
used by the internal benchmarking code, causing confusion.Result<T, BenchmarkError>
syntax and just stick withResult<(), BenchmarkError>
based on feedback and potential WASM boundary issuesRelated Issues
///
comments on benchmarks. #13383