Skip to content

Commit 989802a

Browse files
alexgghbkchr
authored andcommitted
benchmark: storage: Make read/write benchmarks more accurate (#7867)
There are a few problems with these read/write benchmarks which makes them produce misleading results, especially when we enable the trie-cache. The problems are: - Both benchmarks run without PoV recorder enabled, that is not accurate for parachains because without the PoV recorder, you can directly access the key from the value cache, while with the PoV recorder you still need to do the walk through which uses the Node cache, e.g: https://github.com/paritytech/trie/blob/master/trie-db/src/lookup.rs#L446. To fix this I added I parameter enable-pov-recorder which is meant to be used when generating the weights for parachains. - Every write measures both the time to update the key and to compute the storage root and commit all the changes, which is not accurate because the storage root is computed only once at the end of the block. For this I added a new argument --batch-size, which is used to determine how many keys to update and performs the storage root computation only once, it then calculate the per key write cost as `durations / batch-size`. - For reads when you run with the PoV recorder, there is also a benefit from running with the same recorder rather than creating a different recorder every read, so we again use the `batch-size` for than to obtain the amortised cost of a read. - bench warmup seemed to not warmup child keys even when `include-child-trees`, so I fixed that as well ## Results on reference hardware, asset-hub-westend state | Setup | Batch size| Amortized cost of a key write(**ns**) | Amortized cost of a key read(**ns**)| |--------|--------|--------|--------| |Without TrieCache, Without PoV Recorder|1|88_521|46_981| |Without TrieCache, With PoV Recorder|1|95_161|48_711| |With TrieCache, Without PoV Recorder|1|66_008|528| |With TrieCache, With PoV Recorder|1|73_145|12_142| |Without TrieCache, Without PoV Recorder|1000|52_646|72_434| |Without TrieCache, With PoV Recorder|1000|54_896|50_267| |With TrieCache, Without PoV Recorder|1000|30_585|497| |With TrieCache, With PoV Recorder|1000|33_765|6_928| |Without TrieCache, Without PoV Recorder|10_000|48_945|52_730| |Without TrieCache, With PoV Recorder|10_000|50_285|49_860| |With TrieCache, Without PoV Recorder|10_000|25_903|484| |With TrieCache, With PoV Recorder|10_000|28_417|7_153| |Without TrieCache, Without PoV Recorder|100_000|31_359|45_839| |Without TrieCache, With PoV Recorder|100_000|32_932|48_393| |With TrieCache, Without PoV Recorder|100_000|20_255|493| |*With TrieCache, With PoV Recorder*, to be used|100_000|21_998|6_908| ## Results on reference hardware asset-hub-polkadot state | Setup | Batch size| Amortized cost of a key write(**ns**) | Amortized cost of a key read(**ns**)| |--------|--------|--------|--------| |Without TrieCache, Without PoV Recorder|1|102_239|56_209| |Without TrieCache, With PoV Recorder|1|106_659|54_256| |With TrieCache, Without PoV Recorder|1|85_419|608| |With TrieCache, With PoV Recorder|1|95_221|13_567| |Without TrieCache, Without PoV Recorder|1000|61_574|53_767| |Without TrieCache, With PoV Recorder|1000|64_770|66_162| |With TrieCache, Without PoV Recorder|1000|35_879|597| |With TrieCache, With PoV Recorder|1000|39_464|8_482| |Without TrieCache, Without PoV Recorder|10_000|62_465|58_236| |Without TrieCache, With PoV Recorder|10_000|65_082|95_118| |With TrieCache, Without PoV Recorder|10_000|32_259|601| |With TrieCache, With PoV Recorder|10_000|34_620|8_810| |Without TrieCache, Without PoV Recorder|100_000|43_794|69_157| |Without TrieCache, With PoV Recorder|100_000|45_060|66_343| |With TrieCache, Without PoV Recorder|100_000|25_327|596| |*With TrieCache, With PoV Recorder*, to be used|100_000|27_622|8_598| ## Results on my local machine with westend-assethub state. | Setup | Batch size| Amortized cost of a key write(**ns**) | Amortized cost of a key read(**ns**)| |--------|--------|--------|--------| |Without TrieCache, Without PoV Recorder|1| 55_443|27_510| |Without TrieCache, With PoV Recorder|1|143_189|105_103| |With TrieCache, Without PoV Recorder|1|37_519|370| |With TrieCache, With PoV Recorder|1|42_569|7_309| |Without TrieCache, Without PoV Recorder|1000| 29_364|25_150| |Without TrieCache, With PoV Recorder|1000|33_221|107_349| |With TrieCache, Without PoV Recorder|1000|18_355|370| |With TrieCache, With PoV Recorder|1000|19_883|4_063| |Without TrieCache, Without PoV Recorder|10_000| 28_336|27_765| |Without TrieCache, With PoV Recorder|10_000|29_673|62_392| |With TrieCache, Without PoV Recorder|10_000|15_102|370| |With TrieCache, With PoV Recorder|10_000|16_461|4_124| |Without TrieCache, Without PoV Recorder|100_000| 18_935|27_151| |Without TrieCache, With PoV Recorder|100_000|19_681|48_393| |With TrieCache, Without PoV Recorder|100_000|12_569|362| |*With TrieCache, With PoV Recorder*, to be used|100_000|13_469|3_895| Fixes: #7535 ## Todo: - [x] Run this benchmarks on reference hardware on configuration variant closest to the production environment. --------- Signed-off-by: Alexandru Gheorghe <alexandru.gheorghe@parity.io> Co-authored-by: Bastian Köcher <git@kchr.de>
1 parent e21471f commit 989802a

File tree

13 files changed

+217
-33
lines changed

13 files changed

+217
-33
lines changed

cumulus/polkadot-omni-node/lib/src/common/command.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ where
155155
let partial = T::new_partial(&config).map_err(sc_cli::Error::Service)?;
156156
let db = partial.backend.expose_db();
157157
let storage = partial.backend.expose_storage();
158+
let shared_trie_cache = partial.backend.expose_shared_trie_cache();
158159

159-
cmd.run(config, partial.client, db, storage)
160+
cmd.run(config, partial.client, db, storage, shared_trie_cache)
160161
}
161162
}

cumulus/polkadot-omni-node/lib/src/tests/benchmark_storage_works.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ fn benchmark_storage(db: &str, runtime: &str, base_path: &Path) -> ExitStatus {
5353
.arg("--weight-path")
5454
.arg(base_path)
5555
.args(["--state-version", "0"])
56+
.args(["--batch-size", "1"])
5657
.args(["--warmups", "0"])
5758
.args(["--add", "100", "--mul", "1.2", "--metric", "p75"])
5859
.status()

polkadot/cli/src/command.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,8 +386,9 @@ pub fn run() -> Result<()> {
386386
let (client, backend, _, _) = polkadot_service::new_chain_ops(&mut config)?;
387387
let db = backend.expose_db();
388388
let storage = backend.expose_storage();
389+
let shared_trie_cache = backend.expose_shared_trie_cache();
389390

390-
cmd.run(config, client.clone(), db, storage).map_err(Error::SubstrateCli)
391+
cmd.run(config, client.clone(), db, storage, shared_trie_cache).map_err(Error::SubstrateCli)
391392
}),
392393
BenchmarkCmd::Block(cmd) => runner.sync_run(|mut config| {
393394
let (client, _, _, _) = polkadot_service::new_chain_ops(&mut config)?;

polkadot/tests/benchmark_storage_works.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ fn benchmark_storage(db: &str, base_path: &Path) -> ExitStatus {
4646
.arg("--weight-path")
4747
.arg(base_path)
4848
.args(["--state-version", "0"])
49+
.args(["--batch-size", "1"])
4950
.args(["--warmups", "0"])
5051
.args(["--add", "100", "--mul", "1.2", "--metric", "p75"])
5152
.status()

prdoc/pr_7867.prdoc

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
title: benchmark/storage Make read/write benchmarks more accurate
2+
3+
doc:
4+
- audience: [Runtime Dev, Node Dev]
5+
description: |-
6+
Improve the benchmark accuracy of read/write costs by making sure for both
7+
reads and write we compute the amortized cost of a single key operation, by adding
8+
a batch functionality to make sure the cost of common operations like root computation
9+
is spread across multiple keys. Additionally, also add a pov-recorder flag, so that we
10+
are able to replicate the same environment as parachains do.
11+
12+
crates:
13+
- name: sc-client-db
14+
bump: major
15+
- name: frame-benchmarking-cli
16+
bump: major
17+
- name: polkadot-cli
18+
bump: major
19+
- name: polkadot-omni-node-lib
20+
bump: major
21+
- name: polkadot
22+
bump: patch
23+

substrate/bin/node/cli/src/command.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,9 @@ pub fn run() -> Result<()> {
127127
let partial = new_partial(&config, None)?;
128128
let db = partial.backend.expose_db();
129129
let storage = partial.backend.expose_storage();
130+
let shared_trie_cache = partial.backend.expose_shared_trie_cache();
130131

131-
cmd.run(config, partial.client, db, storage)
132+
cmd.run(config, partial.client, db, storage, shared_trie_cache)
132133
},
133134
BenchmarkCmd::Overhead(cmd) => {
134135
// ensure that we keep the task manager alive

substrate/bin/node/cli/tests/benchmark_storage_works.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ fn benchmark_storage(db: &str, base_path: &Path) -> ExitStatus {
4747
.arg("--weight-path")
4848
.arg(base_path)
4949
.args(["--state-version", "1"])
50+
.args(["--batch-size", "1"])
5051
.args(["--warmups", "0"])
5152
.args(["--add", "100", "--mul", "1.2", "--metric", "p75"])
5253
.arg("--include-child-trees")

substrate/client/db/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,6 +1193,16 @@ impl<Block: BlockT> Backend<Block> {
11931193
self.storage.clone()
11941194
}
11951195

1196+
/// Expose the shared trie cache that is used by this backend.
1197+
///
1198+
/// Should only be needed for benchmarking.
1199+
#[cfg(feature = "runtime-benchmarks")]
1200+
pub fn expose_shared_trie_cache(
1201+
&self,
1202+
) -> Option<sp_trie::cache::SharedTrieCache<HashingFor<Block>>> {
1203+
self.shared_trie_cache.clone()
1204+
}
1205+
11961206
fn from_database(
11971207
db: Arc<dyn Database<DbHash>>,
11981208
canonicalization_delay: u64,

substrate/utils/frame/benchmarking-cli/src/storage/cmd.rs

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use sc_cli::{CliConfiguration, DatabaseParams, PruningParams, Result, SharedPara
1919
use sc_client_api::{Backend as ClientBackend, StorageProvider, UsageProvider};
2020
use sc_client_db::DbHash;
2121
use sc_service::Configuration;
22+
use sp_api::CallApiAt;
2223
use sp_blockchain::HeaderBackend;
2324
use sp_database::{ColumnId, Database};
2425
use sp_runtime::traits::{Block as BlockT, HashingFor};
@@ -116,6 +117,25 @@ pub struct StorageParams {
116117
/// Include child trees in benchmark.
117118
#[arg(long)]
118119
pub include_child_trees: bool,
120+
121+
/// Disable PoV recorder.
122+
///
123+
/// The recorder has impact on performance when benchmarking with the TrieCache enabled.
124+
/// If the chain is recording a proof while building/importing a block, the pov recorder
125+
/// should be activated.
126+
///
127+
/// Hence, when generating weights for a parachain this should be activated and when generating
128+
/// weights for a standalone chain this should be deactivated.
129+
#[arg(long, default_value = "false")]
130+
pub disable_pov_recorder: bool,
131+
132+
/// The batch size for the write benchmark.
133+
///
134+
/// Since the write size needs to also include the cost of computing the storage root, which is
135+
/// done once at the end of the block, the batch size is used to simulate multiple writes in a
136+
/// block.
137+
#[arg(long, default_value_t = 100_000)]
138+
pub batch_size: usize,
119139
}
120140

121141
impl StorageCmd {
@@ -127,11 +147,15 @@ impl StorageCmd {
127147
client: Arc<C>,
128148
db: (Arc<dyn Database<DbHash>>, ColumnId),
129149
storage: Arc<dyn Storage<HashingFor<Block>>>,
150+
shared_trie_cache: Option<sp_trie::cache::SharedTrieCache<HashingFor<Block>>>,
130151
) -> Result<()>
131152
where
132153
BA: ClientBackend<Block>,
133154
Block: BlockT<Hash = DbHash>,
134-
C: UsageProvider<Block> + StorageProvider<Block, BA> + HeaderBackend<Block>,
155+
C: UsageProvider<Block>
156+
+ StorageProvider<Block, BA>
157+
+ HeaderBackend<Block>
158+
+ CallApiAt<Block>,
135159
{
136160
let mut template = TemplateData::new(&cfg, &self.params)?;
137161

@@ -140,7 +164,7 @@ impl StorageCmd {
140164

141165
if !self.params.skip_read {
142166
self.bench_warmup(&client)?;
143-
let record = self.bench_read(client.clone())?;
167+
let record = self.bench_read(client.clone(), shared_trie_cache.clone())?;
144168
if let Some(path) = &self.params.json_read_path {
145169
record.save_json(&cfg, path, "read")?;
146170
}
@@ -151,7 +175,7 @@ impl StorageCmd {
151175

152176
if !self.params.skip_write {
153177
self.bench_warmup(&client)?;
154-
let record = self.bench_write(client, db, storage)?;
178+
let record = self.bench_write(client, db, storage, shared_trie_cache)?;
155179
if let Some(path) = &self.params.json_write_path {
156180
record.save_json(&cfg, path, "write")?;
157181
}
@@ -197,11 +221,31 @@ impl StorageCmd {
197221

198222
for i in 0..self.params.warmups {
199223
info!("Warmup round {}/{}", i + 1, self.params.warmups);
224+
let mut child_nodes = Vec::new();
225+
200226
for key in keys.as_slice() {
201227
let _ = client
202228
.storage(hash, &key)
203229
.expect("Checked above to exist")
204230
.ok_or("Value unexpectedly empty");
231+
232+
if let Some(info) = self
233+
.params
234+
.include_child_trees
235+
.then(|| self.is_child_key(key.clone().0))
236+
.flatten()
237+
{
238+
// child tree key
239+
for ck in client.child_storage_keys(hash, info.clone(), None, None)? {
240+
child_nodes.push((ck.clone(), info.clone()));
241+
}
242+
}
243+
}
244+
for (key, info) in child_nodes.as_slice() {
245+
let _ = client
246+
.child_storage(hash, info, key)
247+
.expect("Checked above to exist")
248+
.ok_or("Value unexpectedly empty")?;
205249
}
206250
}
207251

substrate/utils/frame/benchmarking-cli/src/storage/read.rs

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515
// See the License for the specific language governing permissions and
1616
// limitations under the License.
1717

18-
use sc_cli::Result;
19-
use sc_client_api::{Backend as ClientBackend, StorageProvider, UsageProvider};
20-
use sp_runtime::traits::{Block as BlockT, Header as HeaderT};
21-
2218
use log::info;
2319
use rand::prelude::*;
20+
use sc_cli::{Error, Result};
21+
use sc_client_api::{Backend as ClientBackend, StorageProvider, UsageProvider};
22+
use sp_api::CallApiAt;
23+
use sp_runtime::traits::{Block as BlockT, HashingFor, Header as HeaderT};
24+
use sp_state_machine::{backend::AsTrieBackend, Backend};
2425
use std::{fmt::Debug, sync::Arc, time::Instant};
2526

2627
use super::cmd::StorageCmd;
@@ -29,9 +30,13 @@ use crate::shared::{new_rng, BenchRecord};
2930
impl StorageCmd {
3031
/// Benchmarks the time it takes to read a single Storage item.
3132
/// Uses the latest state that is available for the given client.
32-
pub(crate) fn bench_read<B, BA, C>(&self, client: Arc<C>) -> Result<BenchRecord>
33+
pub(crate) fn bench_read<B, BA, C>(
34+
&self,
35+
client: Arc<C>,
36+
_shared_trie_cache: Option<sp_trie::cache::SharedTrieCache<HashingFor<B>>>,
37+
) -> Result<BenchRecord>
3338
where
34-
C: UsageProvider<B> + StorageProvider<B, BA>,
39+
C: UsageProvider<B> + StorageProvider<B, BA> + CallApiAt<B>,
3540
B: BlockT + Debug,
3641
BA: ClientBackend<B>,
3742
<<B as BlockT>::Header as HeaderT>::Number: From<u32>,
@@ -49,6 +54,19 @@ impl StorageCmd {
4954
// Interesting part here:
5055
// Read all the keys in the database and measure the time it takes to access each.
5156
info!("Reading {} keys", keys.len());
57+
58+
// Read using the same TrieBackend and recorder for up to `batch_size` keys.
59+
// This would allow us to measure the amortized cost of reading a key.
60+
let recorder = (!self.params.disable_pov_recorder).then(|| Default::default());
61+
let mut state = client
62+
.state_at(best_hash)
63+
.map_err(|_err| Error::Input("State not found".into()))?;
64+
let mut as_trie_backend = state.as_trie_backend();
65+
let mut backend = sp_state_machine::TrieBackendBuilder::wrap(&as_trie_backend)
66+
.with_optional_recorder(recorder)
67+
.build();
68+
let mut read_in_batch = 0;
69+
5270
for key in keys.as_slice() {
5371
match (self.params.include_child_trees, self.is_child_key(key.clone().0)) {
5472
(true, Some(info)) => {
@@ -60,13 +78,31 @@ impl StorageCmd {
6078
_ => {
6179
// regular key
6280
let start = Instant::now();
63-
let v = client
64-
.storage(best_hash, &key)
81+
82+
let v = backend
83+
.storage(key.0.as_ref())
6584
.expect("Checked above to exist")
6685
.ok_or("Value unexpectedly empty")?;
67-
record.append(v.0.len(), start.elapsed())?;
86+
record.append(v.len(), start.elapsed())?;
6887
},
6988
}
89+
read_in_batch += 1;
90+
if read_in_batch >= self.params.batch_size {
91+
// Using a new recorder for every read vs using the same for the entire batch
92+
// produces significant different results. Since in the real use case we use a
93+
// single recorder per block, simulate the same behavior by creating a new
94+
// recorder every batch size, so that the amortized cost of reading a key is
95+
// measured in conditions closer to the real world.
96+
let recorder = (!self.params.disable_pov_recorder).then(|| Default::default());
97+
state = client
98+
.state_at(best_hash)
99+
.map_err(|_err| Error::Input("State not found".to_string()))?;
100+
as_trie_backend = state.as_trie_backend();
101+
backend = sp_state_machine::TrieBackendBuilder::wrap(&as_trie_backend)
102+
.with_optional_recorder(recorder)
103+
.build();
104+
read_in_batch = 0;
105+
}
70106
}
71107

72108
if self.params.include_child_trees {
@@ -75,11 +111,29 @@ impl StorageCmd {
75111
info!("Reading {} child keys", child_nodes.len());
76112
for (key, info) in child_nodes.as_slice() {
77113
let start = Instant::now();
78-
let v = client
79-
.child_storage(best_hash, info, key)
114+
let v = backend
115+
.child_storage(info, key.0.as_ref())
80116
.expect("Checked above to exist")
81117
.ok_or("Value unexpectedly empty")?;
82-
record.append(v.0.len(), start.elapsed())?;
118+
record.append(v.len(), start.elapsed())?;
119+
120+
read_in_batch += 1;
121+
if read_in_batch >= self.params.batch_size {
122+
// Using a new recorder for every read vs using the same for the entire batch
123+
// produces significant different results. Since in the real use case we use a
124+
// single recorder per block, simulate the same behavior by creating a new
125+
// recorder every batch size, so that the amortized cost of reading a key is
126+
// measured in conditions closer to the real world.
127+
let recorder = (!self.params.disable_pov_recorder).then(|| Default::default());
128+
state = client
129+
.state_at(best_hash)
130+
.map_err(|_err| Error::Input("State not found".to_string()))?;
131+
as_trie_backend = state.as_trie_backend();
132+
backend = sp_state_machine::TrieBackendBuilder::wrap(&as_trie_backend)
133+
.with_optional_recorder(recorder)
134+
.build();
135+
read_in_batch = 0;
136+
}
83137
}
84138
}
85139
Ok(record)

0 commit comments

Comments
 (0)