Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft benchmark sanity weight test #1493

Closed
wants to merge 16 commits into from
Closed

Conversation

Daanvdplas
Copy link
Contributor

@Daanvdplas Daanvdplas commented Sep 11, 2023

This PR introduces a sanity weight check during the benchmark execution. It checks whether the max weight of a given extrinsic exceeds that of the (DispatchClass::Normal) max_extrinsic_weight obtained from the runtime. The max weight of an extrinsic can be default (no complexity parameter) or else, the max component is used.

I have included the benchmark_sanity_check in the frame-benchmarking-cli because the component range is available. In addition, a flag: sanity-check is included to have the cli user choose whether the benchmark execution should fail when the max_extrinsic_weight exceeds. A warning message will always be printed when the max_extrinsic_weight is exceeded. Warning example:
Screenshot 2023-09-11 at 14 44 55

This is a draft PR to get confirmation on my approach. If approved, things that are left to do:

  • Include benchmark_sanity_check for other benchmark operations than benchmarking a pallet.
  • Check for corresponding Polkadot/Cumulus PR
  • Finalise PR

Closes #152

@Daanvdplas Daanvdplas added C1-mentor A task where a mentor is available. Please indicate in the issue who the mentor could be. I5-enhancement An additional feature request. T1-FRAME This PR/Issue is related to core FRAME, the framework. labels Sep 11, 2023
@ggwpez
Copy link
Member

ggwpez commented Sep 15, 2023

Incident where we needed exactly this: #1589
So the default behaviour should actually be to ERROR out at the end of the program, after successfully writing all weight files.

@Daanvdplas
Copy link
Contributor Author

Daanvdplas commented Sep 18, 2023

@ggwpez should I still include a flag that can turn the error into a warning?

@ggwpez
Copy link
Member

ggwpez commented Sep 18, 2023

@ggwpez should I still include a flag that can turn the error into a warning?

Yea maybe something like: --weight-sanity-check=warn | error | ignore where error is the default.

@0xmovses
Copy link
Contributor

@Daanvdplas are you still working on this? I'm looking for issues to pick up.

@Daanvdplas
Copy link
Contributor Author

Yes! I have been blocked for a while but will continue working on it tomorrow!

substrate/client/cli/src/arg_enums.rs Outdated Show resolved Hide resolved
substrate/client/cli/src/arg_enums.rs Show resolved Hide resolved
substrate/client/cli/src/params/shared_params.rs Outdated Show resolved Hide resolved
substrate/client/cli/src/params/shared_params.rs Outdated Show resolved Hide resolved
@@ -235,7 +238,7 @@ sp_api::decl_runtime_apis! {
/// Parameters
/// - `extra`: Also list benchmarks marked "extra" which would otherwise not be
/// needed for weight calculation.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know the docs here are pretty bad, but could you please explain the return args that you added?

let mut sanity_weight_check_passed = true;
// Loop through all benchmark results.
for ((pallet, instance), results) in all_results.iter() {
println!("Pallet: {}\nInstance: {}\n", pallet, instance);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would make sense to add another level to SanityWeightCheck: Info.
Since here you print things that are neither a warning nor an error and are not really important as long as the whole check passes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only do not see a reason for only outputting Info (the above line and weight results, without a warning) when Warning only adds a warning line above the failing weight results and a line regarding passing / failing the sanity weight check at the end.

If someone would like to inspect its weight results, personally I would like an indicator that clearly indicates which weight function is failing and if no failing weight results I would like formatting that gives a clear view to inspect my weight results. The above line is just to format the sanity weight check results the most clear to my opinion.

Please let me know if I'm understanding your incorrectly and/or what you think of the above :)

return (list, storage_info)
let max_extrinsic_weight = BlockWeights::get().per_class.get(DispatchClass::Normal).max_extrinsic.unwrap();
let db_weight: frame_support::weights::RuntimeDbWeight = <Self as frame_system::Config>::DbWeight::get();
(list, storage_info, max_extrinsic_weight, db_weight)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hoped that we could somehow around doing a breaking change for all runtimes, but it seems unavoidable, unless we want to squeeze this into into any of the vectors.
Maybe you can create a single struct that holds the Weight and RuntimeDbWeight. Then we can extend that in the future and avoid more breaking changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating a single struct for Weight and RuntimeDbWeight is either way a better solution. Would you prefer to have it included in BenchmarkList / StorageInfo and make it backwards compatible or add an additional parameter which results in a breaking change?

Copy link
Member

@ggwpez ggwpez Feb 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would need one breaking change, yes. But from then on we can avoid further breaking changes by using the following pattern:

struct AllBenchmarkData { // not a good name
  list: Vec<frame_benchmarking::BenchmarkList>,
  info: Vec<frame_support::traits::StorageInfo>,
  max_ext_weight: Option<..>,
  db_weights: Option<..>
}

and having Default on that thing. The runtime devs can use ..Default::default() to stay forward compatible and we can keep adding Options without directly breaking their code.

@Daanvdplas Daanvdplas marked this pull request as ready for review October 31, 2023 15:42
@Daanvdplas Daanvdplas requested a review from a team October 31, 2023 15:42
@Daanvdplas Daanvdplas requested a review from a team as a code owner October 31, 2023 15:42
@paritytech-ci paritytech-ci requested review from a team October 31, 2023 15:43
@chevdor
Copy link
Contributor

chevdor commented Oct 31, 2023

Nice !
You are introducing a check against an upper bound. Did you consider a min as well ?
A weight that is anormally high can be problematic but a weight that is too low opens a door to a range of security issues incl. spamming.

// value) and compares it with the max. extrinsic weight allowed in a single block.
//
// `max_extrinsic_weight` & `db_weight` are obtained from the runtime configuration.
pub(crate) fn sanity_weight_check(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conceptually, the way to avoid all of these breaking changes is to do this check in the runtime. Is there a reason why it was chosen in the way that it is now?

As in, there is a single new runtime API through which you pass in all the computed weights, and it would do any checking necessary for it.

Ofc this is also a breaking change differently, but possibly an alternative design.

I think the current design is also good, but something to keep in mind.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading the code, I guess the runtime also doesn't know the maximum value for each component..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After looking at it again, the component ranges are coming from the runtime actually: parameter list

I'm investigating another approach

Comment on lines +221 to +224
total_weight = total_weight.saturating_add(
Weight::from_parts(component.slope.try_into().unwrap(), 0)
.saturating_mul(max_component(&component, &result.component_ranges)),
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
total_weight = total_weight.saturating_add(
Weight::from_parts(component.slope.try_into().unwrap(), 0)
.saturating_mul(max_component(&component, &result.component_ranges)),
);
total_weight.saturating_accrue(
Weight::from_parts(component.slope.try_into().unwrap(), 0)
.saturating_mul(max_component(&component, &result.component_ranges)),
);

Weight::from_parts(0, component.slope.try_into().unwrap())
.saturating_mul(max_component(&component, &result.component_ranges)),
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kinda baffled that all of this math to calculate the final weight of a weight-fn, for a given parameter, needs to be re-created here. Is this really the only place where we have this logic?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic only exists in the benchmark templates (i.e. here & here)

Copy link
Contributor

@kianenigma kianenigma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All in all looks good, modulo some comments from me and Oliver. I would consider making the default in fact be Warn or something that lets as many teams out there know about their misconfig.

@Daanvdplas
Copy link
Contributor Author

You are introducing a check against an upper bound. Did you consider a min as well ?

@chevdor I'm not sure how to determine such minimum_extrinsic_weight because from my understanding the weight / fee system is the protection against attacks such as spamming. A warning when a benchmark result is suspiciously low is not a bad idea though. A suggestion; I could add a check which compares the weight result against the weight of the remark extrinsic`. Curious what others think about this!

@chevdor
Copy link
Contributor

chevdor commented Nov 2, 2023

I am not convinced you can use the remark as reference.
An idea is to track the previous values and create an "acceptable range" around that previous value(s) (per extrinsic). That means finding a way to keep track of the previous value(s).

@Daanvdplas
Copy link
Contributor Author

@chevdor sorry I do not follow what you mean with "previous values", could you kindly elaborate?

@chevdor
Copy link
Contributor

chevdor commented Nov 2, 2023

I am afraid my suggestion goes beyond the scope of this PR.
An idea that has been floating around for a while is to keep track of historical weights per extrsinsic and ensure that new weights remain "fairly close" to the weight median value. This is simple but requires storing this historical data (which could even be limited to a single value at first).

@ggwpez ggwpez removed the C1-mentor A task where a mentor is available. Please indicate in the issue who the mentor could be. label Feb 5, 2024
@bkchr bkchr requested a review from ggwpez February 5, 2024 11:38
@kianenigma
Copy link
Contributor

@Daanvdplas do you intend to finish this? happy to delegate to someone else if otherwise.

@Daanvdplas Daanvdplas requested a review from a team as a code owner February 5, 2024 16:17
@kianenigma
Copy link
Contributor

PR also needs a better description that is targeted toward externals rather than internals + prdoc.

@paritytech-cicd-pr
Copy link

The CI pipeline was cancelled due to failure one of the required jobs.
Job name: cargo-clippy
Logs: https://gitlab.parity.io/parity/mirrors/polkadot-sdk/-/jobs/5122166

@kianenigma
Copy link
Contributor

still keen on this, but still missing the CI to be green :)

@ggwpez
Copy link
Member

ggwpez commented Feb 28, 2024

@philoniare this one could also be a good issue for you 😄

@kianenigma
Copy link
Contributor

Also raising this again as something that we'd be highly interested in having. I would suggest a large tip for the champion to finish this :)

@dastansam
Copy link
Contributor

Also raising this again as something that we'd be highly interested in having. I would suggest a large tip for the champion to finish this :)

hey @kianenigma

would like to work on this. can you please assign this to me?

@dastansam
Copy link
Contributor

I completed resolving open comments and fixing CI errors. Opened a new PR to this branch since I don't have write access. let me know if you have any more comments

cc @kianenigma @ggwpez

@kianenigma
Copy link
Contributor

Closing in fav of the one above. Thank you @Daanvdplas.

@kianenigma kianenigma closed this Jul 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
I5-enhancement An additional feature request. T1-FRAME This PR/Issue is related to core FRAME, the framework.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Automatic weight sanity checker
7 participants