Add execution overhead benchmarking (#10977)

* Add benchmark-block

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Remove first approach

This reverts commit cf96a0a2307433f23187e77864de4a89ecbaef0a.

* Add block and extrinsic benchmarks

* Doc

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Fix template

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Beauty fixes

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Check for non-empty chain

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Add tests for Stats

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Review fixes

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Review fixes

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Apply suggestions from code review

Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>

* Review fixes

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Review fixes

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Push first version again

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Push first version again

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Cleanup

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Cleanup

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Cleanup

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Beauty fixes

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Apply suggestions from code review

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Update utils/frame/benchmarking-cli/src/overhead/template.rs

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Review fixes

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Doc + Template fixes

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Review fixes

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Comment fix

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Add test

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Pust merge fixup

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Fixup

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Move code to better place

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>
Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>
This commit is contained in:
Oliver Tale-Yazdi
2022-03-17 11:40:31 +01:00
committed by GitHub
parent 26a8c7e6b2
commit 96cf135586
17 changed files with 852 additions and 53 deletions
@@ -0,0 +1,210 @@
// This file is part of Substrate.
// Copyright (C) 2022 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.
//! Contains the core benchmarking logic.
use sc_block_builder::{BlockBuilderApi, BlockBuilderProvider};
use sc_cli::{Error, Result};
use sc_client_api::Backend as ClientBackend;
use sp_api::{ApiExt, BlockId, Core, ProvideRuntimeApi};
use sp_blockchain::{
ApplyExtrinsicFailed::Validity,
Error::{ApplyExtrinsicFailed, RuntimeApiError},
};
use sp_runtime::{
traits::{Block as BlockT, Zero},
transaction_validity::{InvalidTransaction, TransactionValidityError},
OpaqueExtrinsic,
};
use clap::Args;
use log::info;
use serde::Serialize;
use std::{marker::PhantomData, sync::Arc, time::Instant};
use crate::{overhead::cmd::ExtrinsicBuilder, storage::record::Stats};
/// Parameters to configure an *overhead* benchmark.
#[derive(Debug, Default, Serialize, Clone, PartialEq, Args)]
pub struct BenchmarkParams {
/// Rounds of warmups before measuring.
#[clap(long, default_value = "100")]
pub warmup: u32,
/// How many times the benchmark should be repeated.
#[clap(long, default_value = "1000")]
pub repeat: u32,
/// Maximal number of extrinsics that should be put into a block.
///
/// Only useful for debugging.
#[clap(long)]
pub max_ext_per_block: Option<u32>,
}
/// The results of multiple runs in nano seconds.
pub(crate) type BenchRecord = Vec<u64>;
/// Type of a benchmark.
#[derive(Serialize, Clone, PartialEq, Copy)]
pub(crate) enum BenchmarkType {
/// Measure the per-extrinsic execution overhead.
Extrinsic,
/// Measure the per-block execution overhead.
Block,
}
/// Holds all objects needed to run the *overhead* benchmarks.
pub(crate) struct Benchmark<Block, BA, C> {
client: Arc<C>,
params: BenchmarkParams,
inherent_data: sp_inherents::InherentData,
ext_builder: Arc<dyn ExtrinsicBuilder>,
_p: PhantomData<(Block, BA)>,
}
impl<Block, BA, C> Benchmark<Block, BA, C>
where
Block: BlockT<Extrinsic = OpaqueExtrinsic>,
BA: ClientBackend<Block>,
C: BlockBuilderProvider<BA, Block, C> + ProvideRuntimeApi<Block>,
C::Api: ApiExt<Block, StateBackend = BA::State> + BlockBuilderApi<Block>,
{
/// Create a new [`Self`] from the arguments.
pub fn new(
client: Arc<C>,
params: BenchmarkParams,
inherent_data: sp_inherents::InherentData,
ext_builder: Arc<dyn ExtrinsicBuilder>,
) -> Self {
Self { client, params, inherent_data, ext_builder, _p: PhantomData }
}
/// Run the specified benchmark.
pub fn bench(&self, bench_type: BenchmarkType) -> Result<Stats> {
let (block, num_ext) = self.build_block(bench_type)?;
let record = self.measure_block(&block, num_ext, bench_type)?;
Stats::new(&record)
}
/// Builds a block for the given benchmark type.
///
/// Returns the block and the number of extrinsics in the block
/// that are not inherents.
fn build_block(&self, bench_type: BenchmarkType) -> Result<(Block, u64)> {
let mut builder = self.client.new_block(Default::default())?;
// Create and insert the inherents.
let inherents = builder.create_inherents(self.inherent_data.clone())?;
for inherent in inherents {
builder.push(inherent)?;
}
// Return early if we just want a block with inherents and no additional extrinsics.
if bench_type == BenchmarkType::Block {
return Ok((builder.build()?.block, 0))
}
// Put as many extrinsics into the block as possible and count them.
info!("Building block, this takes some time...");
let mut num_ext = 0;
for nonce in 0..self.max_ext_per_block() {
let ext = self.ext_builder.remark(nonce)?;
match builder.push(ext.clone()) {
Ok(()) => {},
Err(ApplyExtrinsicFailed(Validity(TransactionValidityError::Invalid(
InvalidTransaction::ExhaustsResources,
)))) => break, // Block is full
Err(e) => return Err(Error::Client(e)),
}
num_ext += 1;
}
if num_ext == 0 {
return Err("A Block must hold at least one extrinsic".into())
}
info!("Extrinsics per block: {}", num_ext);
let block = builder.build()?.block;
Ok((block, num_ext))
}
/// Measures the time that it take to execute a block or an extrinsic.
fn measure_block(
&self,
block: &Block,
num_ext: u64,
bench_type: BenchmarkType,
) -> Result<BenchRecord> {
let mut record = BenchRecord::new();
if bench_type == BenchmarkType::Extrinsic && num_ext == 0 {
return Err("Cannot measure the extrinsic time of an empty block".into())
}
let genesis = BlockId::Number(Zero::zero());
info!("Running {} warmups...", self.params.warmup);
for _ in 0..self.params.warmup {
self.client
.runtime_api()
.execute_block(&genesis, block.clone())
.map_err(|e| Error::Client(RuntimeApiError(e)))?;
}
info!("Executing block {} times", self.params.repeat);
// Interesting part here:
// Execute a block multiple times and record each execution time.
for _ in 0..self.params.repeat {
let block = block.clone();
let runtime_api = self.client.runtime_api();
let start = Instant::now();
runtime_api
.execute_block(&genesis, block)
.map_err(|e| Error::Client(RuntimeApiError(e)))?;
let elapsed = start.elapsed().as_nanos();
if bench_type == BenchmarkType::Extrinsic {
// Checked for non-zero div above.
record.push((elapsed as f64 / num_ext as f64).ceil() as u64);
} else {
record.push(elapsed as u64);
}
}
Ok(record)
}
fn max_ext_per_block(&self) -> u32 {
self.params.max_ext_per_block.unwrap_or(u32::MAX)
}
}
impl BenchmarkType {
/// Short name of the benchmark type.
pub(crate) fn short_name(&self) -> &'static str {
match self {
Self::Extrinsic => "extrinsic",
Self::Block => "block",
}
}
/// Long name of the benchmark type.
pub(crate) fn long_name(&self) -> &'static str {
match self {
Self::Extrinsic => "ExtrinsicBase",
Self::Block => "BlockExecution",
}
}
}
@@ -0,0 +1,118 @@
// This file is part of Substrate.
// Copyright (C) 2022 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.
//! Contains the [`OverheadCmd`] as entry point for the CLI to execute
//! the *overhead* benchmarks.
use sc_block_builder::{BlockBuilderApi, BlockBuilderProvider};
use sc_cli::{CliConfiguration, Result, SharedParams};
use sc_client_api::Backend as ClientBackend;
use sc_service::Configuration;
use sp_api::{ApiExt, ProvideRuntimeApi};
use sp_runtime::{traits::Block as BlockT, OpaqueExtrinsic};
use clap::{Args, Parser};
use log::info;
use serde::Serialize;
use std::{fmt::Debug, sync::Arc};
use crate::{
overhead::{
bench::{Benchmark, BenchmarkParams, BenchmarkType},
template::TemplateData,
},
post_processing::WeightParams,
};
/// Benchmarks the per-block and per-extrinsic execution overhead.
#[derive(Debug, Parser)]
pub struct OverheadCmd {
#[allow(missing_docs)]
#[clap(flatten)]
pub shared_params: SharedParams,
#[allow(missing_docs)]
#[clap(flatten)]
pub params: OverheadParams,
}
/// Configures the benchmark, the post-processing and weight generation.
#[derive(Debug, Default, Serialize, Clone, PartialEq, Args)]
pub struct OverheadParams {
#[allow(missing_docs)]
#[clap(flatten)]
pub weight: WeightParams,
#[allow(missing_docs)]
#[clap(flatten)]
pub bench: BenchmarkParams,
}
/// Used by the benchmark to build signed extrinsics.
///
/// The built extrinsics only need to be valid in the first block
/// who's parent block is the genesis block.
pub trait ExtrinsicBuilder {
/// Build a `System::remark` extrinsic.
fn remark(&self, nonce: u32) -> std::result::Result<OpaqueExtrinsic, &'static str>;
}
impl OverheadCmd {
/// Measures the per-block and per-extrinsic execution overhead.
///
/// Writes the results to console and into two instances of the
/// `weights.hbs` template, one for each benchmark.
pub async fn run<Block, BA, C>(
&self,
cfg: Configuration,
client: Arc<C>,
inherent_data: sp_inherents::InherentData,
ext_builder: Arc<dyn ExtrinsicBuilder>,
) -> Result<()>
where
Block: BlockT<Extrinsic = OpaqueExtrinsic>,
BA: ClientBackend<Block>,
C: BlockBuilderProvider<BA, Block, C> + ProvideRuntimeApi<Block>,
C::Api: ApiExt<Block, StateBackend = BA::State> + BlockBuilderApi<Block>,
{
let bench = Benchmark::new(client, self.params.bench.clone(), inherent_data, ext_builder);
// per-block execution overhead
{
let stats = bench.bench(BenchmarkType::Block)?;
info!("Per-block execution overhead [ns]:\n{:?}", stats);
let template = TemplateData::new(BenchmarkType::Block, &cfg, &self.params, &stats)?;
template.write(&self.params.weight.weight_path)?;
}
// per-extrinsic execution overhead
{
let stats = bench.bench(BenchmarkType::Extrinsic)?;
info!("Per-extrinsic execution overhead [ns]:\n{:?}", stats);
let template = TemplateData::new(BenchmarkType::Extrinsic, &cfg, &self.params, &stats)?;
template.write(&self.params.weight.weight_path)?;
}
Ok(())
}
}
// Boilerplate
impl CliConfiguration for OverheadCmd {
fn shared_params(&self) -> &SharedParams {
&self.shared_params
}
}
@@ -0,0 +1,22 @@
// This file is part of Substrate.
// Copyright (C) 2022 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.
mod bench;
pub mod cmd;
mod template;
pub use cmd::{ExtrinsicBuilder, OverheadCmd};
@@ -0,0 +1,110 @@
// This file is part of Substrate.
// Copyright (C) 2022 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.
//! Converts a benchmark result into [`TemplateData`] and writes
//! it into the `weights.hbs` template.
use sc_cli::Result;
use sc_service::Configuration;
use handlebars::Handlebars;
use log::info;
use serde::Serialize;
use std::{env, fs, path::PathBuf};
use crate::{
overhead::{bench::BenchmarkType, cmd::OverheadParams},
storage::record::Stats,
};
static VERSION: &'static str = env!("CARGO_PKG_VERSION");
static TEMPLATE: &str = include_str!("./weights.hbs");
/// Data consumed by Handlebar to fill out the `weights.hbs` template.
#[derive(Serialize, Debug, Clone)]
pub(crate) struct TemplateData {
/// Short name of the benchmark. Can be "block" or "extrinsic".
long_name: String,
/// Long name of the benchmark. Can be "BlockExecution" or "ExtrinsicBase".
short_name: String,
/// Name of the runtime. Taken from the chain spec.
runtime_name: String,
/// Version of the benchmarking CLI used.
version: String,
/// Date that the template was filled out.
date: String,
/// Command line arguments that were passed to the CLI.
args: Vec<String>,
/// Params of the executed command.
params: OverheadParams,
/// Stats about the benchmark result.
stats: Stats,
/// The resulting weight in ns.
weight: u64,
}
impl TemplateData {
/// Returns a new [`Self`] from the given params.
pub(crate) fn new(
t: BenchmarkType,
cfg: &Configuration,
params: &OverheadParams,
stats: &Stats,
) -> Result<Self> {
let weight = params.weight.calc_weight(stats)?;
Ok(TemplateData {
short_name: t.short_name().into(),
long_name: t.long_name().into(),
runtime_name: cfg.chain_spec.name().into(),
version: VERSION.into(),
date: chrono::Utc::now().format("%Y-%m-%d (Y/M/D)").to_string(),
args: env::args().collect::<Vec<String>>(),
params: params.clone(),
stats: stats.clone(),
weight,
})
}
/// Fill out the `weights.hbs` HBS template with its own data.
/// Writes the result to `path` which can be a directory or a file.
pub fn write(&self, path: &Option<PathBuf>) -> Result<()> {
let mut handlebars = Handlebars::new();
// Format large integers with underscores.
handlebars.register_helper("underscore", Box::new(crate::writer::UnderscoreHelper));
// Don't HTML escape any characters.
handlebars.register_escape_fn(|s| -> String { s.to_string() });
let out_path = self.build_path(path)?;
let mut fd = fs::File::create(&out_path)?;
info!("Writing weights to {:?}", fs::canonicalize(&out_path)?);
handlebars
.render_template_to_write(&TEMPLATE, &self, &mut fd)
.map_err(|e| format!("HBS template write: {:?}", e).into())
}
/// Build a path for the weight file.
fn build_path(&self, weight_out: &Option<PathBuf>) -> Result<PathBuf> {
let mut path = weight_out.clone().unwrap_or(PathBuf::from("."));
if !path.is_dir() {
return Err("Need directory as --weight-path".into())
}
path.push(format!("{}_weights.rs", self.short_name));
Ok(path)
}
}
@@ -0,0 +1,92 @@
// This file is part of Substrate.
// Copyright (C) 2022 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.
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION {{version}}
//! DATE: {{date}}
//!
//! SHORT-NAME: `{{short_name}}`, LONG-NAME: `{{long_name}}`, RUNTIME: `{{runtime_name}}`
//! WARMUPS: `{{params.bench.warmup}}`, REPEAT: `{{params.bench.repeat}}`
//! WEIGHT-PATH: `{{params.weight.weight_path}}`
//! WEIGHT-METRIC: `{{params.weight.weight_metric}}`, WEIGHT-MUL: `{{params.weight.weight_mul}}`, WEIGHT-ADD: `{{params.weight.weight_add}}`
// Executed Command:
{{#each args as |arg|}}
// {{arg}}
{{/each}}
use frame_support::{
parameter_types,
weights::{constants::WEIGHT_PER_NANOS, Weight},
};
parameter_types! {
{{#if (eq short_name "block")}}
/// Time to execute an empty block.
{{else}}
/// Time to execute a NO-OP extrinsic eg. `System::remark`.
{{/if}}
/// Calculated by multiplying the *{{params.weight.weight_metric}}* with `{{params.weight.weight_mul}}` and adding `{{params.weight.weight_add}}`.
///
/// Stats [ns]:
/// Min, Max: {{underscore stats.min}}, {{underscore stats.max}}
/// Average: {{underscore stats.avg}}
/// Median: {{underscore stats.median}}
/// StdDev: {{stats.stddev}}
///
/// Percentiles [ns]:
/// 99th: {{underscore stats.p99}}
/// 95th: {{underscore stats.p95}}
/// 75th: {{underscore stats.p75}}
pub const {{long_name}}Weight: Weight = {{underscore weight}} * WEIGHT_PER_NANOS;
}
#[cfg(test)]
mod test_weights {
use frame_support::weights::constants;
/// Checks that the weight exists and is sane.
// NOTE: If this test fails but you are sure that the generated values are fine,
// you can delete it.
#[test]
fn sane() {
let w = super::{{long_name}}Weight::get();
{{#if (eq short_name "block")}}
// At least 100 µs.
assert!(
w >= 100 * constants::WEIGHT_PER_MICROS,
"Weight should be at least 100 µs."
);
// At most 50 ms.
assert!(
w <= 50 * constants::WEIGHT_PER_MILLIS,
"Weight should be at most 50 ms."
);
{{else}}
// At least 10 µs.
assert!(
w >= 10 * constants::WEIGHT_PER_MICROS,
"Weight should be at least 10 µs."
);
// At most 1 ms.
assert!(
w <= constants::WEIGHT_PER_MILLIS,
"Weight should be at most 1 ms."
);
{{/if}}
}
}