diff --git a/Cargo.dev.toml b/Cargo.dev.toml index 1447bf3e7..1b3b68b5b 100644 --- a/Cargo.dev.toml +++ b/Cargo.dev.toml @@ -4,6 +4,7 @@ cargo-features = ["resolver"] members = [ "auction", "authority", + "bencher", "benchmarking", "currencies", "gradually-update", @@ -20,6 +21,8 @@ members = [ # "xcm-support", # "unknown-tokens", "build-script-utils", + "weight-gen", + "weight-meter", ] resolver = "2" @@ -67,6 +70,7 @@ sp-authority-discovery = { git = "https://github.com/paritytech//substrate", rev sc-executor-common = { git = "https://github.com/paritytech//substrate", rev = "3f110196163b5ec03bac5ee188d60bedf3ebd91d" } sc-executor-wasmi = { git = "https://github.com/paritytech//substrate", rev = "3f110196163b5ec03bac5ee188d60bedf3ebd91d" } sc-executor = { git = "https://github.com/paritytech//substrate", rev = "3f110196163b5ec03bac5ee188d60bedf3ebd91d" } +sc-client-db = { git = "https://github.com/paritytech//substrate", rev = "3f110196163b5ec03bac5ee188d60bedf3ebd91d" } sc-client-api = { git = "https://github.com/paritytech//substrate", rev = "3f110196163b5ec03bac5ee188d60bedf3ebd91d" } sp-tasks = { git = "https://github.com/paritytech//substrate", rev = "3f110196163b5ec03bac5ee188d60bedf3ebd91d" } sp-authorship = { git = "https://github.com/paritytech//substrate", rev = "3f110196163b5ec03bac5ee188d60bedf3ebd91d" } diff --git a/bencher/Cargo.toml b/bencher/Cargo.toml new file mode 100644 index 000000000..5c8eeba26 --- /dev/null +++ b/bencher/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "orml-bencher" +description = "Provide macro to benchmark pallets." +repository = "https://github.com/open-web3-stack/open-runtime-module-library/tree/master/bencher" +license = "Apache-2.0" +version = "0.4.1-dev" +authors = ["Laminar Developers "] +edition = "2018" + +[dependencies] +linregress = { version = "0.4.0", optional = true } +serde = { version = "1.0.119", optional = true, features = ['derive'] } +serde_json = {version = "1.0.64", optional = true } +codec = { package = "parity-scale-codec", version = "2.0.0", features = ["derive"], default-features = false } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1", default-features = false } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1", default-features = false } +sp-runtime-interface = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1", default-features = false } +sp-state-machine = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1", default-features = false, optional = true } +sc-executor = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1", default-features = false, features = ["wasmtime"], optional = true } +sc-client-db = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1", default-features = false, features = ["with-kvdb-rocksdb"], optional = true } +frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1", default-features = false } + +[features] +default = ["std"] +std = [ + "linregress", + "serde/std", + "serde_json/std", + "codec/std", + "sp-core/std", + "sp-std/std", + "sp-io/std", + "sp-runtime-interface/std", + "sp-state-machine/std", + "sc-executor/std", + "sc-client-db", + "frame-benchmarking/std", +] diff --git a/bencher/src/bench_runner.rs b/bencher/src/bench_runner.rs new file mode 100644 index 000000000..26fabc066 --- /dev/null +++ b/bencher/src/bench_runner.rs @@ -0,0 +1,39 @@ +use frame_benchmarking::{ + benchmarking, + frame_support::sp_runtime::traits::{Block, NumberFor}, +}; +use sc_client_db::BenchmarkingState; +use sc_executor::{sp_wasm_interface::HostFunctions, WasmExecutionMethod, WasmExecutor}; +use sp_core::traits::{CallInWasm, MissingHostFunctions}; +use sp_io::SubstrateHostFunctions; +use sp_state_machine::{Ext, OverlayedChanges, StorageTransactionCache}; + +/// Run benches +pub fn run(wasm_code: Vec) -> Vec { + let mut overlay = OverlayedChanges::default(); + let mut cache = StorageTransactionCache::default(); + let state = BenchmarkingState::::new(Default::default(), Default::default(), false).unwrap(); + let mut ext = Ext::<_, NumberFor, _>::new(&mut overlay, &mut cache, &state, None, None); + + let mut host_functions = benchmarking::HostFunctions::host_functions(); + host_functions.append(&mut SubstrateHostFunctions::host_functions()); + + let executor = WasmExecutor::new( + WasmExecutionMethod::Compiled, + Default::default(), + host_functions, + 1, + None, + ); + + executor + .call_in_wasm( + &wasm_code[..], + None, + "run_benches", + &[], + &mut ext, + MissingHostFunctions::Disallow, + ) + .unwrap() +} diff --git a/bencher/src/handler.rs b/bencher/src/handler.rs new file mode 100644 index 000000000..8533d2db1 --- /dev/null +++ b/bencher/src/handler.rs @@ -0,0 +1,54 @@ +use crate::BenchResult; +use codec::Decode; +use linregress::{FormulaRegressionBuilder, RegressionDataBuilder}; +use serde::{Deserialize, Serialize}; +use std::io::Write; + +#[derive(Serialize, Deserialize, Default, Debug, Clone)] +struct BenchData { + pub name: String, + pub base_weight: u64, + pub base_reads: u32, + pub base_writes: u32, +} + +/// Handle bench results +pub fn handle(output: Vec) { + let results = as Decode>::decode(&mut &output[..]).unwrap(); + let data: Vec = results + .into_iter() + .map(|result| { + let name = String::from_utf8_lossy(&result.method).to_string(); + + eprintln!("{:#?}", result); + + let y: Vec = result.elapses.into_iter().map(|x| x as f64).collect(); + let x: Vec = (0..y.len()).into_iter().map(|x| x as f64).collect(); + let data = vec![("Y", y), ("X", x)]; + let data = RegressionDataBuilder::new().build_from(data).unwrap(); + let formula = "Y ~ X"; + + let model = FormulaRegressionBuilder::new() + .data(&data) + .formula(formula) + .fit() + .unwrap(); + + BenchData { + name, + base_weight: model.parameters.intercept_value as u64 * 1_000, + base_reads: result.reads, + base_writes: result.writes, + } + }) + .collect(); + + if let Ok(json) = serde_json::to_string(&data) { + let stdout = ::std::io::stdout(); + let mut handle = stdout.lock(); + + handle.write_all(&json.as_bytes()).unwrap(); + } else { + eprintln!("Could not write benchdata to JSON"); + } +} diff --git a/bencher/src/lib.rs b/bencher/src/lib.rs new file mode 100644 index 000000000..2ef25f776 --- /dev/null +++ b/bencher/src/lib.rs @@ -0,0 +1,28 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +#[doc(hidden)] +pub extern crate frame_benchmarking; +#[doc(hidden)] +pub extern crate sp_core; +#[doc(hidden)] +pub extern crate sp_std; + +use codec::{Decode, Encode}; +use sp_std::prelude::Vec; + +#[derive(Encode, Decode, Default, Clone, PartialEq, Debug)] +pub struct BenchResult { + pub method: Vec, + pub elapses: Vec, + pub reads: u32, + pub repeat_reads: u32, + pub writes: u32, + pub repeat_writes: u32, +} + +mod macros; + +#[cfg(feature = "std")] +pub mod bench_runner; +#[cfg(feature = "std")] +pub mod handler; diff --git a/bencher/src/macros.rs b/bencher/src/macros.rs new file mode 100644 index 000000000..f6548136a --- /dev/null +++ b/bencher/src/macros.rs @@ -0,0 +1,160 @@ +/// Run benches in WASM environment. +/// +/// Configure your module to build the mock runtime into wasm code. +/// Create a `build.rs` like you do with your runtime. +/// ```.ignore +/// use substrate_wasm_builder::WasmBuilder; +/// fn main() { +/// WasmBuilder::new() +/// .with_current_project() +/// .export_heap_base() +/// .import_memory() +/// .build() +/// } +/// ``` +/// +/// Update mock runtime to be build into wasm code. +/// ```.ignore +/// #![cfg_attr(not(feature = "std"), no_std)] +/// +/// #[cfg(feature = "std")] +/// include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); +/// +/// #[cfg(feature = "std")] +/// pub fn wasm_binary_unwrap() -> &'static [u8] { WASM_BINARY.unwrap() } +/// .. +/// ``` +/// +/// Create a file `bench_runner.rs` with following code: +/// ```.ignore +/// orml_bencher::run_benches!(my_module::benches); +/// ``` +/// +/// Update Cargo.toml by adding: +/// ```toml +/// .. +/// [package] +/// name = "my-module" +/// .. +/// build = 'build.rs' +/// +/// [build-dependencies] +/// substrate-wasm-builder = '4.0.0' +/// +/// [[bench]] +/// name = 'benches' +/// harness = false +/// path = 'bench_runner.rs' +/// required-features = ['bench'] +/// +/// [features] +/// bench = [] +/// .. +/// ``` +/// +/// Run bench with features bench: `cargo bench --features=bench` +#[cfg(feature = "std")] +#[macro_export] +macro_rules! run_benches { + ($benches:path) => { + use $benches::{wasm_binary_unwrap, Block}; + pub fn main() { + let output = $crate::bench_runner::run::(wasm_binary_unwrap().to_vec()); + $crate::handler::handle(output); + } + }; +} + +/// Define benches +/// +/// Create a file `src/benches.rs`: +/// ```.ignore +/// #![cfg_attr(not(feature = "std"), no_std)] +/// #![allow(dead_code)] +/// +/// #[cfg(feature = "std")] // Re-export for bench_runner +/// pub use crate::mock::{Block, wasm_binary_unwrap}; +/// +/// use crate::mock::YourModule; +/// +/// fn foo(b: &mut Bencher) { +/// b.bench("foo", || { +/// YourModule::foo(); +/// }); +/// } +/// +/// fn bar(b: &mut Bencher) { +/// b.bench("bar", || { +/// YourModule::bar(); +/// }); +/// } +/// +/// orml_bencher::bench!(foo, bar); +/// ``` +/// Update `src/lib.rs`: +/// ```.ignore +/// #[cfg(any(feature = "bench", test))] +/// pub mod mock; /* mock runtime needs to be compiled into wasm */ +/// #[cfg(feature = "bench")] +/// pub mod benches; +/// ``` +#[macro_export] +macro_rules! bench { + ( + $($method:path),+ + ) => { + use $crate::BenchResult; + use $crate::sp_std::{cmp::max, prelude::Vec}; + use $crate::frame_benchmarking::{benchmarking, BenchmarkResults}; + + #[derive(Default, Clone, PartialEq, Debug)] + struct Bencher { + pub results: Vec, + } + + impl Bencher { + pub fn bench ()>(&mut self, name: &str, block: F) { + // Warm up the DB + benchmarking::commit_db(); + benchmarking::wipe_db(); + + let mut result = BenchResult { + method: name.as_bytes().to_vec(), + ..Default::default() + }; + + for _ in 0..50 { + benchmarking::commit_db(); + benchmarking::reset_read_write_count(); + + let start_time = benchmarking::current_time(); + block(); + let end_time = benchmarking::current_time(); + let elasped = end_time - start_time; + result.elapses.push(elasped); + + benchmarking::commit_db(); + let (reads, repeat_reads, writes, repeat_writes) = benchmarking::read_write_count(); + + result.reads = max(result.reads, reads); + result.repeat_reads = max(result.repeat_reads, repeat_reads); + result.writes = max(result.writes, writes); + result.repeat_writes = max(result.repeat_writes, repeat_writes); + + benchmarking::wipe_db(); + } + self.results.push(result); + } + } + + $crate::sp_core::wasm_export_functions! { + fn run_benches() -> Vec { + let mut bencher = Bencher::default(); + $( + $method(&mut bencher); + )+ + bencher.results + } + } + } +} diff --git a/weight-gen/Cargo.toml b/weight-gen/Cargo.toml new file mode 100644 index 000000000..e82e7bf36 --- /dev/null +++ b/weight-gen/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "weight-gen" +description = "CLI for generating weight from bencher output" +license = "Apache-2.0" +version = "0.4.1-dev" +authors = ["Laminar Developers "] +edition = "2018" + +[dependencies] +serde = { version = "1.0.119", features = ['derive'] } +serde_json = "1.0" +clap = "3.0.0-beta.2" +handlebars = { version = "3.5.2" } + +[features] +default = ["std"] +std = [] diff --git a/weight-gen/src/main.rs b/weight-gen/src/main.rs new file mode 100644 index 000000000..1b9cff753 --- /dev/null +++ b/weight-gen/src/main.rs @@ -0,0 +1,174 @@ +use clap::{AppSettings, Clap}; +use serde::{Deserialize, Serialize}; +use std::io::Read; + +#[derive(Clap)] +#[clap(version = "01.0", author = "Laminar Developers ")] +#[clap(setting = AppSettings::ColoredHelp)] +struct Opts { + input: Option, + #[clap(short, long)] + template: Option, + #[clap(short, long)] + header: Option, + #[clap(short, long)] + out: Option, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone)] +pub struct BenchData { + pub name: String, + pub base_weight: u64, + pub base_reads: u32, + pub base_writes: u32, +} + +#[derive(Serialize, Default, Debug, Clone)] +struct TemplateData { + pub header: String, + pub benchmarks: Vec, +} + +// A Handlebars helper to add an underscore after every 3rd character, +// i.e. a separator for large numbers. +#[derive(Clone, Copy)] +struct UnderscoreHelper; +impl handlebars::HelperDef for UnderscoreHelper { + fn call<'reg: 'rc, 'rc>( + &self, + h: &handlebars::Helper, + _: &handlebars::Handlebars, + _: &handlebars::Context, + _rc: &mut handlebars::RenderContext, + out: &mut dyn handlebars::Output, + ) -> handlebars::HelperResult { + use handlebars::JsonRender; + let param = h.param(0).expect("Unable to retrieve param from handlebars helper"); + let underscore_param = underscore(param.value().render()); + out.write(&underscore_param)?; + Ok(()) + } +} + +// Add an underscore after every 3rd character, i.e. a separator for large +// numbers. +fn underscore(i: Number) -> String +where + Number: std::string::ToString, +{ + let mut s = String::new(); + let i_str = i.to_string(); + let a = i_str.chars().rev().enumerate(); + for (idx, val) in a { + if idx != 0 && idx % 3 == 0 { + s.insert(0, '_'); + } + s.insert(0, val); + } + s +} + +// A helper to join a string of vectors. +#[derive(Clone, Copy)] +struct JoinHelper; +impl handlebars::HelperDef for JoinHelper { + fn call<'reg: 'rc, 'rc>( + &self, + h: &handlebars::Helper, + _: &handlebars::Handlebars, + _: &handlebars::Context, + _rc: &mut handlebars::RenderContext, + out: &mut dyn handlebars::Output, + ) -> handlebars::HelperResult { + use handlebars::JsonRender; + let param = h.param(0).expect("Unable to retrieve param from handlebars helper"); + let value = param.value(); + let joined = if value.is_array() { + value + .as_array() + .unwrap() + .iter() + .map(|v| v.render()) + .collect::>() + .join(" ") + } else { + value.render() + }; + out.write(&joined)?; + Ok(()) + } +} + +fn parse_stdio() -> Option> { + let mut buffer = String::new(); + let stdin = std::io::stdin(); + let mut handle = stdin.lock(); + + handle.read_to_string(&mut buffer).expect("Unable to read from stdin"); + + let lines: Vec<&str> = buffer.split('\n').collect(); + for line in lines { + let json = serde_json::from_str(line); + + if let Ok(data) = json { + return Some(data); + } + } + + None +} + +fn main() { + let opts: Opts = Opts::parse(); + + let benchmarks: Vec = { + if let Some(data) = opts.input { + serde_json::from_str(&data).expect("Could not parse JSON data") + } else { + parse_stdio().expect("Could not parse JSON data") + } + }; + + let mut handlebars = handlebars::Handlebars::new(); + handlebars.register_helper("underscore", Box::new(UnderscoreHelper)); + handlebars.register_helper("join", Box::new(JoinHelper)); + // Don't HTML escape any characters. + handlebars.register_escape_fn(|s| -> String { s.to_string() }); + + // Use empty header if a header path is not given. + let header = { + if let Some(path) = opts.header { + ::std::fs::read_to_string(&path).expect("Header file not found") + } else { + String::from("") + } + }; + + let hbs_data = TemplateData { header, benchmarks }; + + const DEFAULT_TEMPLATE: &str = include_str!("./template.hbs"); + + // Use default template if template path is not given. + let template = { + if let Some(path) = opts.template { + ::std::fs::read_to_string(&path).expect("Template file not found") + } else { + String::from(DEFAULT_TEMPLATE) + } + }; + + // Write benchmark to file or print to terminal if output path is not given. + if let Some(path) = opts.out { + let mut output_file = ::std::fs::File::create(&path).expect("Could not create output file"); + + handlebars + .render_template_to_write(&template, &hbs_data, &mut output_file) + .expect("Unable to render template"); + } else { + let template_string = handlebars + .render_template(&template, &hbs_data) + .expect("Unable to render template"); + + println!("{}", template_string); + } +} diff --git a/weight-gen/src/template.hbs b/weight-gen/src/template.hbs new file mode 100644 index 000000000..470b00c25 --- /dev/null +++ b/weight-gen/src/template.hbs @@ -0,0 +1,23 @@ +{{header}} + +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(dead_code)] + +use frame_support::{traits::Get, weights::Weight}; +use sp_std::marker::PhantomData; + +pub struct ModuleWeights(PhantomData); +impl ModuleWeights { + {{~#each benchmarks as |benchmark|}} + pub fn {{benchmark.name~}} () -> Weight { + ({{underscore benchmark.base_weight}} as Weight) + {{~#if (ne benchmark.base_reads "0")}} + .saturating_add(T::DbWeight::get().reads({{benchmark.base_reads}} as Weight)) + {{~/if}} + {{~#if (ne benchmark.base_writes "0")}} + .saturating_add(T::DbWeight::get().writes({{benchmark.base_writes}} as Weight)) + {{~/if}} + } + {{~/each}} +} diff --git a/weight-meter/Cargo.toml b/weight-meter/Cargo.toml new file mode 100644 index 000000000..7ae22ae33 --- /dev/null +++ b/weight-meter/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "orml-weight-meter" +version = "0.4.1-dev" +license = "Apache-2.0" +authors = ["Laminar Developers "] +edition = "2018" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +spin = "0.7.1" +frame-support = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1", default-features = false } +weight-meter-procedural = { path = "weight-meter-procedural", default-features = false } + +[dev-dependencies] +serde = { version = "1.0.124" } +codec = { package = "parity-scale-codec", version = "2.0.0" } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1" } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1" } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1" } + +frame-support = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1"} +frame-system = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1" } +pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1" } + +[features] +default = ["std"] +std = [ + "frame-support/std", + "weight-meter-procedural/std", +] diff --git a/weight-meter/README.md b/weight-meter/README.md new file mode 100644 index 000000000..26f821e28 --- /dev/null +++ b/weight-meter/README.md @@ -0,0 +1,13 @@ +# Weight Meter + +Include `WeightMeter` into your module Cargo.toml +``` +[dependencies] +orml-weight-meter = { version = "..", default-features = false } + +std = [ + .. + 'orml-weight-meter/std', +] + +``` \ No newline at end of file diff --git a/weight-meter/src/lib.rs b/weight-meter/src/lib.rs new file mode 100644 index 000000000..cfdc45d12 --- /dev/null +++ b/weight-meter/src/lib.rs @@ -0,0 +1,80 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +//! 1. Add macro attribute on method you want to benchmark. +//! ```ignore +//! #[orml_weight_meter::weight(0)] +//! fn inner_do_something(something: u32) { +//! // Update storage. +//! Something::::put(something); +//! } +//! ``` +//! Start with `0` and after the weights is generated then it can be replaced +//! with generated weight. Macro will inject callable methods that wraps inner +//! methods. Generated call will start with prefix `method_` followed by method +//! name. This only works for methods with `orml_weight_meter::weight` attribute +//! and only when running benchmarks. +//! +//! 2. Create benchmarks using orml_bencher and generate the weights with +//! orml_weight_gen +//! After running the benchmarks and the weights have been generated then we can +//! replace +//! ```ignore +//! #[orml_weight_meter::weight(0)] +//! ``` +//! with +//!```ignore +//! #[orml_weight_meter::weight(T::WeightInfo::method_inner_do_something())] +//! ``` +//! +//! 3. Use WeightMeter on your calls by adding macro +//! `#[orml_weight_meter::start]` and at the end use +//! `orml_weight_meter::used_weight()` to get used weight. +//!```ignore +//! #[pallet::call] +//! impl Pallet { +//! #[pallet::weight(T::WeightInfo::do_something())] +//! #[orml_weight_meter::start] +//! pub fn do_something(origin: OriginFor, something: u32) -> +//! DispatchResultWithPostInfo { +//! let who = ensure_signed(origin)?; +//! Self::inner_do_something(something); +//! // Emit an event. +//! Self::deposit_event(Event::SomethingStored(something, who)); +//! Ok(PostDispatchInfo::from(Some(orml_weight_meter::used_weight()))) +//! } +//! } +//! ``` + +use frame_support::weights::Weight; + +struct Meter { + used_weight: Weight, + // Depth gets incremented when entering call or a sub-call + // This is used to avoid miscalculation during sub-calls + depth: u8, +} + +mod meter_no_std; +mod meter_std; + +// For use in mock file +#[cfg(test)] +extern crate self as orml_weight_meter; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +#[cfg(feature = "std")] +pub use meter_std::*; + +#[cfg(not(feature = "std"))] +pub use meter_no_std::*; + +/// Start weight meter +pub use weight_meter_procedural::start; + +/// Measure each methods weight +pub use weight_meter_procedural::weight; diff --git a/weight-meter/src/meter_no_std.rs b/weight-meter/src/meter_no_std.rs new file mode 100644 index 000000000..32b129363 --- /dev/null +++ b/weight-meter/src/meter_no_std.rs @@ -0,0 +1,40 @@ +// TODO: research if there's a better way +#![cfg(not(feature = "std"))] + +use super::{Meter, Weight}; + +static mut METER: Meter = Meter { + used_weight: 0, + depth: 0, +}; + +pub fn start() { + unsafe { + if METER.depth == 0 { + METER.used_weight = 0; + } + METER.depth = METER.depth.saturating_add(1); + } +} + +pub fn using(weight: Weight) { + unsafe { + METER.used_weight = METER.used_weight.saturating_add(weight); + } +} + +pub fn finish() { + unsafe { + METER.depth.checked_sub(1).map_or_else( + || { + debug_assert!(false); + 0 + }, + |v| v, + ); + } +} + +pub fn used_weight() -> Weight { + unsafe { METER.used_weight } +} diff --git a/weight-meter/src/meter_std.rs b/weight-meter/src/meter_std.rs new file mode 100644 index 000000000..1e6c4687e --- /dev/null +++ b/weight-meter/src/meter_std.rs @@ -0,0 +1,44 @@ +// TODO: research if there's a better way +#![cfg(feature = "std")] + +use super::{Meter, Weight}; +use std::cell::RefCell; + +thread_local! { + static METER: RefCell = RefCell::new(Meter { + used_weight: 0, + depth: 0, + }); +} + +/// Start weight meter with base weight +pub fn start() { + METER.with(|v| { + let mut meter = v.borrow_mut(); + if meter.depth == 0 { + meter.used_weight = 0; + } + meter.depth = meter.depth.saturating_add(1); + }); +} + +/// Increment used weight +pub fn using(weight: Weight) { + METER.with(|v| { + let mut meter = v.borrow_mut(); + meter.used_weight = meter.used_weight.saturating_add(weight); + }) +} + +/// Finish weight meter +pub fn finish() { + METER.with(|v| { + let mut meter = v.borrow_mut(); + meter.depth = meter.depth.saturating_sub(1); + }) +} + +/// Get used weight +pub fn used_weight() -> Weight { + METER.with(|v| v.borrow().used_weight) +} diff --git a/weight-meter/src/mock.rs b/weight-meter/src/mock.rs new file mode 100644 index 000000000..ecc10e796 --- /dev/null +++ b/weight-meter/src/mock.rs @@ -0,0 +1,229 @@ +#[frame_support::pallet] +pub mod test_module { + use frame_support::{dispatch::DispatchResultWithPostInfo, pallet_prelude::*, weights::Weight}; + use frame_system::pallet_prelude::*; + + #[pallet::config] + pub trait Config: frame_system::Config {} + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(PhantomData); + + #[pallet::hooks] + impl Hooks for Pallet {} + + #[pallet::storage] + #[pallet::getter(fn something)] + pub type Something = StorageValue<_, u32>; + + #[pallet::call] + impl Pallet { + #[pallet::weight(50_000)] + #[orml_weight_meter::start] + pub fn expect_100(origin: OriginFor) -> DispatchResultWithPostInfo { + ensure_signed(origin)?; + + Self::put_100(); + + Ok(Some(orml_weight_meter::used_weight()).into()) + } + + #[pallet::weight(50_000)] + #[orml_weight_meter::start] + pub fn expect_500(origin: OriginFor) -> DispatchResultWithPostInfo { + ensure_signed(origin)?; + + Self::put_100(); + Self::put_100(); + Self::put_100(); + Self::put_100(); + Self::put_100(); + + Ok(Some(orml_weight_meter::used_weight()).into()) + } + + #[pallet::weight(50_000)] + #[orml_weight_meter::start] + pub fn expect_max_weight(origin: OriginFor) -> DispatchResultWithPostInfo { + ensure_signed(origin)?; + + Self::max_weight(); + Self::put_100(); + + Ok(Some(orml_weight_meter::used_weight()).into()) + } + + #[pallet::weight(50_000)] + #[orml_weight_meter::start] + pub fn expect_100_or_200(origin: OriginFor, branch: bool) -> DispatchResultWithPostInfo { + ensure_signed(origin)?; + + if branch { + Self::put_200(); + } else { + Self::put_100(); + } + + Ok(Some(orml_weight_meter::used_weight()).into()) + } + + #[pallet::weight(50_000)] + #[orml_weight_meter::start] + pub fn nested_inner_methods(origin: OriginFor) -> DispatchResultWithPostInfo { + ensure_signed(origin)?; + + Self::put_300_nested(); + + Ok(Some(orml_weight_meter::used_weight()).into()) + } + + #[pallet::weight(50_000)] + #[orml_weight_meter::start] + pub fn nested_extrinsic(origin: OriginFor) -> DispatchResultWithPostInfo { + ensure_signed(origin.clone())?; + + // some module call + Self::put_300_nested(); + + // call extrinsic method + Self::expect_100(origin)?; + + // some module call + Self::put_300_nested(); + + Ok(Some(orml_weight_meter::used_weight()).into()) + } + } + + impl Pallet { + #[orml_weight_meter::weight(100)] + fn put_100() { + let something = Self::something(); + + if let Some(v) = something { + Something::::put(v.checked_add(100).unwrap()); + } else { + Something::::put(100); + } + } + + #[orml_weight_meter::weight(200)] + fn put_200() { + let something = Self::something(); + + if let Some(v) = something { + Something::::put(v.checked_add(200).unwrap()); + } else { + Something::::put(100); + } + } + + #[orml_weight_meter::weight(200)] + fn put_300_nested() { + Self::put_100(); + } + + #[orml_weight_meter::weight(Weight::MAX)] + fn max_weight() { + return; + } + } +} + +use frame_support::sp_runtime::traits::IdentityLookup; +use sp_runtime::testing::{Header, H256}; + +pub type BlockNumber = u64; + +frame_support::parameter_types! { + pub const BlockHashCount: u64 = 250; +} + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; +type Balance = u128; + +impl frame_system::Config for Runtime { + type Origin = Origin; + type Index = u64; + type BlockNumber = BlockNumber; + type Call = Call; + type Hash = H256; + type Hashing = ::sp_runtime::traits::BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type BlockWeights = (); + type BlockLength = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type DbWeight = (); + type BaseCallFilter = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); +} + +frame_support::parameter_types! { + pub const ExistentialDeposit: u64 = 1; +} + +impl pallet_balances::Config for Runtime { + type Balance = Balance; + type Event = Event; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = frame_system::Pallet; + type MaxLocks = (); + type WeightInfo = (); +} + +impl test_module::Config for Runtime {} + +frame_support::construct_runtime!( + pub enum Runtime where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Storage, Config, Event}, + TestModule: test_module::{Pallet, Call, Storage}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + } +); + +pub struct ExtBuilder(); + +impl Default for ExtBuilder { + fn default() -> Self { + Self() + } +} + +impl ExtBuilder { + pub fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + pallet_balances::GenesisConfig:: { + balances: vec![(100, 100_000)], + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + ExtBuilder::default().build() +} diff --git a/weight-meter/src/tests.rs b/weight-meter/src/tests.rs new file mode 100644 index 000000000..8d4c1bae3 --- /dev/null +++ b/weight-meter/src/tests.rs @@ -0,0 +1,60 @@ +use crate::mock::*; +use frame_support::weights::PostDispatchInfo; + +#[test] +fn used_weight_works() { + new_test_ext().execute_with(|| { + let result: PostDispatchInfo = TestModule::expect_100(Origin::signed(100)).unwrap(); + // Check used weight is correct + assert_eq!(Some(100), result.actual_weight); + // Check that the method ran correctly + assert_eq!(Some(100), TestModule::something()); + + let result: PostDispatchInfo = TestModule::expect_500(Origin::signed(100)).unwrap(); + assert_eq!(Some(500), result.actual_weight); + assert_eq!(Some(600), TestModule::something()); + }); +} + +#[test] +fn used_weight_branch_works() { + new_test_ext().execute_with(|| { + let result: PostDispatchInfo = TestModule::expect_100_or_200(Origin::signed(100), false).unwrap(); + // Check used weight is correct + assert_eq!(Some(100), result.actual_weight); + // Check that the method ran correctly + assert_eq!(Some(100), TestModule::something()); + + let result: PostDispatchInfo = TestModule::expect_100_or_200(Origin::signed(100), true).unwrap(); + // Check used weight is correct + assert_eq!(Some(200), result.actual_weight); + // Check that the method ran correctly + assert_eq!(Some(300), TestModule::something()); + }); +} + +#[test] +fn used_weight_nested_calls_works() { + new_test_ext().execute_with(|| { + let result: PostDispatchInfo = TestModule::nested_inner_methods(Origin::signed(100)).unwrap(); + // Check used weight is correct + assert_eq!(Some(300), result.actual_weight); + }); +} + +#[test] +fn exceed_max_weight_works() { + new_test_ext().execute_with(|| { + let result: PostDispatchInfo = TestModule::expect_max_weight(Origin::signed(100)).unwrap(); + // Check used weight is correct + assert_eq!(Some(u64::MAX), result.actual_weight); + }); +} + +#[test] +fn nested_module_calls_works() { + new_test_ext().execute_with(|| { + let result = TestModule::nested_extrinsic(Origin::signed(0)).unwrap(); + assert_eq!(result.actual_weight, Some(700)); + }); +} diff --git a/weight-meter/weight-meter-procedural/Cargo.toml b/weight-meter/weight-meter-procedural/Cargo.toml new file mode 100644 index 000000000..05554eea1 --- /dev/null +++ b/weight-meter/weight-meter-procedural/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "weight-meter-procedural" +version = "0.1.0" +license = "Apache-2.0" +authors = ["Laminar Developers "] +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.6" +quote = "1.0.3" +syn = { version = "1.0.58", features = ["full"] } + +[features] +default = ["std"] +std = [] \ No newline at end of file diff --git a/weight-meter/weight-meter-procedural/src/lib.rs b/weight-meter/weight-meter-procedural/src/lib.rs new file mode 100644 index 000000000..87f5f753c --- /dev/null +++ b/weight-meter/weight-meter-procedural/src/lib.rs @@ -0,0 +1,32 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::ItemFn; + +#[proc_macro_attribute] +pub fn start(_attr: TokenStream, item: TokenStream) -> TokenStream { + let ItemFn { attrs, vis, sig, block } = syn::parse(item).unwrap(); + (quote! { + #(#attrs)* + #vis #sig { + ::orml_weight_meter::start(); + let result = #block; + ::orml_weight_meter::finish(); + result + } + }) + .into() +} + +#[proc_macro_attribute] +pub fn weight(attr: TokenStream, item: TokenStream) -> TokenStream { + let weight: syn::Expr = syn::parse(attr).unwrap(); + let ItemFn { attrs, vis, sig, block } = syn::parse(item).unwrap(); + (quote! { + #(#attrs)* + #vis #sig { + ::orml_weight_meter::using(#weight); + #block + } + }) + .into() +}