Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Add benchmark-block command #11091

Merged
merged 6 commits into from
Mar 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions bin/node/cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ pub enum Subcommand {
#[clap(name = "benchmark", about = "Benchmark runtime pallets.")]
Benchmark(frame_benchmarking_cli::BenchmarkCmd),

/// Benchmark the execution time of historic blocks and compare it to their consumed weight.
#[clap(
name = "benchmark-block",
about = "Benchmark the execution time of historic blocks and compare it to their consumed weight."
)]
BenchmarkBlock(frame_benchmarking_cli::BlockCmd),

/// Sub command for benchmarking the per-block and per-extrinsic execution overhead.
#[clap(
name = "benchmark-overhead",
Expand Down
7 changes: 7 additions & 0 deletions bin/node/cli/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ pub fn run() -> Result<()> {
You can enable it with `--features runtime-benchmarks`."
.into())
},
Some(Subcommand::BenchmarkBlock(cmd)) => {
let runner = cli.create_runner(cmd)?;
runner.async_run(|config| {
let PartialComponents { client, task_manager, .. } = new_partial(&config)?;
Ok((cmd.run(client), task_manager))
})
},
Some(Subcommand::BenchmarkOverhead(cmd)) => {
let runner = cli.create_runner(cmd)?;
runner.async_run(|mut config| {
Expand Down
2 changes: 2 additions & 0 deletions utils/frame/benchmarking-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
frame-benchmarking = { version = "4.0.0-dev", path = "../../../frame/benchmarking" }
frame-support = { version = "4.0.0-dev", path = "../../../frame/support" }
frame-system = { version = "4.0.0-dev", path = "../../../frame/system" }
sp-core = { version = "6.0.0", path = "../../../primitives/core" }
sc-block-builder = { version = "0.10.0-dev", path = "../../../client/block-builder" }
sc-service = { version = "0.10.0-dev", default-features = false, path = "../../../client/service" }
Expand Down Expand Up @@ -50,6 +51,7 @@ hash-db = "0.15.2"
hex = "0.4.3"
memory-db = "0.29.0"
rand = { version = "0.8.4", features = ["small_rng"] }
thousands = "0.2.0"

[features]
default = ["db", "sc-client-db/runtime-benchmarks"]
Expand Down
176 changes: 176 additions & 0 deletions utils/frame/benchmarking-cli/src/block/bench.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// 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 codec::DecodeAll;
use frame_support::weights::constants::WEIGHT_PER_NANOS;
use frame_system::ConsumedWeight;
use sc_block_builder::{BlockBuilderApi, BlockBuilderProvider};
use sc_cli::{Error, Result};
use sc_client_api::{Backend as ClientBackend, BlockBackend, StorageProvider, UsageProvider};
use sp_api::{ApiExt, Core, HeaderT, ProvideRuntimeApi};
use sp_blockchain::Error::RuntimeApiError;
use sp_runtime::{generic::BlockId, traits::Block as BlockT, DigestItem, OpaqueExtrinsic};
use sp_storage::StorageKey;

use clap::Args;
use log::{info, warn};
use serde::Serialize;
use std::{fmt::Debug, marker::PhantomData, sync::Arc, time::Instant};
use thousands::Separable;

use crate::storage::record::{StatSelect, Stats};

/// Log target for printing block weight info.
const LOG_TARGET: &'static str = "benchmark::block::weight";

/// Parameters for modifying the benchmark behaviour.
#[derive(Debug, Default, Serialize, Clone, PartialEq, Args)]
pub struct BenchmarkParams {
/// Number of the first block to consider.
#[clap(long)]
pub from: u32,

/// Last block number to consider.
#[clap(long)]
pub to: u32,

/// Number of times that the benchmark should be repeated for each block.
#[clap(long, default_value = "10")]
pub repeat: u32,
}

/// Convenience closure for the [`Benchmark::run()`] function.
pub struct Benchmark<Block, BA, C> {
client: Arc<C>,
params: BenchmarkParams,
_p: PhantomData<(Block, BA, C)>,
}

/// Helper for nano seconds.
type NanoSeconds = u64;

impl<Block, BA, C> Benchmark<Block, BA, C>
where
Block: BlockT<Extrinsic = OpaqueExtrinsic>,
BA: ClientBackend<Block>,
C: BlockBuilderProvider<BA, Block, C>
+ ProvideRuntimeApi<Block>
+ StorageProvider<Block, BA>
+ UsageProvider<Block>
+ BlockBackend<Block>,
C::Api: ApiExt<Block, StateBackend = BA::State> + BlockBuilderApi<Block>,
{
/// Returns a new [`Self`] from the arguments.
pub fn new(client: Arc<C>, params: BenchmarkParams) -> Self {
Self { client, params, _p: PhantomData }
}

/// Benchmark the execution speed of historic blocks and log the results.
pub fn run(&self) -> Result<()> {
if self.params.from == 0 {
return Err("Cannot benchmark the genesis block".into())
}

for i in self.params.from..=self.params.to {
let block_num = BlockId::Number(i.into());
let parent_num = BlockId::Number(((i - 1) as u32).into());
let consumed = self.consumed_weight(&block_num)?;

let block =
self.client.block(&block_num)?.ok_or(format!("Block {} not found", block_num))?;
let block = self.unsealed(block.block);
let took = self.measure_block(&block, &parent_num)?;

self.log_weight(i, block.extrinsics().len(), consumed, took);
}

Ok(())
}

/// Return the average *execution* aka. *import* time of the block.
fn measure_block(&self, block: &Block, parent_num: &BlockId<Block>) -> Result<NanoSeconds> {
let mut record = Vec::<NanoSeconds>::default();
// Interesting part here:
// Execute the block multiple times and collect stats about its execution time.
for _ in 0..self.params.repeat {
ggwpez marked this conversation as resolved.
Show resolved Hide resolved
let block = block.clone();
let runtime_api = self.client.runtime_api();
let start = Instant::now();

runtime_api
.execute_block(&parent_num, block)
.map_err(|e| Error::Client(RuntimeApiError(e)))?;

record.push(start.elapsed().as_nanos() as NanoSeconds);
}

let took = Stats::new(&record)?.select(StatSelect::Average);
Ok(took)
}

/// Returns the total nanoseconds of a [`frame_system::ConsumedWeight`] for a block number.
///
/// This is the post-dispatch corrected weight and is only available
/// after executing the block.
fn consumed_weight(&self, block: &BlockId<Block>) -> Result<NanoSeconds> {
// Hard-coded key for System::BlockWeight. It could also be passed in as argument
// for the benchmark, but I think this should work as well.
ggwpez marked this conversation as resolved.
Show resolved Hide resolved
let hash = hex::decode("26aa394eea5630e07c48ae0c9558cef734abf5cb34d6244378cddbf18e849d96")?;
let key = StorageKey(hash);

let mut raw_weight = &self
.client
.storage(&block, &key)?
.ok_or(format!("Could not find System::BlockWeight for block: {}", block))?
.0[..];

let weight = ConsumedWeight::decode_all(&mut raw_weight)?;
// Should be divisible, but still use floats in case we ever change that.
Ok((weight.total() as f64 / WEIGHT_PER_NANOS as f64).floor() as NanoSeconds)
}

/// Prints the weight info of a block to the console.
fn log_weight(&self, num: u32, num_ext: usize, consumed: NanoSeconds, took: NanoSeconds) {
// The ratio of weight that the block used vs what it consumed.
// This should in general not exceed 100% (minus outliers).
let percent = (took as f64 / consumed as f64) * 100.0;

let msg = format!(
"Block {} with {: >5} tx used {: >6.2}% of its weight ({: >14} of {: >14} ns)",
num,
num_ext,
percent,
took.separate_with_commas(),
consumed.separate_with_commas()
);

if took <= consumed {
info!(target: LOG_TARGET, "{}", msg);
} else {
warn!(target: LOG_TARGET, "{} - OVER WEIGHT!", msg);
}
}

/// Removes the consensus seal from the block.
fn unsealed(&self, block: Block) -> Block {
let (mut header, exts) = block.deconstruct();
header.digest_mut().logs.retain(|item| !matches!(item, DigestItem::Seal(_, _)));
Block::new(header, exts)
}
}
101 changes: 101 additions & 0 deletions utils/frame/benchmarking-cli/src/block/cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// 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 [`BlockCmd`] as entry point for the CLI to execute
//! the *block* benchmark.

use sc_block_builder::{BlockBuilderApi, BlockBuilderProvider};
use sc_cli::{CliConfiguration, ImportParams, Result, SharedParams};
use sc_client_api::{Backend as ClientBackend, BlockBackend, StorageProvider, UsageProvider};
use sp_api::{ApiExt, ProvideRuntimeApi};
use sp_runtime::{traits::Block as BlockT, OpaqueExtrinsic};

use clap::Parser;
use std::{fmt::Debug, sync::Arc};

use super::bench::{Benchmark, BenchmarkParams};

/// Benchmark the execution time historic blocks.
///
/// This can be used to verify that blocks do not use more weight than they consumed
/// in their `WeightInfo`. Example:
///
/// Let's say you are on a Substrate chain and want to verify that the first 3 blocks
/// did not use more weight than declared which would otherwise be an issue.
/// To test this with a dev node, first create one with a temp directory:
///
/// $ substrate --dev -d /tmp/my-dev --execution wasm --wasm-execution compiled
///
/// And wait some time to let it produce 3 blocks. Then benchmark them with:
///
/// $ substrate benchmark-block --from 1 --to 3 --dev -d /tmp/my-dev
/// --execution wasm --wasm-execution compiled --pruning archive
///
/// The output will be similar to this:
///
/// Block 1 with 1 tx used 77.34% of its weight ( 5,308,964 of 6,864,645 ns)
/// Block 2 with 1 tx used 77.99% of its weight ( 5,353,992 of 6,864,645 ns)
/// Block 3 with 1 tx used 75.91% of its weight ( 5,305,938 of 6,989,645 ns)
///
/// The percent number is important and indicates how much weight
/// was used as compared to the consumed weight.
/// This number should be below 100% for reference hardware.
#[derive(Debug, Parser)]
pub struct BlockCmd {
#[allow(missing_docs)]
#[clap(flatten)]
pub shared_params: SharedParams,

#[allow(missing_docs)]
#[clap(flatten)]
pub import_params: ImportParams,

#[allow(missing_docs)]
#[clap(flatten)]
pub params: BenchmarkParams,
}

impl BlockCmd {
/// Benchmark the execution time of historic blocks and compare it to their consumed weight.
///
/// Output will be printed to console.
pub async fn run<Block, BA, C>(&self, client: Arc<C>) -> Result<()>
where
Block: BlockT<Extrinsic = OpaqueExtrinsic>,
BA: ClientBackend<Block>,
C: BlockBuilderProvider<BA, Block, C>
+ BlockBackend<Block>
+ ProvideRuntimeApi<Block>
+ StorageProvider<Block, BA>
+ UsageProvider<Block>,
C::Api: ApiExt<Block, StateBackend = BA::State> + BlockBuilderApi<Block>,
{
// Put everything in the benchmark type to have the generic types handy.
Benchmark::new(client, self.params.clone()).run()
}
}

// Boilerplate
impl CliConfiguration for BlockCmd {
fn shared_params(&self) -> &SharedParams {
&self.shared_params
}

fn import_params(&self) -> Option<&ImportParams> {
Some(&self.import_params)
}
}
24 changes: 24 additions & 0 deletions utils/frame/benchmarking-cli/src/block/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// 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.

//! Crate to benchmark the execution time of historic blocks
//! and compare it to their consumed weight.

mod bench;
mod cmd;

pub use cmd::BlockCmd;
2 changes: 2 additions & 0 deletions utils/frame/benchmarking-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

mod block;
mod command;
pub mod overhead;
mod post_processing;
Expand All @@ -24,6 +25,7 @@ mod writer;
use sc_cli::{ExecutionStrategy, WasmExecutionMethod, DEFAULT_WASM_EXECUTION_METHOD};
use std::{fmt::Debug, path::PathBuf};

pub use block::BlockCmd;
pub use overhead::{ExtrinsicBuilder, OverheadCmd};
pub use storage::StorageCmd;

Expand Down
Loading