From 13413b7f417e3b87b2801dd068c72e95b4e966e7 Mon Sep 17 00:00:00 2001 From: Ermal Kaleci <ermalkaleci@gmail.com> Date: Tue, 8 Jun 2021 00:16:28 +0200 Subject: [PATCH] Bencher update (#507) * update weight template * ignore resources consumed by benched submethods * * refactor bencher / no need for build script * add wasm_builder for building benches into wasm * colorized output * update docs * remove cfg(test) * clippy * clippy * wasm handle panic * generate bench tests * update docs * fix features * update the way benches are defined * fix reset reduntant meter * fix repeat-reads/writes * cleanup --- bencher/Cargo.toml | 22 + bencher/src/bench_runner.rs | 28 +- bencher/src/build_wasm/mod.rs | 64 ++ bencher/src/build_wasm/prerequisites.rs | 301 ++++++++ bencher/src/build_wasm/wasm_project.rs | 709 ++++++++++++++++++ bencher/src/colorize.rs | 39 + bencher/src/handler.rs | 30 +- bencher/src/lib.rs | 176 ++++- bencher/src/macros.rs | 182 ++--- bencher/src/redundant_meter.rs | 111 +++ weight-gen/src/template.hbs | 4 +- weight-meter/Cargo.toml | 6 + weight-meter/src/lib.rs | 2 - .../weight-meter-procedural/Cargo.toml | 3 +- .../weight-meter-procedural/src/lib.rs | 17 + 15 files changed, 1554 insertions(+), 140 deletions(-) create mode 100644 bencher/src/build_wasm/mod.rs create mode 100644 bencher/src/build_wasm/prerequisites.rs create mode 100644 bencher/src/build_wasm/wasm_project.rs create mode 100644 bencher/src/colorize.rs create mode 100644 bencher/src/redundant_meter.rs diff --git a/bencher/Cargo.toml b/bencher/Cargo.toml index d64711b..0f95818 100644 --- a/bencher/Cargo.toml +++ b/bencher/Cargo.toml @@ -8,6 +8,15 @@ authors = ["Laminar Developers <hello@laminar.one>"] edition = "2018" [dependencies] +paste = "1.0" +build-helper = { version = "0.1.1", optional = true } +cargo_metadata = { version = "0.13.1", optional = true } +tempfile = { version = "3.1.0", optional = true } +toml = { version = "0.5.4", optional = true } +walkdir = { version = "2.3.1", optional = true } +ansi_term = { version = "0.12.1", optional = true } +wasm-gc-api = { version = "0.1.11", optional = true } +rand = {version = "0.8.3", optional = true } linregress = { version = "0.4.0", optional = true } serde = { version = "1.0.119", optional = true, features = ['derive'] } serde_json = {version = "1.0.64", optional = true } @@ -20,11 +29,20 @@ sp-state-machine = { git = "https://github.com/paritytech/substrate", branch = " sc-executor = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.4", default-features = false, features = ["wasmtime"], optional = true } sc-executor-common = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.4", optional = true } sc-client-db = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.4", default-features = false, features = ["with-kvdb-rocksdb"], optional = true } +sp-maybe-compressed-blob = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.4", default-features = false, optional = true } frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.4", default-features = false } [features] default = ["std"] std = [ + "build-helper", + "cargo_metadata", + "tempfile", + "toml", + "walkdir", + "ansi_term", + "wasm-gc-api", + "rand", "linregress", "serde/std", "serde_json/std", @@ -37,5 +55,9 @@ std = [ "sc-executor/std", "sc-executor-common", "sc-client-db", + "sp-maybe-compressed-blob", "frame-benchmarking/std", ] +bench = [ + "sp-io/disable_panic_handler" +] diff --git a/bencher/src/bench_runner.rs b/bencher/src/bench_runner.rs index 1dc7fff..a667100 100644 --- a/bencher/src/bench_runner.rs +++ b/bencher/src/bench_runner.rs @@ -1,22 +1,18 @@ -use frame_benchmarking::{ - benchmarking, - frame_support::sp_runtime::traits::{Block, NumberFor}, -}; -use sc_client_db::BenchmarkingState; +use frame_benchmarking::frame_support::sp_runtime::traits::{Block, NumberFor}; use sc_executor::{sp_wasm_interface::HostFunctions, WasmExecutionMethod, WasmExecutor}; use sc_executor_common::runtime_blob::RuntimeBlob; -use sp_io::SubstrateHostFunctions; use sp_state_machine::{Ext, OverlayedChanges, StorageTransactionCache}; /// Run benches -pub fn run<B: Block>(wasm_code: Vec<u8>) -> Vec<u8> { +pub fn run<B: Block>(wasm_code: Vec<u8>) -> std::result::Result<Vec<u8>, String> { let mut overlay = OverlayedChanges::default(); let mut cache = StorageTransactionCache::default(); - let state = BenchmarkingState::<B>::new(Default::default(), Default::default(), false).unwrap(); + let state = sc_client_db::BenchmarkingState::<B>::new(Default::default(), Default::default(), false).unwrap(); let mut ext = Ext::<_, NumberFor<B>, _>::new(&mut overlay, &mut cache, &state, None, None); - let mut host_functions = benchmarking::HostFunctions::host_functions(); - host_functions.append(&mut SubstrateHostFunctions::host_functions()); + let mut host_functions = sp_io::SubstrateHostFunctions::host_functions(); + host_functions.append(&mut frame_benchmarking::benchmarking::HostFunctions::host_functions()); + host_functions.append(&mut super::bencher::HostFunctions::host_functions()); let executor = WasmExecutor::new( WasmExecutionMethod::Compiled, @@ -26,13 +22,7 @@ pub fn run<B: Block>(wasm_code: Vec<u8>) -> Vec<u8> { None, ); - executor - .uncached_call( - RuntimeBlob::uncompress_if_needed(&wasm_code[..]).unwrap(), - &mut ext, - true, - "run_benches", - &[], - ) - .unwrap() + let blob = RuntimeBlob::uncompress_if_needed(&wasm_code[..]).unwrap(); + + executor.uncached_call(blob, &mut ext, true, "run_benches", &[]) } diff --git a/bencher/src/build_wasm/mod.rs b/bencher/src/build_wasm/mod.rs new file mode 100644 index 0000000..eca9348 --- /dev/null +++ b/bencher/src/build_wasm/mod.rs @@ -0,0 +1,64 @@ +use rand::{distributions::Alphanumeric, thread_rng, Rng}; + +pub mod prerequisites; +pub mod wasm_project; + +/// Environment variable to disable color output of the wasm build. +const WASM_BUILD_NO_COLOR: &str = "WASM_BUILD_NO_COLOR"; + +/// Returns `true` when color output is enabled. +pub fn color_output_enabled() -> bool { + std::env::var(WASM_BUILD_NO_COLOR).is_err() +} + +pub fn build() -> std::io::Result<Vec<u8>> { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let pkg_name = std::env::var("CARGO_PKG_NAME").unwrap(); + + let random = thread_rng() + .sample_iter(&Alphanumeric) + .take(16) + .map(char::from) + .collect::<String>(); + + let mut out_dir = std::path::PathBuf::from(manifest_dir); + out_dir.push(format!("target/release/build/{}-{}/out", pkg_name, random)); + + std::env::set_var("OUT_DIR", out_dir.display().to_string()); + + let mut project_cargo_toml = std::env::current_dir()?; + project_cargo_toml.push("Cargo.toml"); + + let default_rustflags = "-Clink-arg=--export=__heap_base -C link-arg=--import-memory"; + let cargo_cmd = match prerequisites::check() { + Ok(cmd) => cmd, + Err(err_msg) => { + eprintln!("{}", err_msg); + std::process::exit(1); + } + }; + + let (wasm_binary, bloaty) = wasm_project::create_and_compile( + &project_cargo_toml, + &default_rustflags, + cargo_cmd, + vec!["bench".to_string()], + None, + ); + + let (wasm_binary, _wasm_binary_bloaty) = if let Some(wasm_binary) = wasm_binary { + ( + wasm_binary.wasm_binary_path_escaped(), + bloaty.wasm_binary_bloaty_path_escaped(), + ) + } else { + ( + bloaty.wasm_binary_bloaty_path_escaped(), + bloaty.wasm_binary_bloaty_path_escaped(), + ) + }; + + let bytes = std::fs::read(wasm_binary)?; + + Ok(bytes.to_vec()) +} diff --git a/bencher/src/build_wasm/prerequisites.rs b/bencher/src/build_wasm/prerequisites.rs new file mode 100644 index 0000000..fc09c4d --- /dev/null +++ b/bencher/src/build_wasm/prerequisites.rs @@ -0,0 +1,301 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::colorize::{color_output_enabled, red_bold, yellow_bold}; +use std::{ + env, fs, + io::BufRead, + path::{Path, PathBuf}, + process::Command, +}; + +/// Environment variable to set the toolchain used to compile the wasm binary. +pub const WASM_BUILD_TOOLCHAIN: &str = "WASM_BUILD_TOOLCHAIN"; + +/// Write to the given `file` if the `content` is different. +pub fn write_file_if_changed(file: impl AsRef<Path>, content: impl AsRef<str>) { + if fs::read_to_string(file.as_ref()).ok().as_deref() != Some(content.as_ref()) { + fs::write(file.as_ref(), content.as_ref()) + .unwrap_or_else(|_| panic!("Writing `{}` can not fail!", file.as_ref().display())); + } +} + +/// Copy `src` to `dst` if the `dst` does not exist or is different. +pub fn copy_file_if_changed(src: PathBuf, dst: PathBuf) { + let src_file = fs::read_to_string(&src).ok(); + let dst_file = fs::read_to_string(&dst).ok(); + + if src_file != dst_file { + fs::copy(&src, &dst) + .unwrap_or_else(|_| panic!("Copying `{}` to `{}` can not fail; qed", src.display(), dst.display())); + } +} + +/// Get a cargo command that compiles with nightly +fn get_nightly_cargo() -> CargoCommand { + let env_cargo = CargoCommand::new(&env::var("CARGO").expect("`CARGO` env variable is always set by cargo")); + let default_cargo = CargoCommand::new("cargo"); + let rustup_run_nightly = CargoCommand::new_with_args("rustup", &["run", "nightly", "cargo"]); + let wasm_toolchain = env::var(WASM_BUILD_TOOLCHAIN).ok(); + + // First check if the user requested a specific toolchain + if let Some(cmd) = wasm_toolchain.and_then(|t| get_rustup_nightly(Some(t))) { + cmd + } else if env_cargo.is_nightly() { + env_cargo + } else if default_cargo.is_nightly() { + default_cargo + } else if rustup_run_nightly.is_nightly() { + rustup_run_nightly + } else { + // If no command before provided us with a nightly compiler, we try to search + // one with rustup. If that fails as well, we return the default cargo and let + // the prequisities check fail. + get_rustup_nightly(None).unwrap_or(default_cargo) + } +} + +/// Get a nightly from rustup. If `selected` is `Some(_)`, a `CargoCommand` +/// using the given nightly is returned. +fn get_rustup_nightly(selected: Option<String>) -> Option<CargoCommand> { + let host = format!("-{}", env::var("HOST").expect("`HOST` is always set by cargo")); + + let version = match selected { + Some(selected) => selected, + None => { + let output = Command::new("rustup") + .args(&["toolchain", "list"]) + .output() + .ok()? + .stdout; + let lines = output.as_slice().lines(); + + let mut latest_nightly = None; + for line in lines.filter_map(|l| l.ok()) { + if line.starts_with("nightly-") && line.ends_with(&host) { + // Rustup prints them sorted + latest_nightly = Some(line.clone()); + } + } + + latest_nightly?.trim_end_matches(&host).into() + } + }; + + Some(CargoCommand::new_with_args("rustup", &["run", &version, "cargo"])) +} + +/// Wraps a specific command which represents a cargo invocation. +#[derive(Debug)] +pub struct CargoCommand { + program: String, + args: Vec<String>, +} + +impl CargoCommand { + fn new(program: &str) -> Self { + CargoCommand { + program: program.into(), + args: Vec::new(), + } + } + + fn new_with_args(program: &str, args: &[&str]) -> Self { + CargoCommand { + program: program.into(), + args: args.iter().map(ToString::to_string).collect(), + } + } + + pub fn command(&self) -> Command { + let mut cmd = Command::new(&self.program); + cmd.args(&self.args); + cmd + } + + /// Check if the supplied cargo command is a nightly version + fn is_nightly(&self) -> bool { + // `RUSTC_BOOTSTRAP` tells a stable compiler to behave like a nightly. So, when + // this env variable is set, we can assume that whatever rust compiler we have, + // it is a nightly compiler. For "more" information, see: + // https://github.com/rust-lang/rust/blob/fa0f7d0080d8e7e9eb20aa9cbf8013f96c81287f/src/libsyntax/feature_gate/check.rs#L891 + env::var("RUSTC_BOOTSTRAP").is_ok() + || self + .command() + .arg("--version") + .output() + .map_err(|_| ()) + .and_then(|o| String::from_utf8(o.stdout).map_err(|_| ())) + .unwrap_or_default() + .contains("-nightly") + } +} + +/// Wraps a [`CargoCommand`] and the version of `rustc` the cargo command uses. +pub struct CargoCommandVersioned { + command: CargoCommand, + version: String, +} + +impl CargoCommandVersioned { + fn new(command: CargoCommand, version: String) -> Self { + Self { command, version } + } + + /// Returns the `rustc` version. + pub fn rustc_version(&self) -> &str { + &self.version + } +} + +impl std::ops::Deref for CargoCommandVersioned { + type Target = CargoCommand; + + fn deref(&self) -> &CargoCommand { + &self.command + } +} + +use tempfile::tempdir; + +/// Checks that all prerequisites are installed. +/// +/// Returns the versioned cargo command on success. +pub fn check() -> Result<CargoCommandVersioned, String> { + let cargo_command = get_nightly_cargo(); + + if !cargo_command.is_nightly() { + return Err(red_bold("Rust nightly not installed, please install it!")); + } + + check_wasm_toolchain_installed(cargo_command) +} + +/// Create the project that will be used to check that the wasm toolchain is +/// installed and to extract the rustc version. +fn create_check_toolchain_project(project_dir: &Path) { + let lib_rs_file = project_dir.join("src/lib.rs"); + let main_rs_file = project_dir.join("src/main.rs"); + let build_rs_file = project_dir.join("build.rs"); + let manifest_path = project_dir.join("Cargo.toml"); + + write_file_if_changed( + &manifest_path, + r#" + [package] + name = "wasm-test" + version = "1.0.0" + edition = "2018" + build = "build.rs" + + [lib] + name = "wasm_test" + crate-type = ["cdylib"] + + [workspace] + "#, + ); + write_file_if_changed(lib_rs_file, "pub fn test() {}"); + + // We want to know the rustc version of the rustc that is being used by our + // cargo command. The cargo command is determined by some *very* complex + // algorithm to find the cargo command that supports nightly. + // The best solution would be if there is a `cargo rustc --version` command, + // which sadly doesn't exists. So, the only available way of getting the rustc + // version is to build a project and capture the rustc version in this build + // process. This `build.rs` is exactly doing this. It gets the rustc version by + // calling `rustc --version` and exposing it in the `RUSTC_VERSION` environment + // variable. + write_file_if_changed( + build_rs_file, + r#" + fn main() { + let rustc_cmd = std::env::var("RUSTC").ok().unwrap_or_else(|| "rustc".into()); + + let rustc_version = std::process::Command::new(rustc_cmd) + .arg("--version") + .output() + .ok() + .and_then(|o| String::from_utf8(o.stdout).ok()); + + println!( + "cargo:rustc-env=RUSTC_VERSION={}", + rustc_version.unwrap_or_else(|| "unknown rustc version".into()), + ); + } + "#, + ); + // Just prints the `RURSTC_VERSION` environment variable that is being created + // by the `build.rs` script. + write_file_if_changed( + main_rs_file, + r#" + fn main() { + println!("{}", env!("RUSTC_VERSION")); + } + "#, + ); +} + +fn check_wasm_toolchain_installed(cargo_command: CargoCommand) -> Result<CargoCommandVersioned, String> { + let temp = tempdir().expect("Creating temp dir does not fail; qed"); + fs::create_dir_all(temp.path().join("src")).expect("Creating src dir does not fail; qed"); + create_check_toolchain_project(temp.path()); + + let err_msg = red_bold("Rust WASM toolchain not installed, please install it!"); + let manifest_path = temp.path().join("Cargo.toml").display().to_string(); + + let mut build_cmd = cargo_command.command(); + build_cmd.args(&[ + "build", + "--target=wasm32-unknown-unknown", + "--manifest-path", + &manifest_path, + ]); + + if color_output_enabled() { + build_cmd.arg("--color=always"); + } + + let mut run_cmd = cargo_command.command(); + run_cmd.args(&["run", "--manifest-path", &manifest_path]); + + build_cmd.output().map_err(|_| err_msg.clone()).and_then(|s| { + if s.status.success() { + let version = run_cmd.output().ok().and_then(|o| String::from_utf8(o.stdout).ok()); + Ok(CargoCommandVersioned::new( + cargo_command, + version.unwrap_or_else(|| "unknown rustc version".into()), + )) + } else { + match String::from_utf8(s.stderr) { + Ok(ref err) if err.contains("linker `rust-lld` not found") => { + Err(red_bold("`rust-lld` not found, please install it!")) + } + Ok(ref err) => Err(format!( + "{}\n\n{}\n{}\n{}{}\n", + err_msg, + yellow_bold("Further error information:"), + yellow_bold(&"-".repeat(60)), + err, + yellow_bold(&"-".repeat(60)), + )), + Err(_) => Err(err_msg), + } + } + }) +} diff --git a/bencher/src/build_wasm/wasm_project.rs b/bencher/src/build_wasm/wasm_project.rs new file mode 100644 index 0000000..c924fb0 --- /dev/null +++ b/bencher/src/build_wasm/wasm_project.rs @@ -0,0 +1,709 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![allow(clippy::option_map_unit_fn)] + +use super::{ + color_output_enabled, + prerequisites::{copy_file_if_changed, write_file_if_changed, CargoCommandVersioned}, +}; + +use std::{ + borrow::ToOwned, + collections::HashSet, + env, fs, + hash::{Hash, Hasher}, + ops::Deref, + path::{Path, PathBuf}, + process, +}; + +use toml::value::Table; + +use build_helper::rerun_if_changed; + +use cargo_metadata::{Metadata, MetadataCommand}; + +/// Environment variable that tells us to skip building the wasm binary. +const SKIP_BUILD_ENV: &str = "SKIP_WASM_BUILD"; + +/// Environment variable to force a certain build type when building the wasm +/// binary. Expects "debug" or "release" as value. +/// +/// By default the WASM binary uses the same build type as the main cargo build. +const WASM_BUILD_TYPE_ENV: &str = "WASM_BUILD_TYPE"; + +/// Environment variable to extend the `RUSTFLAGS` variable given to the wasm +/// build. +const WASM_BUILD_RUSTFLAGS_ENV: &str = "WASM_BUILD_RUSTFLAGS"; + +/// Environment variable to set the target directory to copy the final wasm +/// binary. +/// +/// The directory needs to be an absolute path. +const WASM_TARGET_DIRECTORY: &str = "WASM_TARGET_DIRECTORY"; + +/// Colorize an info message. +/// +/// Returns the colorized message. +fn colorize_info_message(message: &str) -> String { + if color_output_enabled() { + ansi_term::Color::Yellow.bold().paint(message).to_string() + } else { + message.into() + } +} + +/// Holds the path to the bloaty WASM binary. +pub struct WasmBinaryBloaty(PathBuf); + +impl WasmBinaryBloaty { + /// Returns the escaped path to the bloaty wasm binary. + pub fn wasm_binary_bloaty_path_escaped(&self) -> String { + self.0.display().to_string().escape_default().to_string() + } +} + +/// Holds the path to the WASM binary. +pub struct WasmBinary(PathBuf); + +impl WasmBinary { + /// Returns the path to the wasm binary. + pub fn wasm_binary_path(&self) -> &Path { + &self.0 + } + + /// Returns the escaped path to the wasm binary. + pub fn wasm_binary_path_escaped(&self) -> String { + self.0.display().to_string().escape_default().to_string() + } +} + +fn crate_metadata(cargo_manifest: &Path) -> Metadata { + let mut cargo_lock = cargo_manifest.to_path_buf(); + cargo_lock.set_file_name("Cargo.lock"); + + let cargo_lock_existed = cargo_lock.exists(); + + let crate_metadata = MetadataCommand::new() + .manifest_path(cargo_manifest) + .exec() + .expect("`cargo metadata` can not fail on project `Cargo.toml`; qed"); + + // If the `Cargo.lock` didn't exist, we need to remove it after + // calling `cargo metadata`. This is required to ensure that we don't change + // the build directory outside of the `target` folder. Commands like + // `cargo publish` require this. + if !cargo_lock_existed { + let _ = fs::remove_file(&cargo_lock); + } + + crate_metadata +} + +/// Creates the WASM project, compiles the WASM binary and compacts the WASM +/// binary. +/// +/// # Returns +/// +/// The path to the compact WASM binary and the bloaty WASM binary. +pub fn create_and_compile( + project_cargo_toml: &Path, + default_rustflags: &str, + cargo_cmd: CargoCommandVersioned, + features_to_enable: Vec<String>, + wasm_binary_name: Option<String>, +) -> (Option<WasmBinary>, WasmBinaryBloaty) { + let wasm_workspace_root = get_wasm_workspace_root(); + let wasm_workspace = wasm_workspace_root.join("wbuild"); + + let crate_metadata = crate_metadata(project_cargo_toml); + + let project = create_project( + project_cargo_toml, + &wasm_workspace, + &crate_metadata, + crate_metadata.workspace_root.as_ref(), + features_to_enable, + ); + + build_project(&project, default_rustflags, cargo_cmd); + let (wasm_binary, wasm_binary_compressed, bloaty) = + compact_wasm_file(&project, project_cargo_toml, wasm_binary_name); + + wasm_binary + .as_ref() + .map(|wasm_binary| copy_wasm_to_target_directory(project_cargo_toml, wasm_binary)); + + wasm_binary_compressed + .as_ref() + .map(|wasm_binary_compressed| copy_wasm_to_target_directory(project_cargo_toml, wasm_binary_compressed)); + + generate_rerun_if_changed_instructions(project_cargo_toml, &project, &wasm_workspace); + + (wasm_binary_compressed.or(wasm_binary), bloaty) +} + +/// Find the `Cargo.lock` relative to the `OUT_DIR` environment variable. +/// +/// If the `Cargo.lock` cannot be found, we emit a warning and return `None`. +fn find_cargo_lock(cargo_manifest: &Path) -> Option<PathBuf> { + fn find_impl(mut path: PathBuf) -> Option<PathBuf> { + loop { + if path.join("Cargo.lock").exists() { + return Some(path.join("Cargo.lock")); + } + + if !path.pop() { + return None; + } + } + } + + if let Some(path) = find_impl(build_helper::out_dir()) { + return Some(path); + } + + if let Some(path) = find_impl(cargo_manifest.to_path_buf()) { + return Some(path); + } + + build_helper::warning!( + "Could not find `Cargo.lock` for `{}`, while searching from `{}`.", + cargo_manifest.display(), + build_helper::out_dir().display() + ); + + None +} + +/// Extract the crate name from the given `Cargo.toml`. +fn get_crate_name(cargo_manifest: &Path) -> String { + let cargo_toml: Table = + toml::from_str(&fs::read_to_string(cargo_manifest).expect("File exists as checked before; qed")) + .expect("Cargo manifest is a valid toml file; qed"); + + let package = cargo_toml + .get("package") + .and_then(|t| t.as_table()) + .expect("`package` key exists in valid `Cargo.toml`; qed"); + + package + .get("name") + .and_then(|p| p.as_str()) + .map(ToOwned::to_owned) + .expect("Package name exists; qed") +} + +/// Returns the name for the wasm binary. +fn get_wasm_binary_name(cargo_manifest: &Path) -> String { + get_crate_name(cargo_manifest).replace('-', "_") +} + +/// Returns the root path of the wasm workspace. +fn get_wasm_workspace_root() -> PathBuf { + let mut out_dir = build_helper::out_dir(); + + loop { + match out_dir.parent() { + Some(parent) if out_dir.ends_with("build") => return parent.to_path_buf(), + _ => { + if !out_dir.pop() { + break; + } + } + } + } + + panic!("Could not find target dir in: {}", build_helper::out_dir().display()) +} + +fn create_project_cargo_toml( + wasm_workspace: &Path, + workspace_root_path: &Path, + crate_name: &str, + crate_path: &Path, + wasm_binary: &str, + enabled_features: impl Iterator<Item = String>, +) { + let mut workspace_toml: Table = toml::from_str( + &fs::read_to_string(workspace_root_path.join("Cargo.toml")).expect("Workspace root `Cargo.toml` exists; qed"), + ) + .expect("Workspace root `Cargo.toml` is a valid toml file; qed"); + + let mut wasm_workspace_toml = Table::new(); + + // Add `profile` with release and dev + let mut release_profile = Table::new(); + release_profile.insert("panic".into(), "abort".into()); + release_profile.insert("lto".into(), true.into()); + + let mut dev_profile = Table::new(); + dev_profile.insert("panic".into(), "abort".into()); + + let mut profile = Table::new(); + profile.insert("release".into(), release_profile.into()); + profile.insert("dev".into(), dev_profile.into()); + + wasm_workspace_toml.insert("profile".into(), profile.into()); + + // Add patch section from the project root `Cargo.toml` + if let Some(mut patch) = workspace_toml.remove("patch").and_then(|p| p.try_into::<Table>().ok()) { + // Iterate over all patches and make the patch path absolute from the workspace + // root path. + patch + .iter_mut() + .filter_map(|p| { + p.1.as_table_mut() + .map(|t| t.iter_mut().filter_map(|t| t.1.as_table_mut())) + }) + .flatten() + .for_each(|p| { + p.iter_mut().filter(|(k, _)| k == &"path").for_each(|(_, v)| { + if let Some(path) = v.as_str().map(PathBuf::from) { + if path.is_relative() { + *v = workspace_root_path.join(path).display().to_string().into(); + } + } + }) + }); + + wasm_workspace_toml.insert("patch".into(), patch.into()); + } + + let mut package = Table::new(); + package.insert("name".into(), format!("{}-wasm", crate_name).into()); + package.insert("version".into(), "1.0.0".into()); + package.insert("edition".into(), "2018".into()); + package.insert("resolver".into(), "2".into()); + + wasm_workspace_toml.insert("package".into(), package.into()); + + let mut lib = Table::new(); + lib.insert("name".into(), wasm_binary.into()); + lib.insert("crate-type".into(), vec!["cdylib".to_string()].into()); + + wasm_workspace_toml.insert("lib".into(), lib.into()); + + let mut dependencies = Table::new(); + + let mut wasm_project = Table::new(); + wasm_project.insert("package".into(), crate_name.into()); + wasm_project.insert("path".into(), crate_path.display().to_string().into()); + wasm_project.insert("default-features".into(), false.into()); + wasm_project.insert("features".into(), enabled_features.collect::<Vec<_>>().into()); + + dependencies.insert("wasm-project".into(), wasm_project.into()); + + wasm_workspace_toml.insert("dependencies".into(), dependencies.into()); + + wasm_workspace_toml.insert("workspace".into(), Table::new().into()); + + write_file_if_changed( + wasm_workspace.join("Cargo.toml"), + toml::to_string_pretty(&wasm_workspace_toml).expect("Wasm workspace toml is valid; qed"), + ); +} + +/// Find a package by the given `manifest_path` in the metadata. +/// +/// Panics if the package could not be found. +fn find_package_by_manifest_path<'a>( + manifest_path: &Path, + crate_metadata: &'a cargo_metadata::Metadata, +) -> &'a cargo_metadata::Package { + crate_metadata + .packages + .iter() + .find(|p| p.manifest_path == manifest_path) + .expect("Wasm project exists in its own metadata; qed") +} + +/// Get a list of enabled features for the project. +fn project_enabled_features(cargo_manifest: &Path, crate_metadata: &cargo_metadata::Metadata) -> Vec<String> { + let package = find_package_by_manifest_path(cargo_manifest, crate_metadata); + + let mut enabled_features = package + .features + .keys() + .filter(|f| { + let mut feature_env = f.replace("-", "_"); + feature_env.make_ascii_uppercase(); + + // We don't want to enable the `std`/`default` feature for the wasm build and + // we need to check if the feature is enabled by checking the env variable. + *f != "std" + && *f != "default" + && env::var(format!("CARGO_FEATURE_{}", feature_env)) + .map(|v| v == "1") + .unwrap_or_default() + }) + .cloned() + .collect::<Vec<_>>(); + + enabled_features.sort(); + enabled_features +} + +/// Returns if the project has the `runtime-wasm` feature +fn has_runtime_wasm_feature_declared(cargo_manifest: &Path, crate_metadata: &cargo_metadata::Metadata) -> bool { + let package = find_package_by_manifest_path(cargo_manifest, crate_metadata); + + package.features.keys().any(|k| k == "runtime-wasm") +} + +/// Create the project used to build the wasm binary. +/// +/// # Returns +/// +/// The path to the created wasm project. +fn create_project( + project_cargo_toml: &Path, + wasm_workspace: &Path, + crate_metadata: &Metadata, + workspace_root_path: &Path, + features_to_enable: Vec<String>, +) -> PathBuf { + let crate_name = get_crate_name(project_cargo_toml); + let crate_path = project_cargo_toml.parent().expect("Parent path exists; qed"); + let wasm_binary = get_wasm_binary_name(project_cargo_toml); + let wasm_project_folder = wasm_workspace.join(&crate_name); + + fs::create_dir_all(wasm_project_folder.join("src")).expect("Wasm project dir create can not fail; qed"); + + let mut enabled_features = project_enabled_features(&project_cargo_toml, &crate_metadata); + + if has_runtime_wasm_feature_declared(project_cargo_toml, crate_metadata) { + enabled_features.push("runtime-wasm".into()); + } + + let mut enabled_features = enabled_features.into_iter().collect::<HashSet<_>>(); + enabled_features.extend(features_to_enable.into_iter()); + + create_project_cargo_toml( + &wasm_project_folder, + workspace_root_path, + &crate_name, + &crate_path, + &wasm_binary, + enabled_features.into_iter(), + ); + + write_file_if_changed( + wasm_project_folder.join("src/lib.rs"), + "#![no_std] pub use wasm_project::*;", + ); + + if let Some(crate_lock_file) = find_cargo_lock(project_cargo_toml) { + // Use the `Cargo.lock` of the main project. + copy_file_if_changed(crate_lock_file, wasm_project_folder.join("Cargo.lock")); + } + + wasm_project_folder +} + +/// Returns if the project should be built as a release. +fn is_release_build() -> bool { + if let Ok(var) = env::var(WASM_BUILD_TYPE_ENV) { + match var.as_str() { + "release" => true, + "debug" => false, + var => panic!( + "Unexpected value for `{}` env variable: {}\nOne of the following are expected: `debug` or `release`.", + WASM_BUILD_TYPE_ENV, var, + ), + } + } else { + true + } +} + +/// Build the project to create the WASM binary. +fn build_project(project: &Path, default_rustflags: &str, cargo_cmd: CargoCommandVersioned) { + let manifest_path = project.join("Cargo.toml"); + let mut build_cmd = cargo_cmd.command(); + + let rustflags = format!( + "-C link-arg=--export-table {} {}", + default_rustflags, + env::var(WASM_BUILD_RUSTFLAGS_ENV).unwrap_or_default(), + ); + + build_cmd + .args(&["rustc", "--target=wasm32-unknown-unknown"]) + .arg(format!("--manifest-path={}", manifest_path.display())) + .env("RUSTFLAGS", rustflags) + // Unset the `CARGO_TARGET_DIR` to prevent a cargo deadlock (cargo locks a target dir exclusive). + // The runner project is created in `CARGO_TARGET_DIR` and executing it will create a sub target + // directory inside of `CARGO_TARGET_DIR`. + .env_remove("CARGO_TARGET_DIR") + // We don't want to call ourselves recursively + .env(SKIP_BUILD_ENV, ""); + + if color_output_enabled() { + build_cmd.arg("--color=always"); + } + + if is_release_build() { + build_cmd.arg("--release"); + }; + + println!( + "{}", + colorize_info_message("Information that should be included in a bug report.") + ); + println!("{} {:?}", colorize_info_message("Executing build command:"), build_cmd); + println!( + "{} {}", + colorize_info_message("Using rustc version:"), + cargo_cmd.rustc_version() + ); + + match build_cmd.status().map(|s| s.success()) { + Ok(true) => {} + // Use `process.exit(1)` to have a clean error output. + _ => process::exit(1), + } +} + +/// Compact the WASM binary using `wasm-gc` and compress it using zstd. +fn compact_wasm_file( + project: &Path, + cargo_manifest: &Path, + wasm_binary_name: Option<String>, +) -> (Option<WasmBinary>, Option<WasmBinary>, WasmBinaryBloaty) { + let is_release_build = is_release_build(); + let target = if is_release_build { "release" } else { "debug" }; + let default_wasm_binary_name = get_wasm_binary_name(cargo_manifest); + let wasm_file = project + .join("target/wasm32-unknown-unknown") + .join(target) + .join(format!("{}.wasm", default_wasm_binary_name)); + + let wasm_compact_file = if is_release_build { + let wasm_compact_file = project.join(format!( + "{}.compact.wasm", + wasm_binary_name + .clone() + .unwrap_or_else(|| default_wasm_binary_name.clone()), + )); + wasm_gc::garbage_collect_file(&wasm_file, &wasm_compact_file) + .expect("Failed to compact generated WASM binary."); + Some(WasmBinary(wasm_compact_file)) + } else { + None + }; + + let wasm_compact_compressed_file = wasm_compact_file.as_ref().and_then(|compact_binary| { + let file_name = wasm_binary_name + .clone() + .unwrap_or_else(|| default_wasm_binary_name.clone()); + + let wasm_compact_compressed_file = project.join(format!("{}.compact.compressed.wasm", file_name,)); + + if compress_wasm(&compact_binary.0, &wasm_compact_compressed_file) { + Some(WasmBinary(wasm_compact_compressed_file)) + } else { + None + } + }); + + let bloaty_file_name = if let Some(name) = wasm_binary_name { + format!("{}.wasm", name) + } else { + format!("{}.wasm", default_wasm_binary_name) + }; + + let bloaty_file = project.join(bloaty_file_name); + fs::copy(wasm_file, &bloaty_file).expect("Copying the bloaty file to the project dir."); + + ( + wasm_compact_file, + wasm_compact_compressed_file, + WasmBinaryBloaty(bloaty_file), + ) +} + +fn compress_wasm(wasm_binary_path: &Path, compressed_binary_out_path: &Path) -> bool { + use sp_maybe_compressed_blob::CODE_BLOB_BOMB_LIMIT; + + let data = fs::read(wasm_binary_path).expect("Failed to read WASM binary"); + if let Some(compressed) = sp_maybe_compressed_blob::compress(&data, CODE_BLOB_BOMB_LIMIT) { + fs::write(compressed_binary_out_path, &compressed[..]).expect("Failed to write WASM binary"); + + true + } else { + println!( + "cargo:warning=Writing uncompressed wasm. Exceeded maximum size {}", + CODE_BLOB_BOMB_LIMIT, + ); + + false + } +} + +/// Custom wrapper for a [`cargo_metadata::Package`] to store it in +/// a `HashSet`. +#[derive(Debug)] +struct DeduplicatePackage<'a> { + package: &'a cargo_metadata::Package, + identifier: String, +} + +impl<'a> From<&'a cargo_metadata::Package> for DeduplicatePackage<'a> { + fn from(package: &'a cargo_metadata::Package) -> Self { + Self { + package, + identifier: format!("{}{}{:?}", package.name, package.version, package.source), + } + } +} + +impl<'a> Hash for DeduplicatePackage<'a> { + fn hash<H: Hasher>(&self, state: &mut H) { + self.identifier.hash(state); + } +} + +impl<'a> PartialEq for DeduplicatePackage<'a> { + fn eq(&self, other: &Self) -> bool { + self.identifier == other.identifier + } +} + +impl<'a> Eq for DeduplicatePackage<'a> {} + +impl<'a> Deref for DeduplicatePackage<'a> { + type Target = cargo_metadata::Package; + + fn deref(&self) -> &Self::Target { + self.package + } +} + +/// Generate the `rerun-if-changed` instructions for cargo to make sure that the +/// WASM binary is rebuilt when needed. +fn generate_rerun_if_changed_instructions(cargo_manifest: &Path, project_folder: &Path, wasm_workspace: &Path) { + // Rerun `build.rs` if the `Cargo.lock` changes + if let Some(cargo_lock) = find_cargo_lock(cargo_manifest) { + rerun_if_changed(cargo_lock); + } + + let metadata = MetadataCommand::new() + .manifest_path(project_folder.join("Cargo.toml")) + .exec() + .expect("`cargo metadata` can not fail!"); + + let package = metadata + .packages + .iter() + .find(|p| p.manifest_path == cargo_manifest) + .expect("The crate package is contained in its own metadata; qed"); + + // Start with the dependencies of the crate we want to compile for wasm. + let mut dependencies = package.dependencies.iter().collect::<Vec<_>>(); + + // Collect all packages by follow the dependencies of all packages we find. + let mut packages = HashSet::new(); + packages.insert(DeduplicatePackage::from(package)); + + while let Some(dependency) = dependencies.pop() { + let path_or_git_dep = dependency + .source + .as_ref() + .map(|s| s.starts_with("git+")) + .unwrap_or(true); + + let package = metadata + .packages + .iter() + .filter(|p| !p.manifest_path.starts_with(wasm_workspace)) + .find(|p| { + // Check that the name matches and that the version matches or this is + // a git or path dep. A git or path dependency can only occur once, so we don't + // need to check the version. + (path_or_git_dep || dependency.req.matches(&p.version)) && dependency.name == p.name + }); + + if let Some(package) = package { + if packages.insert(DeduplicatePackage::from(package)) { + dependencies.extend(package.dependencies.iter()); + } + } + } + + // Make sure that if any file/folder of a dependency change, we need to rerun + // the `build.rs` + packages.iter().for_each(package_rerun_if_changed); + + // Register our env variables + println!("cargo:rerun-if-env-changed={}", SKIP_BUILD_ENV); + println!("cargo:rerun-if-env-changed={}", WASM_BUILD_TYPE_ENV); + println!("cargo:rerun-if-env-changed={}", WASM_BUILD_RUSTFLAGS_ENV); + println!("cargo:rerun-if-env-changed={}", WASM_TARGET_DIRECTORY); + println!( + "cargo:rerun-if-env-changed={}", + super::prerequisites::WASM_BUILD_TOOLCHAIN + ); +} + +/// Track files and paths related to the given package to rerun `build.rs` on +/// any relevant change. +fn package_rerun_if_changed(package: &DeduplicatePackage) { + let mut manifest_path = package.manifest_path.clone(); + if manifest_path.ends_with("Cargo.toml") { + manifest_path.pop(); + } + + walkdir::WalkDir::new(&manifest_path) + .into_iter() + .filter_entry(|p| { + // Ignore this entry if it is a directory that contains a `Cargo.toml` that is + // not the `Cargo.toml` related to the current package. This is done to ignore + // sub-crates of a crate. If such a sub-crate is a dependency, it will be + // processed independently anyway. + p.path() == manifest_path || !p.path().is_dir() || !p.path().join("Cargo.toml").exists() + }) + .filter_map(|p| p.ok().map(|p| p.into_path())) + .filter(|p| p.is_dir() || p.extension().map(|e| e == "rs" || e == "toml").unwrap_or_default()) + .for_each(rerun_if_changed); +} + +/// Copy the WASM binary to the target directory set in `WASM_TARGET_DIRECTORY` +/// environment variable. If the variable is not set, this is a no-op. +fn copy_wasm_to_target_directory(cargo_manifest: &Path, wasm_binary: &WasmBinary) { + let target_dir = match env::var(WASM_TARGET_DIRECTORY) { + Ok(path) => PathBuf::from(path), + Err(_) => return, + }; + + if !target_dir.is_absolute() { + panic!( + "Environment variable `{}` with `{}` is not an absolute path!", + WASM_TARGET_DIRECTORY, + target_dir.display(), + ); + } + + fs::create_dir_all(&target_dir).expect("Creates `WASM_TARGET_DIRECTORY`."); + + fs::copy( + wasm_binary.wasm_binary_path(), + target_dir.join(format!("{}.wasm", get_wasm_binary_name(cargo_manifest))), + ) + .expect("Copies WASM binary to `WASM_TARGET_DIRECTORY`."); +} diff --git a/bencher/src/colorize.rs b/bencher/src/colorize.rs new file mode 100644 index 0000000..98cf737 --- /dev/null +++ b/bencher/src/colorize.rs @@ -0,0 +1,39 @@ +/// Environment variable to disable color output of the wasm build. +const WASM_BUILD_NO_COLOR: &str = "WASM_BUILD_NO_COLOR"; + +/// Returns `true` when color output is enabled. +pub fn color_output_enabled() -> bool { + std::env::var(WASM_BUILD_NO_COLOR).is_err() +} + +pub fn red_bold(message: &str) -> String { + if color_output_enabled() { + ansi_term::Color::Red.bold().paint(message).to_string() + } else { + message.into() + } +} + +pub fn yellow_bold(message: &str) -> String { + if color_output_enabled() { + ansi_term::Color::Yellow.bold().paint(message).to_string() + } else { + message.into() + } +} + +pub fn cyan(message: &str) -> String { + if crate::build_wasm::color_output_enabled() { + ansi_term::Color::Cyan.paint(message).to_string() + } else { + message.into() + } +} + +pub fn green_bold(message: &str) -> String { + if crate::build_wasm::color_output_enabled() { + ansi_term::Color::Green.bold().paint(message).to_string() + } else { + message.into() + } +} diff --git a/bencher/src/handler.rs b/bencher/src/handler.rs index 8533d2d..a855037 100644 --- a/bencher/src/handler.rs +++ b/bencher/src/handler.rs @@ -1,27 +1,35 @@ -use crate::BenchResult; +use crate::{ + colorize::{cyan, green_bold}, + BenchResult, +}; use codec::Decode; use linregress::{FormulaRegressionBuilder, RegressionDataBuilder}; use serde::{Deserialize, Serialize}; use std::io::Write; +use std::time::Duration; #[derive(Serialize, Deserialize, Default, Debug, Clone)] struct BenchData { pub name: String, pub base_weight: u64, pub base_reads: u32, + pub base_repeat_reads: u32, pub base_writes: u32, + pub base_repeat_writes: u32, } /// Handle bench results pub fn handle(output: Vec<u8>) { + println!(); + + let pkg_name = std::env::var("CARGO_PKG_NAME").unwrap_or_default().replace("-", "_"); + let results = <Vec<BenchResult> as Decode>::decode(&mut &output[..]).unwrap(); let data: Vec<BenchData> = results .into_iter() .map(|result| { let name = String::from_utf8_lossy(&result.method).to_string(); - eprintln!("{:#?}", result); - let y: Vec<f64> = result.elapses.into_iter().map(|x| x as f64).collect(); let x: Vec<f64> = (0..y.len()).into_iter().map(|x| x as f64).collect(); let data = vec![("Y", y), ("X", x)]; @@ -34,15 +42,31 @@ pub fn handle(output: Vec<u8>) { .fit() .unwrap(); + println!( + "{} {:<60} {:>20} {:<20} {:<20}", + green_bold("Bench"), + cyan(&format!("{}::{}", pkg_name, name)), + green_bold(&format!( + "{:?}", + Duration::from_nanos(model.parameters.intercept_value as u64) + )), + format!("reads: {}", green_bold(&result.reads.to_string())), + format!("writes: {}", green_bold(&result.writes.to_string())) + ); + BenchData { name, base_weight: model.parameters.intercept_value as u64 * 1_000, base_reads: result.reads, + base_repeat_reads: result.repeat_reads, base_writes: result.writes, + base_repeat_writes: result.repeat_writes, } }) .collect(); + println!(); + if let Ok(json) = serde_json::to_string(&data) { let stdout = ::std::io::stdout(); let mut handle = stdout.lock(); diff --git a/bencher/src/lib.rs b/bencher/src/lib.rs index 2ef25f7..746f4e6 100644 --- a/bencher/src/lib.rs +++ b/bencher/src/lib.rs @@ -3,12 +3,29 @@ #[doc(hidden)] pub extern crate frame_benchmarking; #[doc(hidden)] +pub extern crate paste; +#[doc(hidden)] pub extern crate sp_core; #[doc(hidden)] +pub extern crate sp_io; +#[doc(hidden)] pub extern crate sp_std; +mod macros; + +#[cfg(feature = "std")] +pub mod bench_runner; +#[cfg(feature = "std")] +pub mod build_wasm; +#[cfg(feature = "std")] +mod colorize; +#[cfg(feature = "std")] +pub mod handler; +#[cfg(feature = "std")] +mod redundant_meter; + use codec::{Decode, Encode}; -use sp_std::prelude::Vec; +use sp_std::prelude::{Box, Vec}; #[derive(Encode, Decode, Default, Clone, PartialEq, Debug)] pub struct BenchResult { @@ -20,9 +37,158 @@ pub struct BenchResult { pub repeat_writes: u32, } -mod macros; +pub struct Bencher { + pub name: Vec<u8>, + pub results: Vec<BenchResult>, + pub prepare: Box<dyn Fn()>, + pub bench: Box<dyn Fn()>, + pub verify: Box<dyn Fn()>, +} + +impl Default for Bencher { + fn default() -> Self { + Bencher { + name: Vec::new(), + results: Vec::new(), + prepare: Box::new(|| {}), + bench: Box::new(|| {}), + verify: Box::new(|| {}), + } + } +} + +impl Bencher { + /// Reset name and blocks + pub fn reset(&mut self) { + self.name = Vec::new(); + self.prepare = Box::new(|| {}); + self.bench = Box::new(|| {}); + self.verify = Box::new(|| {}); + } + + /// Set bench name + pub fn name(&mut self, name: &str) -> &mut Self { + self.name = name.as_bytes().to_vec(); + self + } + + /// Set prepare block + pub fn prepare(&mut self, prepare: impl Fn() + 'static) -> &mut Self { + self.prepare = Box::new(prepare); + self + } + + /// Set verify block + pub fn verify(&mut self, verify: impl Fn() + 'static) -> &mut Self { + self.verify = Box::new(verify); + self + } + + /// Set bench block + pub fn bench(&mut self, bench: impl Fn() + 'static) -> &mut Self { + self.bench = Box::new(bench); + self + } + + /// Run benchmark for tests + #[cfg(feature = "std")] + pub fn run(&mut self) { + // Execute prepare block + (self.prepare)(); + // Execute bench block + (self.bench)(); + // Execute verify block + (self.verify)(); + } + + /// Run benchmark + #[cfg(not(feature = "std"))] + pub fn run(&mut self) { + assert!(self.name.len() > 0, "bench name not defined"); + // Warm up the DB + frame_benchmarking::benchmarking::commit_db(); + frame_benchmarking::benchmarking::wipe_db(); + + let mut result = BenchResult { + method: self.name.clone(), + ..Default::default() + }; + + for _ in 0..50 { + // Execute prepare block + (self.prepare)(); + + frame_benchmarking::benchmarking::commit_db(); + frame_benchmarking::benchmarking::reset_read_write_count(); + bencher::reset(); + + let start_time = frame_benchmarking::benchmarking::current_time(); + // Execute bench block + (self.bench)(); + let end_time = frame_benchmarking::benchmarking::current_time(); + frame_benchmarking::benchmarking::commit_db(); + + let (elapsed, reads, repeat_reads, writes, repeat_writes) = + bencher::finalized_results(end_time - start_time); + + // Execute verify block + (self.verify)(); + + // Reset the DB + frame_benchmarking::benchmarking::wipe_db(); + + result.elapses.push(elapsed); + + result.reads = sp_std::cmp::max(result.reads, reads); + result.repeat_reads = sp_std::cmp::max(result.repeat_reads, repeat_reads); + result.writes = sp_std::cmp::max(result.writes, writes); + result.repeat_writes = sp_std::cmp::max(result.repeat_writes, repeat_writes); + } + self.results.push(result); + } +} #[cfg(feature = "std")] -pub mod bench_runner; -#[cfg(feature = "std")] -pub mod handler; +thread_local! { + static REDUNDANT_METER: std::cell::RefCell<redundant_meter::RedundantMeter> = std::cell::RefCell::new(redundant_meter::RedundantMeter::default()); +} + +#[sp_runtime_interface::runtime_interface] +pub trait Bencher { + fn panic(str: Vec<u8>) { + let msg = String::from_utf8_lossy(&str); + eprintln!("{}", colorize::red_bold(&msg)); + std::process::exit(-1); + } + + fn entering_method() -> Vec<u8> { + REDUNDANT_METER.with(|x| x.borrow_mut().entering_method()) + } + + fn leaving_method(identifier: Vec<u8>) { + REDUNDANT_METER.with(|x| { + x.borrow_mut().leaving_method(identifier); + }); + } + + fn finalized_results(elapsed: u128) -> (u128, u32, u32, u32, u32) { + let (reads, repeat_reads, writes, repeat_writes) = frame_benchmarking::benchmarking::read_write_count(); + + let (redundant_elapsed, redundant_reads, redundant_repeat_reads, redundant_writes, redundant_repeat_writes) = + REDUNDANT_METER.with(|x| x.borrow_mut().take_results()); + + let elapsed = elapsed - redundant_elapsed; + let reads = reads - redundant_reads; + let repeat_reads = repeat_reads - redundant_repeat_reads; + let writes = writes - redundant_writes; + let repeat_writes = repeat_writes - redundant_repeat_writes; + + (elapsed, reads, repeat_reads, writes, repeat_writes) + } + + fn reset() { + REDUNDANT_METER.with(|x| { + x.borrow_mut().reset(); + }); + } +} diff --git a/bencher/src/macros.rs b/bencher/src/macros.rs index f654813..e8b1831 100644 --- a/bencher/src/macros.rs +++ b/bencher/src/macros.rs @@ -1,160 +1,126 @@ /// 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" +/// name = "your-module" /// .. -/// build = 'build.rs' -/// -/// [build-dependencies] -/// substrate-wasm-builder = '4.0.0' -/// /// [[bench]] -/// name = 'benches' +/// name = 'your-module-benches' /// harness = false -/// path = 'bench_runner.rs' +/// path = 'src/benches.rs' /// required-features = ['bench'] /// /// [features] -/// bench = [] +/// bench = [ +/// 'orml-bencher/bench' +/// 'orml-weight-meter/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::<Block>(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; +/// use orml_bencher::{Bencher, bench}; +/// use your_module::mock::{Block, YourModule}; /// /// fn foo(b: &mut Bencher) { -/// b.bench("foo", || { +/// b.prepare(|| { +/// // optional. prepare block, runs before bench +/// }) +/// .bench(|| { +/// // foo must have macro `[orml_weight_meter::weight(..)]` /// YourModule::foo(); +/// }) +/// .verify(|| { +/// // optional. verify block, runs before bench /// }); /// } /// /// fn bar(b: &mut Bencher) { -/// b.bench("bar", || { +/// // optional. method name is used by default i.e: `bar` +/// b.name("bench_name") +/// .bench(|| { +/// // bar must have macro `[orml_weight_meter::weight(..)]` /// YourModule::bar(); /// }); /// } /// -/// orml_bencher::bench!(foo, bar); +/// bench!(Block, foo, bar); // Tests are generated automatically /// ``` /// Update `src/lib.rs`: /// ```.ignore /// #[cfg(any(feature = "bench", test))] /// pub mod mock; /* mock runtime needs to be compiled into wasm */ -/// #[cfg(feature = "bench")] +/// #[cfg(any(feature = "bench", test))] /// pub mod benches; +/// +/// extern crate self as your_module; /// ``` +/// +/// Run benchmarking: `cargo bench --features=bench` #[macro_export] macro_rules! bench { ( + $block:tt, $($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<BenchResult>, - } - - impl Bencher { - pub fn bench<F: Fn() -> ()>(&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); - } - } - + #[cfg(feature = "bench")] $crate::sp_core::wasm_export_functions! { - fn run_benches() -> Vec<BenchResult> { - let mut bencher = Bencher::default(); + fn run_benches() -> $crate::sp_std::vec::Vec<$crate::BenchResult> { + let mut bencher = $crate::Bencher::default(); $( + bencher.reset(); $method(&mut bencher); + if bencher.name.len() == 0 { + // use method name as default bench name + bencher.name(stringify!($method)); + } + bencher.run(); )+ bencher.results } } + + #[cfg(all(feature = "bench", not(feature = "std")))] + #[panic_handler] + #[no_mangle] + fn panic_handler(info: &::core::panic::PanicInfo) -> ! { + unsafe { + let message = $crate::sp_std::alloc::format!("{}", info); + $crate::bencher::panic(message.as_bytes().to_vec()); + core::arch::wasm32::unreachable(); + } + } + + #[cfg(all(feature = "std", feature = "bench"))] + pub fn main() -> std::io::Result<()> { + let wasm = $crate::build_wasm::build()?; + match $crate::bench_runner::run::<$block>(wasm) { + Ok(output) => { $crate::handler::handle(output); } + Err(e) => { eprintln!("{:?}", e); } + }; + Ok(()) + } + + // Tests + $( + $crate::paste::item! { + #[test] + fn [<test_ $method>] () { + $crate::sp_io::TestExternalities::new_empty().execute_with(|| { + let mut bencher = $crate::Bencher::default(); + $method(&mut bencher); + bencher.run(); + }); + } + } + )+ + } } diff --git a/bencher/src/redundant_meter.rs b/bencher/src/redundant_meter.rs new file mode 100644 index 0000000..64d11ce --- /dev/null +++ b/bencher/src/redundant_meter.rs @@ -0,0 +1,111 @@ +use codec::{Decode, Encode}; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; + +#[derive(Encode, Decode, Default, Clone, PartialEq, Debug)] +struct RedundantResult { + identifier: Vec<u8>, + timestamp: u128, + reads: u32, + repeat_reads: u32, + writes: u32, + repeat_writes: u32, +} + +/// RedundantMeter is used to measure resources been used by methods that +/// already been benchmarked and have `[orml_weight_meter::weight(..)] macro +/// defined. First method with that macro will be skipped and after that every +/// method with macro defined will be measured as redundant result. +#[derive(Default)] +pub struct RedundantMeter { + started: bool, + results: Vec<RedundantResult>, + current: Option<RedundantResult>, +} + +impl RedundantMeter { + /// Entering method with `[orml_weight_meter::weight(..)]` macro + pub fn entering_method(&mut self) -> Vec<u8> { + if !self.started { + self.started = true; + return Vec::new(); + } + + if self.current.is_some() { + return Vec::new(); + } + + let timestamp = frame_benchmarking::benchmarking::current_time(); + frame_benchmarking::benchmarking::commit_db(); + let (reads, repeat_reads, writes, repeat_writes) = frame_benchmarking::benchmarking::read_write_count(); + + let identifier: Vec<u8> = thread_rng() + .sample_iter(&Alphanumeric) + .take(10) + .map(char::from) + .collect::<String>() + .encode(); + + self.current = Some(RedundantResult { + identifier: identifier.to_owned(), + timestamp, + reads, + repeat_reads, + writes, + repeat_writes, + }); + + identifier + } + + /// Leaving method with `[orml_weight_meter::weight(..)]` macro + pub fn leaving_method(&mut self, identifier: Vec<u8>) { + if let Some(current) = &self.current { + if current.identifier.eq(&identifier) { + frame_benchmarking::benchmarking::commit_db(); + let (reads, repeat_reads, writes, repeat_writes) = frame_benchmarking::benchmarking::read_write_count(); + let timestamp = frame_benchmarking::benchmarking::current_time(); + + self.results.push(RedundantResult { + identifier, + timestamp: timestamp - current.timestamp, + reads: reads - current.reads, + repeat_reads: repeat_reads - current.repeat_reads, + writes: writes - current.writes, + repeat_writes: repeat_writes - current.repeat_writes, + }); + + // reset current + self.current = None; + } + } + } + + /// Take bench results and reset for next measurement + pub fn take_results(&mut self) -> (u128, u32, u32, u32, u32) { + assert!(self.current == None, "benchmark in progress"); + + let mut elapsed = 0u128; + let mut reads = 0u32; + let mut repeat_reads = 0u32; + let mut writes = 0u32; + let mut repeat_writes = 0u32; + + self.results.iter().for_each(|x| { + elapsed += x.timestamp; + reads += x.reads; + repeat_reads += x.repeat_reads; + writes += x.writes; + repeat_writes += x.repeat_writes; + }); + + self.reset(); + + (elapsed, reads, repeat_reads, writes, repeat_writes) + } + + pub fn reset(&mut self) { + self.started = false; + self.results = Vec::new(); + self.current = None; + } +} diff --git a/weight-gen/src/template.hbs b/weight-gen/src/template.hbs index 470b00c..3ad2b78 100644 --- a/weight-gen/src/template.hbs +++ b/weight-gen/src/template.hbs @@ -12,10 +12,10 @@ impl<T: frame_system::Config> ModuleWeights<T> { {{~#each benchmarks as |benchmark|}} pub fn {{benchmark.name~}} () -> Weight { ({{underscore benchmark.base_weight}} as Weight) - {{~#if (ne benchmark.base_reads "0")}} + {{~#if (ne benchmark.base_reads 0)}} .saturating_add(T::DbWeight::get().reads({{benchmark.base_reads}} as Weight)) {{~/if}} - {{~#if (ne benchmark.base_writes "0")}} + {{~#if (ne benchmark.base_writes 0)}} .saturating_add(T::DbWeight::get().writes({{benchmark.base_writes}} as Weight)) {{~/if}} } diff --git a/weight-meter/Cargo.toml b/weight-meter/Cargo.toml index 781e12f..339d4a8 100644 --- a/weight-meter/Cargo.toml +++ b/weight-meter/Cargo.toml @@ -12,6 +12,7 @@ targets = ["x86_64-unknown-linux-gnu"] spin = "0.7.1" frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.4", default-features = false } weight-meter-procedural = { path = "weight-meter-procedural", default-features = false } +orml-bencher = { path = "../bencher", default-features = false, optional = true } [dev-dependencies] serde = { version = "1.0.124" } @@ -29,4 +30,9 @@ default = ["std"] std = [ "frame-support/std", "weight-meter-procedural/std", + "orml-bencher/std", +] +bench = [ + "weight-meter-procedural/bench", + "orml-bencher", ] diff --git a/weight-meter/src/lib.rs b/weight-meter/src/lib.rs index cfdc45d..84d20cd 100644 --- a/weight-meter/src/lib.rs +++ b/weight-meter/src/lib.rs @@ -57,8 +57,6 @@ struct Meter { mod meter_no_std; mod meter_std; -// For use in mock file -#[cfg(test)] extern crate self as orml_weight_meter; #[cfg(test)] diff --git a/weight-meter/weight-meter-procedural/Cargo.toml b/weight-meter/weight-meter-procedural/Cargo.toml index 05554ee..7447cb6 100644 --- a/weight-meter/weight-meter-procedural/Cargo.toml +++ b/weight-meter/weight-meter-procedural/Cargo.toml @@ -15,4 +15,5 @@ syn = { version = "1.0.58", features = ["full"] } [features] default = ["std"] -std = [] \ No newline at end of file +std = [] +bench = [] diff --git a/weight-meter/weight-meter-procedural/src/lib.rs b/weight-meter/weight-meter-procedural/src/lib.rs index 87f5f75..953d7e6 100644 --- a/weight-meter/weight-meter-procedural/src/lib.rs +++ b/weight-meter/weight-meter-procedural/src/lib.rs @@ -17,6 +17,7 @@ pub fn start(_attr: TokenStream, item: TokenStream) -> TokenStream { .into() } +#[cfg(not(feature = "bench"))] #[proc_macro_attribute] pub fn weight(attr: TokenStream, item: TokenStream) -> TokenStream { let weight: syn::Expr = syn::parse(attr).unwrap(); @@ -30,3 +31,19 @@ pub fn weight(attr: TokenStream, item: TokenStream) -> TokenStream { }) .into() } + +#[cfg(feature = "bench")] +#[proc_macro_attribute] +pub fn weight(_attr: TokenStream, item: TokenStream) -> TokenStream { + let ItemFn { attrs, sig, block, .. } = syn::parse(item).unwrap(); + (quote! { + #(#attrs)* + pub #sig { + let identifier: ::sp_std::vec::Vec<u8> = ::orml_bencher::bencher::entering_method(); + let result = #block; + ::orml_bencher::bencher::leaving_method(identifier); + result + } + }) + .into() +} -- GitLab