//! # FRAME Benchmarking and Weights. //! //! This reference doc explores the concept of weights within Pezkuwi-SDK runtimes, and more //! specifically how FRAME-based runtimes handle it. //! //! ## Metering //! //! The existence of "weight" as a concept in Pezkuwi-SDK is a direct consequence of the usage of //! WASM as a virtual machine. Unlike a metered virtual machine like EVM, where every instruction //! can have a (fairly) deterministic "cost" (also known as "gas price") associated with it, WASM is //! a stack machine with more complex instruction set, and more unpredictable execution times. This //! means that unlike EVM, it is not possible to implement a "metering" system in WASM. A metering //! system is one in which instructions are executed one by one, and the cost/gas is stored in an //! accumulator. The execution may then halt once a gas limit is reached. //! //! In Pezkuwi-SDK, the WASM runtime is not assumed to be metered. //! //! ## Trusted Code //! //! Another important difference is that EVM is mostly used to express smart contracts, which are //! foreign and untrusted codes from the perspective of the blockchain executing them. In such //! cases, metering is crucial, in order to ensure a malicious code cannot consume more gas than //! expected. //! //! This assumption does not hold about the runtime of Pezkuwi-SDK-based blockchains. The runtime //! is trusted code, and it is assumed to be written by the same team/developers who are running the //! blockchain itself. Therefore, this assumption of "untrusted foreign code" does not hold. //! //! This is why the runtime can opt for a more performant, more flexible virtual machine like WASM, //! and get away without having metering. //! //! ## Benchmarking //! //! With the matter of untrusted code execution out of the way, the need for strict metering goes //! out of the way. Yet, it would still be very beneficial for block producers to be able to know an //! upper bound on how much resources a operation is going to consume before actually executing that //! operation. This is why FRAME has a toolkit for benchmarking pallets: So that this upper bound //! can be empirically determined. //! //! > Note: Benchmarking is a static analysis: It is all about knowing the upper bound of how much //! > resources an operation takes statically, without actually executing it. In the context of //! > FRAME extrinsics, this static-ness is expressed by the keyword "pre-dispatch". //! //! To understand why this upper bound is needed, consider the following: A block producer knows //! they have 20ms left to finish producing their block, and wishes to include more transactions in //! the block. Yet, in a metered environment, it would not know which transaction is likely to fit //! the 20ms. In a benchmarked environment, it can examine the transactions for their upper bound, //! and include the ones that are known to fit based on the worst case. //! //! The benchmarking code can be written as a part of FRAME pallet, using the macros provided in //! [`pezframe_benchmarking`]. See any of the existing pallets in `pezkuwi-sdk`, or the pallets in our //! [`crate::pezkuwi_sdk::templates`] for examples. //! //! ## Weight //! //! Finally, [`pezsp_weights::Weight`] is the output of the benchmarking process. It is a //! two-dimensional data structure that demonstrates the resources consumed by a given block of //! code (for example, a transaction). The two dimensions are: //! //! * reference time: The time consumed in pico-seconds, on a reference hardware. //! * proof size: The amount of storage proof necessary to re-execute the block of code. This is //! mainly needed for teyrchain <> relay-chain verification. //! //! ## How To Write Benchmarks: Worst Case //! //! The most important detail about writing benchmarking code is that it must be written such that //! it captures the worst case execution of any block of code. //! //! Consider: #![doc = docify::embed!("./src/reference_docs/pezframe_benchmarking_weight.rs", simple_transfer)] //! //! If this block of code is to be benchmarked, then the benchmarking code must be written such that //! it captures the worst case. //! //! ## Gluing Pallet Benchmarking with Runtime //! //! FRAME pallets are mandated to provide their own benchmarking code. Runtimes contain the //! boilerplate needed to run these benchmarking (see [Running Benchmarks //! below](#running-benchmarks)). The outcome of running these benchmarks are meant to be fed back //! into the pallet via a conventional `trait WeightInfo` on `Config`: #![doc = docify::embed!("src/reference_docs/pezframe_benchmarking_weight.rs", WeightInfo)] //! //! Then, individual functions of this trait are the final values that we assigned to the //! [`frame::pezpallet_macros::weight`] attribute: #![doc = docify::embed!("./src/reference_docs/pezframe_benchmarking_weight.rs", simple_transfer_2)] //! //! ## Manual Refund //! //! Back to the assumption of writing benchmarks for worst case: Sometimes, the pre-dispatch weight //! significantly differ from the post-dispatch actual weight consumed. This can be expressed with //! the following FRAME syntax: #![doc = docify::embed!("./src/reference_docs/pezframe_benchmarking_weight.rs", simple_transfer_3)] //! //! ## Running Benchmarks //! //! Two ways exist to run the benchmarks of a runtime. //! //! 1. The old school way: Most Pezkuwi-SDK based nodes (such as the ones integrated in //! [`templates`]) have a `benchmark` subcommand integrated into themselves. //! 2. The more [`crate::reference_docs::omni_node`] compatible way of running the benchmarks would //! be using [`frame-omni-bencher`] CLI, which only relies on a runtime. //! //! Note that by convention, the runtime and pallets always have their benchmarking code feature //! gated as behind `runtime-benchmarks`. So, the runtime should be compiled with `--features //! runtime-benchmarks`. //! //! ## Automatic Refund of `proof_size`. //! //! A new feature in FRAME allows the runtime to be configured for "automatic refund" of the proof //! size weight. This is very useful for maximizing the throughput of teyrchains. Please see: //! [`crate::guides::enable_pov_reclaim`]. //! //! ## Summary //! //! Pezkuwi-SDK runtimes use a more performant VM, namely WASM, which does not have metering. In //! return they have to be benchmarked to provide an upper bound on the resources they consume. This //! upper bound is represented as [`pezsp_weights::Weight`]. //! //! ## Future: PolkaVM //! //! With the transition of Pezkuwi relay chain to [JAM], a set of new features are being //! introduced, one of which being a new virtual machine named [PolkaVM] that is as flexible as //! WASM, but also capable of metering. This might alter the future of benchmarking in FRAME and //! Pezkuwi-SDK, rendering them not needed anymore once PolkaVM is fully integrated into //! Pezkuwi-sdk. For a basic explanation of JAM and PolkaVM, see [here](https://blog.kianenigma.com/posts/tech/demystifying-jam/#pvm). //! //! //! [`frame-omni-bencher`]: https://crates.io/crates/frame-omni-bencher //! [`templates`]: crate::pezkuwi_sdk::templates //! [PolkaVM]: https://github.com/koute/polkavm //! [JAM]: https://graypaper.com #[frame::pallet(dev_mode)] #[allow(unused_variables, unreachable_code, unused, clippy::diverging_sub_expression)] pub mod pallet { use frame::prelude::*; #[docify::export] pub trait WeightInfo { fn simple_transfer() -> Weight; } #[pallet::config] pub trait Config: pezframe_system::Config { type WeightInfo: WeightInfo; } #[pallet::pallet] pub struct Pallet(_); #[pallet::call] impl Pallet { #[docify::export] #[pallet::weight(10_000)] pub fn simple_transfer( origin: OriginFor, destination: T::AccountId, amount: u32, ) -> DispatchResult { let destination_exists = todo!(); if destination_exists { // simpler code path } else { // more complex code path } Ok(()) } #[docify::export] #[pallet::weight(T::WeightInfo::simple_transfer())] pub fn simple_transfer_2( origin: OriginFor, destination: T::AccountId, amount: u32, ) -> DispatchResult { let destination_exists = todo!(); if destination_exists { // simpler code path } else { // more complex code path } Ok(()) } #[docify::export] // This is the worst-case, pre-dispatch weight. #[pallet::weight(T::WeightInfo::simple_transfer())] pub fn simple_transfer_3( origin: OriginFor, destination: T::AccountId, amount: u32, ) -> DispatchResultWithPostInfo { // ^^ Notice the new return type let destination_exists = todo!(); if destination_exists { // simpler code path // Note that need for .into(), to convert `()` to `PostDispatchInfo` // See: https://docs.pezkuwichain.io/sdk/master/pezframe_support/dispatch/struct.PostDispatchInfo.html#impl-From%3C()%3E-for-PostDispatchInfo Ok(().into()) } else { // more complex code path let actual_weight = todo!("this can likely come from another benchmark that is NOT the worst case"); let pays_fee = todo!("You can set this to `Pays::Yes` or `Pays::No` to change if this transaction should pay fees"); Ok(frame::deps::pezframe_support::dispatch::PostDispatchInfo { actual_weight: Some(actual_weight), pays_fee, }) } } } }