subsystem-bench: add regression tests for availability read and write (#3311)

### What's been done
- `subsystem-bench` has been split into two parts: a cli benchmark
runner and a library.
- The cli runner is quite simple. It just allows us to run `.yaml` based
test sequences. Now it should only be used to run benchmarks during
development.
- The library is used in the cli runner and in regression tests. Some
code is changed to make the library independent of the runner.
- Added first regression tests for availability read and write that
replicate existing test sequences.

### How we run regression tests
- Regression tests are simply rust integration tests without the
harnesses.
- They should only be compiled under the `subsystem-benchmarks` feature
to prevent them from running with other tests.
- This doesn't work when running tests with `nextest` in CI, so
additional filters have been added to the `nextest` runs.
- Each benchmark run takes a different time in the beginning, so we
"warm up" the tests until their CPU usage differs by only 1%.
- After the warm-up, we run the benchmarks a few more times and compare
the average with the exception using a precision.

### What is still wrong?
- I haven't managed to set up approval voting tests. The spread of their
results is too large and can't be narrowed down in a reasonable amount
of time in the warm-up phase.
- The tests start an unconfigurable prometheus endpoint inside, which
causes errors because they use the same 9999 port. I disable it with a
flag, but I think it's better to extract the endpoint launching outside
the test, as we already do with `valgrind` and `pyroscope`. But we still
use `prometheus` inside the tests.

### Future work
* https://github.com/paritytech/polkadot-sdk/issues/3528
* https://github.com/paritytech/polkadot-sdk/issues/3529
* https://github.com/paritytech/polkadot-sdk/issues/3530
* https://github.com/paritytech/polkadot-sdk/issues/3531

---------

Co-authored-by: Alexander Samusev <41779041+alvicsam@users.noreply.github.com>
This commit is contained in:
Andrei Eres
2024-03-01 15:30:43 +01:00
committed by GitHub
parent 6f81a4a092
commit f0e589d72e
35 changed files with 712 additions and 412 deletions
+2 -1
View File
@@ -25,6 +25,7 @@ test-linux-stable:
# "upgrade_version_checks_should_work" is currently failing
- |
time cargo nextest run \
--filter-expr 'not deps(/polkadot-subsystem-bench/)' \
--workspace \
--locked \
--release \
@@ -69,7 +70,7 @@ test-linux-stable-runtime-benchmarks:
# but still want to have debug assertions.
RUSTFLAGS: "-Cdebug-assertions=y -Dwarnings"
script:
- time cargo nextest run --workspace --features runtime-benchmarks benchmark --locked --cargo-profile testnet
- time cargo nextest run --filter-expr 'not deps(/polkadot-subsystem-bench/)' --workspace --features runtime-benchmarks benchmark --locked --cargo-profile testnet
# can be used to run all tests
# test-linux-stable-all:
Generated
+2
View File
@@ -12269,6 +12269,7 @@ dependencies = [
"polkadot-node-subsystem-util",
"polkadot-primitives",
"polkadot-primitives-test-helpers",
"polkadot-subsystem-bench",
"rand",
"sc-network",
"schnellru",
@@ -12300,6 +12301,7 @@ dependencies = [
"polkadot-node-subsystem-util",
"polkadot-primitives",
"polkadot-primitives-test-helpers",
"polkadot-subsystem-bench",
"rand",
"sc-network",
"schnellru",
@@ -36,3 +36,14 @@ sc-network = { path = "../../../../substrate/client/network" }
futures-timer = "3.0.2"
assert_matches = "1.4.0"
polkadot-primitives-test-helpers = { path = "../../../primitives/test-helpers" }
polkadot-subsystem-bench = { path = "../../subsystem-bench" }
[[test]]
name = "availability-distribution-regression-bench"
path = "tests/availability-distribution-regression-bench.rs"
harness = false
required-features = ["subsystem-benchmarks"]
[features]
subsystem-benchmarks = []
@@ -0,0 +1,113 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! availability-read regression tests
//!
//! TODO: Explain the test case after configuration adjusted to Kusama
//!
//! Subsystems involved:
//! - availability-distribution
//! - bitfield-distribution
//! - availability-store
use polkadot_subsystem_bench::{
availability::{benchmark_availability_write, prepare_test, TestDataAvailability, TestState},
configuration::{PeerLatency, TestConfiguration},
usage::BenchmarkUsage,
};
const BENCH_COUNT: usize = 3;
const WARM_UP_COUNT: usize = 20;
const WARM_UP_PRECISION: f64 = 0.01;
fn main() -> Result<(), String> {
let mut messages = vec![];
// TODO: Adjust the test configurations to Kusama values
let mut config = TestConfiguration::default();
config.latency = Some(PeerLatency { mean_latency_ms: 30, std_dev: 2.0 });
config.n_validators = 1000;
config.n_cores = 200;
config.max_validators_per_core = 5;
config.min_pov_size = 5120;
config.max_pov_size = 5120;
config.peer_bandwidth = 52428800;
config.bandwidth = 52428800;
config.connectivity = 75;
config.num_blocks = 3;
config.generate_pov_sizes();
warm_up(config.clone())?;
let usage = benchmark(config.clone());
messages.extend(usage.check_network_usage(&[
("Received from peers", 4330.0, 0.05),
("Sent to peers", 15900.0, 0.05),
]));
messages.extend(usage.check_cpu_usage(&[
("availability-distribution", 0.025, 0.05),
("bitfield-distribution", 0.085, 0.05),
("availability-store", 0.180, 0.05),
]));
if messages.is_empty() {
Ok(())
} else {
eprintln!("{}", messages.join("\n"));
Err("Regressions found".to_string())
}
}
fn warm_up(config: TestConfiguration) -> Result<(), String> {
println!("Warming up...");
let mut prev_run: Option<BenchmarkUsage> = None;
for _ in 0..WARM_UP_COUNT {
let curr = run(config.clone());
if let Some(ref prev) = prev_run {
let av_distr_diff =
curr.cpu_usage_diff(prev, "availability-distribution").expect("Must exist");
let bitf_distr_diff =
curr.cpu_usage_diff(prev, "bitfield-distribution").expect("Must exist");
let av_store_diff =
curr.cpu_usage_diff(prev, "availability-store").expect("Must exist");
if av_distr_diff < WARM_UP_PRECISION &&
bitf_distr_diff < WARM_UP_PRECISION &&
av_store_diff < WARM_UP_PRECISION
{
return Ok(())
}
}
prev_run = Some(curr);
}
Err("Can't warm up".to_string())
}
fn benchmark(config: TestConfiguration) -> BenchmarkUsage {
println!("Benchmarking...");
let usages: Vec<BenchmarkUsage> = (0..BENCH_COUNT).map(|_| run(config.clone())).collect();
let usage = BenchmarkUsage::average(&usages);
println!("{}", usage);
usage
}
fn run(config: TestConfiguration) -> BenchmarkUsage {
let mut state = TestState::new(&config);
let (mut env, _protocol_config) =
prepare_test(config.clone(), &mut state, TestDataAvailability::Write, false);
env.runtime()
.block_on(benchmark_availability_write("data_availability_write", &mut env, state))
}
@@ -41,6 +41,13 @@ sc-network = { path = "../../../../substrate/client/network" }
polkadot-node-subsystem-test-helpers = { path = "../../subsystem-test-helpers" }
polkadot-primitives-test-helpers = { path = "../../../primitives/test-helpers" }
polkadot-subsystem-bench = { path = "../../subsystem-bench" }
[[test]]
name = "availability-recovery-regression-bench"
path = "tests/availability-recovery-regression-bench.rs"
harness = false
required-features = ["subsystem-benchmarks"]
[features]
subsystem-benchmarks = []
@@ -0,0 +1,103 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! availability-write regression tests
//!
//! TODO: Explain the test case after configuration adjusted to Kusama
//!
//! Subsystems involved:
//! - availability-recovery
use polkadot_subsystem_bench::{
availability::{
benchmark_availability_read, prepare_test, DataAvailabilityReadOptions,
TestDataAvailability, TestState,
},
configuration::{PeerLatency, TestConfiguration},
usage::BenchmarkUsage,
};
const BENCH_COUNT: usize = 3;
const WARM_UP_COUNT: usize = 10;
const WARM_UP_PRECISION: f64 = 0.01;
fn main() -> Result<(), String> {
let mut messages = vec![];
// TODO: Adjust the test configurations to Kusama values
let options = DataAvailabilityReadOptions { fetch_from_backers: true };
let mut config = TestConfiguration::default();
config.latency = Some(PeerLatency { mean_latency_ms: 100, std_dev: 1.0 });
config.n_validators = 300;
config.n_cores = 20;
config.min_pov_size = 5120;
config.max_pov_size = 5120;
config.peer_bandwidth = 52428800;
config.bandwidth = 52428800;
config.num_blocks = 3;
config.connectivity = 90;
config.generate_pov_sizes();
warm_up(config.clone(), options.clone())?;
let usage = benchmark(config.clone(), options.clone());
messages.extend(usage.check_network_usage(&[
("Received from peers", 102400.000, 0.05),
("Sent to peers", 0.335, 0.05),
]));
messages.extend(usage.check_cpu_usage(&[("availability-recovery", 3.850, 0.05)]));
if messages.is_empty() {
Ok(())
} else {
eprintln!("{}", messages.join("\n"));
Err("Regressions found".to_string())
}
}
fn warm_up(config: TestConfiguration, options: DataAvailabilityReadOptions) -> Result<(), String> {
println!("Warming up...");
let mut prev_run: Option<BenchmarkUsage> = None;
for _ in 0..WARM_UP_COUNT {
let curr = run(config.clone(), options.clone());
if let Some(ref prev) = prev_run {
let diff = curr.cpu_usage_diff(prev, "availability-recovery").expect("Must exist");
if diff < WARM_UP_PRECISION {
return Ok(())
}
}
prev_run = Some(curr);
}
Err("Can't warm up".to_string())
}
fn benchmark(config: TestConfiguration, options: DataAvailabilityReadOptions) -> BenchmarkUsage {
println!("Benchmarking...");
let usages: Vec<BenchmarkUsage> =
(0..BENCH_COUNT).map(|_| run(config.clone(), options.clone())).collect();
let usage = BenchmarkUsage::average(&usages);
println!("{}", usage);
usage
}
fn run(config: TestConfiguration, options: DataAvailabilityReadOptions) -> BenchmarkUsage {
let mut state = TestState::new(&config);
let (mut env, _protocol_config) =
prepare_test(config.clone(), &mut state, TestDataAvailability::Read(options), false);
env.runtime()
.block_on(benchmark_availability_read("data_availability_read", &mut env, state))
}
+5 -1
View File
@@ -8,9 +8,13 @@ license.workspace = true
readme = "README.md"
publish = false
[lib]
name = "polkadot_subsystem_bench"
path = "src/lib/lib.rs"
[[bin]]
name = "subsystem-bench"
path = "src/subsystem-bench.rs"
path = "src/cli/subsystem-bench.rs"
# Prevent rustdoc error. Already documented from top-level Cargo.toml.
doc = false
+74 -76
View File
@@ -1,6 +1,6 @@
# Subsystem benchmark client
Run parachain consensus stress and performance tests on your development machine.
Run parachain consensus stress and performance tests on your development machine or in CI.
## Motivation
@@ -32,7 +32,8 @@ a local Grafana/Prometheus stack is needed.
### Run Prometheus, Pyroscope and Graphana in Docker
If docker is not usable, then follow the next sections to manually install Prometheus, Pyroscope and Graphana on your machine.
If docker is not usable, then follow the next sections to manually install Prometheus, Pyroscope and Graphana
on your machine.
```bash
cd polkadot/node/subsystem-bench/docker
@@ -95,39 +96,16 @@ If you are running the servers in Docker, use the following URLs:
Follow [this guide](https://grafana.com/docs/grafana/latest/dashboards/manage-dashboards/#export-and-import-dashboards)
to import the dashboards from the repository `grafana` folder.
## How to run a test
To run a test, you need to first choose a test objective. Currently, we support the following:
```
target/testnet/subsystem-bench --help
The almighty Subsystem Benchmark Tool™️
Usage: subsystem-bench [OPTIONS] <COMMAND>
Commands:
data-availability-read Benchmark availability recovery strategies
```
Note: `test-sequence` is a special test objective that wraps up an arbitrary number of test objectives. It is tipically
used to run a suite of tests defined in a `yaml` file like in this [example](examples/availability_read.yaml).
### Standard test options
```
--network <NETWORK> The type of network to be emulated [default: ideal] [possible values: ideal, healthy,
degraded]
--n-cores <N_CORES> Number of cores to fetch availability for [default: 100]
--n-validators <N_VALIDATORS> Number of validators to fetch chunks from [default: 500]
--min-pov-size <MIN_POV_SIZE> The minimum pov size in KiB [default: 5120]
--max-pov-size <MAX_POV_SIZE> The maximum pov size bytes [default: 5120]
-n, --num-blocks <NUM_BLOCKS> The number of blocks the test is going to run [default: 1]
-p, --peer-bandwidth <PEER_BANDWIDTH> The bandwidth of emulated remote peers in KiB
-b, --bandwidth <BANDWIDTH> The bandwidth of our node in KiB
--connectivity <CONNECTIVITY> Emulated peer connection ratio [0-100]
--peer-mean-latency <PEER_MEAN_LATENCY> Mean remote peer latency in milliseconds [0-5000]
--peer-latency-std-dev <PEER_LATENCY_STD_DEV> Remote peer latency standard deviation
$ subsystem-bench --help
Usage: subsystem-bench [OPTIONS] <PATH>
Arguments:
<PATH> Path to the test sequence configuration file
Options:
--profile Enable CPU Profiling with Pyroscope
--pyroscope-url <PYROSCOPE_URL> Pyroscope Server URL [default: http://localhost:4040]
--pyroscope-sample-rate <PYROSCOPE_SAMPLE_RATE> Pyroscope Sample Rate [default: 113]
@@ -135,27 +113,17 @@ Note: `test-sequence` is a special test objective that wraps up an arbitrary num
-h, --help Print help
```
These apply to all test objectives, except `test-sequence` which relies on the values being specified in a file.
## How to run a test
### Test objectives
Each test objective can have it's specific configuration options, in contrast with the standard test options.
For `data-availability-read` the recovery strategy to be used is configurable.
To run a test, you need to use a path to a test objective:
```
target/testnet/subsystem-bench data-availability-read --help
Benchmark availability recovery strategies
Usage: subsystem-bench data-availability-read [OPTIONS]
Options:
-f, --fetch-from-backers Turbo boost AD Read by fetching the full availability datafrom backers first. Saves CPU
as we don't need to re-construct from chunks. Tipically this is only faster if nodes
have enough bandwidth
-h, --help Print help
target/testnet/subsystem-bench polkadot/node/subsystem-bench/examples/availability_read.yaml
```
Note: test objectives may be wrapped up into a test sequence.
It is tipically used to run a suite of tests like in this [example](examples/availability_read.yaml).
### Understanding the test configuration
A single test configuration `TestConfiguration` struct applies to a single run of a certain test objective.
@@ -175,36 +143,65 @@ the test is started.
### Example run
Let's run an availabilty read test which will recover availability for 10 cores with max PoV size on a 500
Let's run an availabilty read test which will recover availability for 200 cores with max PoV size on a 1000
node validator network.
<!-- markdownlint-disable line-length -->
```
target/testnet/subsystem-bench --n-cores 10 data-availability-read
[2023-11-28T09:01:59Z INFO subsystem_bench::core::display] n_validators = 500, n_cores = 10, pov_size = 5120 - 5120,
latency = None
[2023-11-28T09:01:59Z INFO subsystem-bench::availability] Generating template candidate index=0 pov_size=5242880
[2023-11-28T09:01:59Z INFO subsystem-bench::availability] Created test environment.
[2023-11-28T09:01:59Z INFO subsystem-bench::availability] Pre-generating 10 candidates.
[2023-11-28T09:02:01Z INFO subsystem-bench::core] Initializing network emulation for 500 peers.
[2023-11-28T09:02:01Z INFO substrate_prometheus_endpoint] 〽️ Prometheus exporter started at 127.0.0.1:9999
[2023-11-28T09:02:01Z INFO subsystem-bench::availability] Current block 1/1
[2023-11-28T09:02:01Z INFO subsystem_bench::availability] 10 recoveries pending
[2023-11-28T09:02:04Z INFO subsystem_bench::availability] Block time 3231ms
[2023-11-28T09:02:04Z INFO subsystem-bench::availability] Sleeping till end of block (2768ms)
[2023-11-28T09:02:07Z INFO subsystem_bench::availability] All blocks processed in 6001ms
[2023-11-28T09:02:07Z INFO subsystem_bench::availability] Throughput: 51200 KiB/block
[2023-11-28T09:02:07Z INFO subsystem_bench::availability] Block time: 6001 ms
[2023-11-28T09:02:07Z INFO subsystem_bench::availability]
Total received from network: 66 MiB
Total sent to network: 58 KiB
Total subsystem CPU usage 4.16s
CPU usage per block 4.16s
Total test environment CPU usage 0.00s
CPU usage per block 0.00s
target/testnet/subsystem-bench polkadot/node/subsystem-bench/examples/availability_write.yaml
[2024-02-19T14:10:32.981Z INFO subsystem_bench] Sequence contains 1 step(s)
[2024-02-19T14:10:32.981Z INFO subsystem-bench::cli] Step 1/1
[2024-02-19T14:10:32.981Z INFO subsystem-bench::cli] [objective = DataAvailabilityWrite] n_validators = 1000, n_cores = 200, pov_size = 5120 - 5120, connectivity = 75, latency = Some(PeerLatency { mean_latency_ms: 30, std_dev: 2.0 })
[2024-02-19T14:10:32.982Z INFO subsystem-bench::availability] Generating template candidate index=0 pov_size=5242880
[2024-02-19T14:10:33.106Z INFO subsystem-bench::availability] Created test environment.
[2024-02-19T14:10:33.106Z INFO subsystem-bench::availability] Pre-generating 600 candidates.
[2024-02-19T14:10:34.096Z INFO subsystem-bench::network] Initializing emulation for a 1000 peer network.
[2024-02-19T14:10:34.096Z INFO subsystem-bench::network] connectivity 75%, latency Some(PeerLatency { mean_latency_ms: 30, std_dev: 2.0 })
[2024-02-19T14:10:34.098Z INFO subsystem-bench::network] Network created, connected validator count 749
[2024-02-19T14:10:34.099Z INFO subsystem-bench::availability] Seeding availability store with candidates ...
[2024-02-19T14:10:34.100Z INFO substrate_prometheus_endpoint] 〽️ Prometheus exporter started at 127.0.0.1:9999
[2024-02-19T14:10:34.387Z INFO subsystem-bench::availability] Done
[2024-02-19T14:10:34.387Z INFO subsystem-bench::availability] Current block #1
[2024-02-19T14:10:34.389Z INFO subsystem-bench::availability] Waiting for all emulated peers to receive their chunk from us ...
[2024-02-19T14:10:34.625Z INFO subsystem-bench::availability] All chunks received in 237ms
[2024-02-19T14:10:34.626Z INFO polkadot_subsystem_bench::availability] Waiting for 749 bitfields to be received and processed
[2024-02-19T14:10:35.710Z INFO subsystem-bench::availability] All bitfields processed
[2024-02-19T14:10:35.710Z INFO subsystem-bench::availability] All work for block completed in 1322ms
[2024-02-19T14:10:35.710Z INFO subsystem-bench::availability] Current block #2
[2024-02-19T14:10:35.712Z INFO subsystem-bench::availability] Waiting for all emulated peers to receive their chunk from us ...
[2024-02-19T14:10:35.947Z INFO subsystem-bench::availability] All chunks received in 236ms
[2024-02-19T14:10:35.947Z INFO polkadot_subsystem_bench::availability] Waiting for 749 bitfields to be received and processed
[2024-02-19T14:10:37.038Z INFO subsystem-bench::availability] All bitfields processed
[2024-02-19T14:10:37.038Z INFO subsystem-bench::availability] All work for block completed in 1328ms
[2024-02-19T14:10:37.039Z INFO subsystem-bench::availability] Current block #3
[2024-02-19T14:10:37.040Z INFO subsystem-bench::availability] Waiting for all emulated peers to receive their chunk from us ...
[2024-02-19T14:10:37.276Z INFO subsystem-bench::availability] All chunks received in 237ms
[2024-02-19T14:10:37.276Z INFO polkadot_subsystem_bench::availability] Waiting for 749 bitfields to be received and processed
[2024-02-19T14:10:38.362Z INFO subsystem-bench::availability] All bitfields processed
[2024-02-19T14:10:38.362Z INFO subsystem-bench::availability] All work for block completed in 1323ms
[2024-02-19T14:10:38.362Z INFO subsystem-bench::availability] All blocks processed in 3974ms
[2024-02-19T14:10:38.362Z INFO subsystem-bench::availability] Avg block time: 1324 ms
[2024-02-19T14:10:38.362Z INFO parachain::availability-store] received `Conclude` signal, exiting
[2024-02-19T14:10:38.362Z INFO parachain::bitfield-distribution] Conclude
[2024-02-19T14:10:38.362Z INFO subsystem-bench::network] Downlink channel closed, network interface task exiting
polkadot/node/subsystem-bench/examples/availability_write.yaml #1 DataAvailabilityWrite
Network usage, KiB total per block
Received from peers 12922.000 4307.333
Sent to peers 47705.000 15901.667
CPU usage, seconds total per block
availability-distribution 0.045 0.015
bitfield-distribution 0.104 0.035
availability-store 0.304 0.101
Test environment 3.213 1.071
```
`Block time` in the context of `data-availability-read` has a different meaning. It measures the amount of time it
<!-- markdownlint-enable line-length -->
`Block time` in the current context has a different meaning. It measures the amount of time it
took the subsystem to finish processing all of the messages sent in the context of the current test block.
### Test logs
@@ -233,8 +230,9 @@ Since the execution will be very slow, it's recommended not to run it together w
benchmark results into account. A report is saved in a file `cachegrind_report.txt`.
Example run results:
```
$ target/testnet/subsystem-bench --n-cores 10 --cache-misses data-availability-read
$ target/testnet/subsystem-bench --cache-misses cache-misses-data-availability-read.yaml
$ cat cachegrind_report.txt
I refs: 64,622,081,485
I1 misses: 3,018,168
@@ -275,7 +273,7 @@ happy and negative scenarios (low bandwidth, network errors and low connectivity
To faster write a new test objective you need to use some higher level wrappers and logic: `TestEnvironment`,
`TestConfiguration`, `TestAuthorities`, `NetworkEmulator`. To create the `TestEnvironment` you will
need to also build an `Overseer`, but that should be easy using the mockups for subsystems in`core::mock`.
need to also build an `Overseer`, but that should be easy using the mockups for subsystems in `mock`.
### Mocking
@@ -1,14 +1,14 @@
TestConfiguration:
# Test 1
- objective: !ApprovalVoting
last_considered_tranche: 89
coalesce_mean: 3.0
coalesce_std_dev: 1.0
enable_assignments_v2: true
last_considered_tranche: 89
stop_when_approved: true
coalesce_tranche_diff: 12
workdir_prefix: "/tmp/"
enable_assignments_v2: true
num_no_shows_per_candidate: 10
workdir_prefix: "/tmp/"
n_validators: 500
n_cores: 100
min_pov_size: 1120
@@ -7,11 +7,10 @@ TestConfiguration:
last_considered_tranche: 89
stop_when_approved: false
coalesce_tranche_diff: 12
workdir_prefix: "/tmp"
num_no_shows_per_candidate: 0
workdir_prefix: "/tmp"
n_validators: 500
n_cores: 100
n_included_candidates: 100
min_pov_size: 1120
max_pov_size: 5120
peer_bandwidth: 524288000000
@@ -7,8 +7,8 @@ TestConfiguration:
last_considered_tranche: 89
stop_when_approved: true
coalesce_tranche_diff: 12
workdir_prefix: "/tmp/"
num_no_shows_per_candidate: 0
workdir_prefix: "/tmp/"
n_validators: 500
n_cores: 100
min_pov_size: 1120
@@ -7,8 +7,8 @@ TestConfiguration:
last_considered_tranche: 89
stop_when_approved: false
coalesce_tranche_diff: 12
workdir_prefix: "/tmp/"
num_no_shows_per_candidate: 0
workdir_prefix: "/tmp/"
n_validators: 500
n_cores: 100
min_pov_size: 1120
@@ -1,37 +0,0 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use serde::{Deserialize, Serialize};
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq)]
#[value(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum NetworkEmulation {
Ideal,
Healthy,
Degraded,
}
#[derive(Debug, Clone, Serialize, Deserialize, clap::Parser)]
#[clap(rename_all = "kebab-case")]
#[allow(missing_docs)]
pub struct DataAvailabilityReadOptions {
#[clap(short, long, default_value_t = false)]
/// Turbo boost AD Read by fetching the full availability datafrom backers first. Saves CPU as
/// we don't need to re-construct from chunks. Tipically this is only faster if nodes have
/// enough bandwidth.
pub fetch_from_backers: bool,
}
@@ -14,54 +14,32 @@
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! A tool for running subsystem benchmark tests designed for development and
//! CI regression testing.
//! A tool for running subsystem benchmark tests
//! designed for development and CI regression testing.
use approval::{bench_approvals, ApprovalsOptions};
use availability::{
cli::{DataAvailabilityReadOptions, NetworkEmulation},
prepare_test, TestState,
};
use clap::Parser;
use clap_num::number_range;
use color_eyre::eyre;
use colored::Colorize;
use core::{
configuration::TestConfiguration,
display::display_configuration,
environment::{TestEnvironment, GENESIS_HASH},
};
use polkadot_subsystem_bench::{approval, availability, configuration};
use pyroscope::PyroscopeAgent;
use pyroscope_pprofrs::{pprof_backend, PprofConfig};
use serde::{Deserialize, Serialize};
use std::path::Path;
mod approval;
mod availability;
mod core;
mod valgrind;
const LOG_TARGET: &str = "subsystem-bench";
fn le_100(s: &str) -> Result<usize, String> {
number_range(s, 0, 100)
}
fn le_5000(s: &str) -> Result<usize, String> {
number_range(s, 0, 5000)
}
const LOG_TARGET: &str = "subsystem-bench::cli";
/// Supported test objectives
#[derive(Debug, Clone, Parser, Serialize, Deserialize)]
#[command(rename_all = "kebab-case")]
pub enum TestObjective {
/// Benchmark availability recovery strategies.
DataAvailabilityRead(DataAvailabilityReadOptions),
DataAvailabilityRead(availability::DataAvailabilityReadOptions),
/// Benchmark availability and bitfield distribution.
DataAvailabilityWrite,
/// Benchmark the approval-voting and approval-distribution subsystems.
ApprovalVoting(ApprovalsOptions),
Unimplemented,
ApprovalVoting(approval::ApprovalsOptions),
}
impl std::fmt::Display for TestObjective {
@@ -73,39 +51,37 @@ impl std::fmt::Display for TestObjective {
Self::DataAvailabilityRead(_) => "DataAvailabilityRead",
Self::DataAvailabilityWrite => "DataAvailabilityWrite",
Self::ApprovalVoting(_) => "ApprovalVoting",
Self::Unimplemented => "Unimplemented",
}
)
}
}
/// The test input parameters
#[derive(Clone, Debug, Serialize, Deserialize)]
struct CliTestConfiguration {
/// Test Objective
pub objective: TestObjective,
/// Test Configuration
#[serde(flatten)]
pub test_config: configuration::TestConfiguration,
}
#[derive(Serialize, Deserialize)]
pub struct TestSequence {
#[serde(rename(serialize = "TestConfiguration", deserialize = "TestConfiguration"))]
test_configurations: Vec<CliTestConfiguration>,
}
impl TestSequence {
fn new_from_file(path: &Path) -> std::io::Result<TestSequence> {
let string = String::from_utf8(std::fs::read(path)?).expect("File is valid UTF8");
Ok(serde_yaml::from_str(&string).expect("File is valid test sequence YA"))
}
}
#[derive(Debug, Parser)]
#[allow(missing_docs)]
struct BenchCli {
#[arg(long, value_enum, ignore_case = true, default_value_t = NetworkEmulation::Ideal)]
/// The type of network to be emulated
pub network: NetworkEmulation,
#[clap(short, long)]
/// The bandwidth of emulated remote peers in KiB
pub peer_bandwidth: Option<usize>,
#[clap(short, long)]
/// The bandwidth of our node in KiB
pub bandwidth: Option<usize>,
#[clap(long, value_parser=le_100)]
/// Emulated peer connection ratio [0-100].
pub connectivity: Option<usize>,
#[clap(long, value_parser=le_5000)]
/// Mean remote peer latency in milliseconds [0-5000].
pub peer_mean_latency: Option<usize>,
#[clap(long, value_parser=le_5000)]
/// Remote peer latency standard deviation
pub peer_latency_std_dev: Option<f64>,
#[clap(long, default_value_t = false)]
/// Enable CPU Profiling with Pyroscope
pub profile: bool,
@@ -122,10 +98,6 @@ struct BenchCli {
/// Enable Cache Misses Profiling with Valgrind. Linux only, Valgrind must be in the PATH
pub cache_misses: bool,
#[clap(long, default_value_t = false)]
/// Shows the output in YAML format
pub yaml_output: bool,
#[arg(required = true)]
/// Path to the test sequence configuration file
pub path: String,
@@ -148,49 +120,60 @@ impl BenchCli {
None
};
let test_sequence = core::configuration::TestSequence::new_from_file(Path::new(&self.path))
let test_sequence = TestSequence::new_from_file(Path::new(&self.path))
.expect("File exists")
.into_vec();
.test_configurations;
let num_steps = test_sequence.len();
gum::info!("{}", format!("Sequence contains {} step(s)", num_steps).bright_purple());
for (index, test_config) in test_sequence.into_iter().enumerate() {
let benchmark_name = format!("{} #{} {}", &self.path, index + 1, test_config.objective);
gum::info!(target: LOG_TARGET, "{}", format!("Step {}/{}", index + 1, num_steps).bright_purple(),);
display_configuration(&test_config);
let usage = match test_config.objective {
TestObjective::DataAvailabilityRead(ref _opts) => {
let mut state = TestState::new(&test_config);
let (mut env, _protocol_config) = prepare_test(test_config, &mut state);
for (index, CliTestConfiguration { objective, mut test_config }) in
test_sequence.into_iter().enumerate()
{
let benchmark_name = format!("{} #{} {}", &self.path, index + 1, objective);
gum::info!(target: LOG_TARGET, "{}", format!("Step {}/{}", index + 1, num_steps).bright_purple(),);
gum::info!(target: LOG_TARGET, "[{}] {}", format!("objective = {:?}", objective).green(), test_config);
test_config.generate_pov_sizes();
let usage = match objective {
TestObjective::DataAvailabilityRead(opts) => {
let mut state = availability::TestState::new(&test_config);
let (mut env, _protocol_config) = availability::prepare_test(
test_config,
&mut state,
availability::TestDataAvailability::Read(opts),
true,
);
env.runtime().block_on(availability::benchmark_availability_read(
&benchmark_name,
&mut env,
state,
))
},
TestObjective::DataAvailabilityWrite => {
let mut state = availability::TestState::new(&test_config);
let (mut env, _protocol_config) = availability::prepare_test(
test_config,
&mut state,
availability::TestDataAvailability::Write,
true,
);
env.runtime().block_on(availability::benchmark_availability_write(
&benchmark_name,
&mut env,
state,
))
},
TestObjective::ApprovalVoting(ref options) => {
let (mut env, state) =
approval::prepare_test(test_config.clone(), options.clone());
env.runtime().block_on(bench_approvals(&benchmark_name, &mut env, state))
},
TestObjective::DataAvailabilityWrite => {
let mut state = TestState::new(&test_config);
let (mut env, _protocol_config) = prepare_test(test_config, &mut state);
env.runtime().block_on(availability::benchmark_availability_write(
approval::prepare_test(test_config.clone(), options.clone(), true);
env.runtime().block_on(approval::bench_approvals(
&benchmark_name,
&mut env,
state,
))
},
TestObjective::Unimplemented => todo!(),
};
let output = if self.yaml_output {
serde_yaml::to_string(&vec![usage])?
} else {
usage.to_string()
};
println!("{}", output);
println!("{}", usage);
}
if let Some(agent_running) = agent_running {
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use crate::core::configuration::TestAuthorities;
use crate::configuration::TestAuthorities;
use itertools::Itertools;
use polkadot_node_core_approval_voting::time::{Clock, SystemClock, Tick};
use polkadot_node_network_protocol::{
@@ -18,15 +18,12 @@ use crate::{
approval::{
helpers::{generate_babe_epoch, generate_topology},
test_message::{MessagesBundle, TestMessageInfo},
ApprovalTestState, BlockTestData, GeneratedState, BUFFER_FOR_GENERATION_MILLIS, LOG_TARGET,
SLOT_DURATION_MILLIS,
ApprovalTestState, ApprovalsOptions, BlockTestData, GeneratedState,
BUFFER_FOR_GENERATION_MILLIS, LOG_TARGET, SLOT_DURATION_MILLIS,
},
core::{
configuration::{TestAuthorities, TestConfiguration},
mock::runtime_api::session_info_for_peers,
NODE_UNDER_TEST,
},
ApprovalsOptions, TestObjective,
configuration::{TestAuthorities, TestConfiguration},
mock::runtime_api::session_info_for_peers,
NODE_UNDER_TEST,
};
use futures::SinkExt;
use itertools::Itertools;
@@ -132,11 +129,7 @@ impl PeerMessagesGenerator {
options: &ApprovalsOptions,
) -> String {
let mut fingerprint = options.fingerprint();
let mut exclude_objective = configuration.clone();
// The objective contains the full content of `ApprovalOptions`, we don't want to put all of
// that in fingerprint, so execlute it because we add it manually see above.
exclude_objective.objective = TestObjective::Unimplemented;
let configuration_bytes = bincode::serialize(&exclude_objective).unwrap();
let configuration_bytes = bincode::serialize(&configuration).unwrap();
fingerprint.extend(configuration_bytes);
let mut sha1 = sha1::Sha1::new();
sha1.update(fingerprint);
@@ -24,25 +24,21 @@ use crate::{
mock_chain_selection::MockChainSelection,
test_message::{MessagesBundle, TestMessageInfo},
},
core::{
configuration::TestAuthorities,
environment::{
BenchmarkUsage, TestEnvironment, TestEnvironmentDependencies, MAX_TIME_OF_FLIGHT,
},
mock::{
chain_api::{ChainApiState, MockChainApi},
dummy_builder,
network_bridge::{MockNetworkBridgeRx, MockNetworkBridgeTx},
runtime_api::MockRuntimeApi,
AlwaysSupportsParachains, TestSyncOracle,
},
network::{
new_network, HandleNetworkMessage, NetworkEmulatorHandle, NetworkInterface,
NetworkInterfaceReceiver,
},
NODE_UNDER_TEST,
configuration::{TestAuthorities, TestConfiguration},
dummy_builder,
environment::{TestEnvironment, TestEnvironmentDependencies, MAX_TIME_OF_FLIGHT},
mock::{
chain_api::{ChainApiState, MockChainApi},
network_bridge::{MockNetworkBridgeRx, MockNetworkBridgeTx},
runtime_api::MockRuntimeApi,
AlwaysSupportsParachains, TestSyncOracle,
},
TestConfiguration,
network::{
new_network, HandleNetworkMessage, NetworkEmulatorHandle, NetworkInterface,
NetworkInterfaceReceiver,
},
usage::BenchmarkUsage,
NODE_UNDER_TEST,
};
use colored::Colorize;
use futures::channel::oneshot;
@@ -472,11 +468,9 @@ impl ApprovalTestState {
impl HandleNetworkMessage for ApprovalTestState {
fn handle(
&self,
_message: crate::core::network::NetworkMessage,
_node_sender: &mut futures::channel::mpsc::UnboundedSender<
crate::core::network::NetworkMessage,
>,
) -> Option<crate::core::network::NetworkMessage> {
_message: crate::network::NetworkMessage,
_node_sender: &mut futures::channel::mpsc::UnboundedSender<crate::network::NetworkMessage>,
) -> Option<crate::network::NetworkMessage> {
self.total_sent_messages_from_node
.as_ref()
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
@@ -841,8 +835,14 @@ fn build_overseer(
pub fn prepare_test(
config: TestConfiguration,
options: ApprovalsOptions,
with_prometheus_endpoint: bool,
) -> (TestEnvironment, ApprovalTestState) {
prepare_test_inner(config, TestEnvironmentDependencies::default(), options)
prepare_test_inner(
config,
TestEnvironmentDependencies::default(),
options,
with_prometheus_endpoint,
)
}
/// Build the test environment for an Approval benchmark.
@@ -850,6 +850,7 @@ fn prepare_test_inner(
config: TestConfiguration,
dependencies: TestEnvironmentDependencies,
options: ApprovalsOptions,
with_prometheus_endpoint: bool,
) -> (TestEnvironment, ApprovalTestState) {
gum::info!("Prepare test state");
let state = ApprovalTestState::new(&config, options, &dependencies);
@@ -878,6 +879,7 @@ fn prepare_test_inner(
overseer,
overseer_handle,
state.test_authorities.clone(),
with_prometheus_endpoint,
),
state,
)
@@ -15,9 +15,8 @@
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use crate::{
approval::{BlockTestData, CandidateTestData},
core::configuration::TestAuthorities,
ApprovalsOptions,
approval::{ApprovalsOptions, BlockTestData, CandidateTestData},
configuration::TestAuthorities,
};
use itertools::Itertools;
use parity_scale_codec::{Decode, Encode};
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use crate::core::{environment::TestEnvironmentDependencies, mock::TestSyncOracle};
use crate::{environment::TestEnvironmentDependencies, mock::TestSyncOracle};
use polkadot_node_core_av_store::{AvailabilityStoreSubsystem, Config};
use polkadot_node_metrics::metrics::Metrics;
use polkadot_node_subsystem_util::database::Database;
@@ -15,22 +15,18 @@
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use crate::{
core::{
configuration::TestConfiguration,
environment::{BenchmarkUsage, TestEnvironmentDependencies},
mock::{
av_store,
av_store::MockAvailabilityStore,
chain_api::{ChainApiState, MockChainApi},
dummy_builder,
network_bridge::{self, MockNetworkBridgeRx, MockNetworkBridgeTx},
runtime_api,
runtime_api::MockRuntimeApi,
AlwaysSupportsParachains,
},
network::new_network,
configuration::TestConfiguration,
dummy_builder,
environment::{TestEnvironment, TestEnvironmentDependencies, GENESIS_HASH},
mock::{
av_store::{self, MockAvailabilityStore},
chain_api::{ChainApiState, MockChainApi},
network_bridge::{self, MockNetworkBridgeRx, MockNetworkBridgeTx},
runtime_api::{self, MockRuntimeApi},
AlwaysSupportsParachains,
},
TestEnvironment, TestObjective, GENESIS_HASH,
network::new_network,
usage::BenchmarkUsage,
};
use av_store::NetworkAvailabilityState;
use av_store_helpers::new_av_store;
@@ -73,14 +69,30 @@ use sc_network::{
PeerId,
};
use sc_service::SpawnTaskHandle;
use serde::{Deserialize, Serialize};
use sp_core::H256;
use std::{collections::HashMap, iter::Cycle, ops::Sub, sync::Arc, time::Instant};
mod av_store_helpers;
pub(crate) mod cli;
const LOG_TARGET: &str = "subsystem-bench::availability";
#[derive(Debug, Clone, Serialize, Deserialize, clap::Parser)]
#[clap(rename_all = "kebab-case")]
#[allow(missing_docs)]
pub struct DataAvailabilityReadOptions {
#[clap(short, long, default_value_t = false)]
/// Turbo boost AD Read by fetching the full availability datafrom backers first. Saves CPU as
/// we don't need to re-construct from chunks. Tipically this is only faster if nodes have
/// enough bandwidth.
pub fetch_from_backers: bool,
}
pub enum TestDataAvailability {
Read(DataAvailabilityReadOptions),
Write,
}
fn build_overseer_for_availability_read(
spawn_task_handle: SpawnTaskHandle,
runtime_api: MockRuntimeApi,
@@ -141,14 +153,24 @@ fn build_overseer_for_availability_write(
pub fn prepare_test(
config: TestConfiguration,
state: &mut TestState,
mode: TestDataAvailability,
with_prometheus_endpoint: bool,
) -> (TestEnvironment, Vec<ProtocolConfig>) {
prepare_test_inner(config, state, TestEnvironmentDependencies::default())
prepare_test_inner(
config,
state,
mode,
TestEnvironmentDependencies::default(),
with_prometheus_endpoint,
)
}
fn prepare_test_inner(
config: TestConfiguration,
state: &mut TestState,
mode: TestDataAvailability,
dependencies: TestEnvironmentDependencies,
with_prometheus_endpoint: bool,
) -> (TestEnvironment, Vec<ProtocolConfig>) {
// Generate test authorities.
let test_authorities = config.generate_authorities();
@@ -216,8 +238,8 @@ fn prepare_test_inner(
let network_bridge_rx =
network_bridge::MockNetworkBridgeRx::new(network_receiver, Some(chunk_req_cfg.clone()));
let (overseer, overseer_handle) = match &state.config().objective {
TestObjective::DataAvailabilityRead(options) => {
let (overseer, overseer_handle) = match &mode {
TestDataAvailability::Read(options) => {
let use_fast_path = options.fetch_from_backers;
let subsystem = if use_fast_path {
@@ -247,7 +269,7 @@ fn prepare_test_inner(
&dependencies,
)
},
TestObjective::DataAvailabilityWrite => {
TestDataAvailability::Write => {
let availability_distribution = AvailabilityDistributionSubsystem::new(
test_authorities.keyring.keystore(),
IncomingRequestReceivers { pov_req_receiver, chunk_req_receiver },
@@ -284,9 +306,6 @@ fn prepare_test_inner(
&dependencies,
)
},
_ => {
unimplemented!("Invalid test objective")
},
};
(
@@ -297,6 +316,7 @@ fn prepare_test_inner(
overseer,
overseer_handle,
test_authorities,
with_prometheus_endpoint,
),
req_cfgs,
)
@@ -326,10 +346,6 @@ pub struct TestState {
}
impl TestState {
fn config(&self) -> &TestConfiguration {
&self.config
}
pub fn next_candidate(&mut self) -> Option<CandidateReceipt> {
let candidate = self.candidates.next();
let candidate_hash = candidate.as_ref().unwrap().hash();
@@ -16,7 +16,7 @@
//! Test configuration definition and helpers.
use crate::{core::keyring::Keyring, TestObjective};
use crate::keyring::Keyring;
use itertools::Itertools;
use polkadot_primitives::{AssignmentId, AuthorityDiscoveryId, ValidatorId};
use rand::thread_rng;
@@ -24,17 +24,7 @@ use rand_distr::{Distribution, Normal, Uniform};
use sc_network::PeerId;
use serde::{Deserialize, Serialize};
use sp_consensus_babe::AuthorityId;
use std::{collections::HashMap, path::Path};
pub fn random_pov_size(min_pov_size: usize, max_pov_size: usize) -> usize {
random_uniform_sample(min_pov_size, max_pov_size)
}
fn random_uniform_sample<T: Into<usize> + From<usize>>(min_value: T, max_value: T) -> T {
Uniform::from(min_value.into()..=max_value.into())
.sample(&mut thread_rng())
.into()
}
use std::collections::HashMap;
/// Peer networking latency configuration.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
@@ -87,8 +77,6 @@ fn default_no_show_slots() -> usize {
/// The test input parameters
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TestConfiguration {
/// The test objective
pub objective: TestObjective,
/// Number of validators
pub n_validators: usize,
/// Number of cores
@@ -115,7 +103,7 @@ pub struct TestConfiguration {
pub max_pov_size: usize,
/// Randomly sampled pov_sizes
#[serde(skip)]
pov_sizes: Vec<usize>,
pub pov_sizes: Vec<usize>,
/// The amount of bandiwdth remote validators have.
#[serde(default = "default_bandwidth")]
pub peer_bandwidth: usize,
@@ -133,56 +121,32 @@ pub struct TestConfiguration {
pub num_blocks: usize,
}
fn generate_pov_sizes(count: usize, min_kib: usize, max_kib: usize) -> Vec<usize> {
(0..count).map(|_| random_pov_size(min_kib * 1024, max_kib * 1024)).collect()
}
#[derive(Serialize, Deserialize)]
pub struct TestSequence {
#[serde(rename(serialize = "TestConfiguration", deserialize = "TestConfiguration"))]
test_configurations: Vec<TestConfiguration>,
}
impl TestSequence {
pub fn into_vec(self) -> Vec<TestConfiguration> {
self.test_configurations
.into_iter()
.map(|mut config| {
config.pov_sizes =
generate_pov_sizes(config.n_cores, config.min_pov_size, config.max_pov_size);
config
})
.collect()
impl Default for TestConfiguration {
fn default() -> Self {
Self {
n_validators: Default::default(),
n_cores: Default::default(),
needed_approvals: default_needed_approvals(),
zeroth_delay_tranche_width: default_zeroth_delay_tranche_width(),
relay_vrf_modulo_samples: default_relay_vrf_modulo_samples(),
n_delay_tranches: default_n_delay_tranches(),
no_show_slots: default_no_show_slots(),
max_validators_per_core: default_backing_group_size(),
min_pov_size: default_pov_size(),
max_pov_size: default_pov_size(),
pov_sizes: Default::default(),
peer_bandwidth: default_bandwidth(),
bandwidth: default_bandwidth(),
latency: Default::default(),
connectivity: default_connectivity(),
num_blocks: Default::default(),
}
}
}
impl TestSequence {
pub fn new_from_file(path: &Path) -> std::io::Result<TestSequence> {
let string = String::from_utf8(std::fs::read(path)?).expect("File is valid UTF8");
Ok(serde_yaml::from_str(&string).expect("File is valid test sequence YA"))
}
}
/// Helper struct for authority related state.
#[derive(Clone)]
pub struct TestAuthorities {
pub keyring: Keyring,
pub validator_public: Vec<ValidatorId>,
pub validator_authority_id: Vec<AuthorityDiscoveryId>,
pub validator_babe_id: Vec<AuthorityId>,
pub validator_assignment_id: Vec<AssignmentId>,
pub key_seeds: Vec<String>,
pub peer_ids: Vec<PeerId>,
pub peer_id_to_authority: HashMap<PeerId, AuthorityDiscoveryId>,
}
impl TestConfiguration {
#[allow(unused)]
pub fn write_to_disk(&self) {
// Serialize a slice of configurations
let yaml = serde_yaml::to_string(&TestSequence { test_configurations: vec![self.clone()] })
.unwrap();
std::fs::write("last_test.yaml", yaml).unwrap();
pub fn generate_pov_sizes(&mut self) {
self.pov_sizes = generate_pov_sizes(self.n_cores, self.min_pov_size, self.max_pov_size);
}
pub fn pov_sizes(&self) -> &[usize] {
@@ -239,6 +203,33 @@ impl TestConfiguration {
}
}
fn random_uniform_sample<T: Into<usize> + From<usize>>(min_value: T, max_value: T) -> T {
Uniform::from(min_value.into()..=max_value.into())
.sample(&mut thread_rng())
.into()
}
fn random_pov_size(min_pov_size: usize, max_pov_size: usize) -> usize {
random_uniform_sample(min_pov_size, max_pov_size)
}
fn generate_pov_sizes(count: usize, min_kib: usize, max_kib: usize) -> Vec<usize> {
(0..count).map(|_| random_pov_size(min_kib * 1024, max_kib * 1024)).collect()
}
/// Helper struct for authority related state.
#[derive(Clone)]
pub struct TestAuthorities {
pub keyring: Keyring,
pub validator_public: Vec<ValidatorId>,
pub validator_authority_id: Vec<AuthorityDiscoveryId>,
pub validator_babe_id: Vec<AuthorityId>,
pub validator_assignment_id: Vec<AssignmentId>,
pub key_seeds: Vec<String>,
pub peer_ids: Vec<PeerId>,
pub peer_id_to_authority: HashMap<PeerId, AuthorityDiscoveryId>,
}
/// Sample latency (in milliseconds) from a normal distribution with parameters
/// specified in `maybe_peer_latency`.
pub fn random_latency(maybe_peer_latency: Option<&PeerLatency>) -> usize {
@@ -19,7 +19,7 @@
//!
//! Currently histogram buckets are skipped.
use crate::{TestConfiguration, LOG_TARGET};
use crate::configuration::TestConfiguration;
use colored::Colorize;
use prometheus::{
proto::{MetricFamily, MetricType},
@@ -27,6 +27,8 @@ use prometheus::{
};
use std::fmt::Display;
const LOG_TARGET: &str = "subsystem-bench::display";
#[derive(Default, Debug)]
pub struct MetricCollection(Vec<TestMetric>);
@@ -85,6 +87,7 @@ impl Display for MetricCollection {
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct TestMetric {
name: String,
@@ -184,15 +187,16 @@ pub fn parse_metrics(registry: &Registry) -> MetricCollection {
test_metrics.into()
}
pub fn display_configuration(test_config: &TestConfiguration) {
gum::info!(
"[{}] {}, {}, {}, {}, {}",
format!("objective = {:?}", test_config.objective).green(),
format!("n_validators = {}", test_config.n_validators).blue(),
format!("n_cores = {}", test_config.n_cores).blue(),
format!("pov_size = {} - {}", test_config.min_pov_size, test_config.max_pov_size)
.bright_black(),
format!("connectivity = {}", test_config.connectivity).bright_black(),
format!("latency = {:?}", test_config.latency).bright_black(),
);
impl Display for TestConfiguration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}, {}, {}, {}, {}",
format!("n_validators = {}", self.n_validators).blue(),
format!("n_cores = {}", self.n_cores).blue(),
format!("pov_size = {} - {}", self.min_pov_size, self.max_pov_size).bright_black(),
format!("connectivity = {}", self.connectivity).bright_black(),
format!("latency = {:?}", self.latency).bright_black(),
)
}
}
@@ -17,13 +17,11 @@
//! Test environment implementation
use crate::{
core::{
configuration::TestAuthorities, mock::AlwaysSupportsParachains,
network::NetworkEmulatorHandle,
},
TestConfiguration,
configuration::{TestAuthorities, TestConfiguration},
mock::AlwaysSupportsParachains,
network::NetworkEmulatorHandle,
usage::{BenchmarkUsage, ResourceUsage},
};
use colored::Colorize;
use core::time::Duration;
use futures::{Future, FutureExt};
use polkadot_node_subsystem::{messages::AllMessages, Overseer, SpawnGlue, TimeoutExt};
@@ -33,7 +31,6 @@ use polkadot_node_subsystem_util::metrics::prometheus::{
};
use polkadot_overseer::{BlockInfo, Handle as OverseerHandle};
use sc_service::{SpawnTaskHandle, TaskManager};
use serde::{Deserialize, Serialize};
use std::net::{Ipv4Addr, SocketAddr};
use tokio::runtime::Handle;
@@ -204,6 +201,7 @@ impl TestEnvironment {
overseer: Overseer<SpawnGlue<SpawnTaskHandle>, AlwaysSupportsParachains>,
overseer_handle: OverseerHandle,
authorities: TestAuthorities,
with_prometheus_endpoint: bool,
) -> Self {
let metrics = TestEnvironmentMetrics::new(&dependencies.registry)
.expect("Metrics need to be registered");
@@ -211,19 +209,21 @@ impl TestEnvironment {
let spawn_handle = dependencies.task_manager.spawn_handle();
spawn_handle.spawn_blocking("overseer", "overseer", overseer.run().boxed());
let registry_clone = dependencies.registry.clone();
dependencies.task_manager.spawn_handle().spawn_blocking(
"prometheus",
"test-environment",
async move {
prometheus_endpoint::init_prometheus(
SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::LOCALHOST), 9999),
registry_clone,
)
.await
.unwrap();
},
);
if with_prometheus_endpoint {
let registry_clone = dependencies.registry.clone();
dependencies.task_manager.spawn_handle().spawn_blocking(
"prometheus",
"test-environment",
async move {
prometheus_endpoint::init_prometheus(
SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::LOCALHOST), 9999),
registry_clone,
)
.await
.unwrap();
},
);
}
TestEnvironment {
runtime_handle: dependencies.runtime.handle().clone(),
@@ -411,41 +411,3 @@ impl TestEnvironment {
usage
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BenchmarkUsage {
benchmark_name: String,
network_usage: Vec<ResourceUsage>,
cpu_usage: Vec<ResourceUsage>,
}
impl std::fmt::Display for BenchmarkUsage {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"\n{}\n\n{}\n{}\n\n{}\n{}\n",
self.benchmark_name.purple(),
format!("{:<32}{:>12}{:>12}", "Network usage, KiB", "total", "per block").blue(),
self.network_usage
.iter()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join("\n"),
format!("{:<32}{:>12}{:>12}", "CPU usage in seconds", "total", "per block").blue(),
self.cpu_usage.iter().map(|v| v.to_string()).collect::<Vec<String>>().join("\n")
)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ResourceUsage {
resource_name: String,
total: f64,
per_block: f64,
}
impl std::fmt::Display for ResourceUsage {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:<32}{:>12.3}{:>12.3}", self.resource_name.cyan(), self.total, self.per_block)
}
}
@@ -15,11 +15,14 @@
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
// The validator index that represent the node that is under test.
pub(crate) const NODE_UNDER_TEST: u32 = 0;
pub const NODE_UNDER_TEST: u32 = 0;
pub(crate) mod configuration;
pub mod approval;
pub mod availability;
pub mod configuration;
pub(crate) mod display;
pub(crate) mod environment;
pub(crate) mod keyring;
pub(crate) mod mock;
pub(crate) mod network;
pub mod usage;
@@ -16,7 +16,7 @@
//! A generic av store subsystem mockup suitable to be used in benchmarks.
use crate::core::network::{HandleNetworkMessage, NetworkMessage};
use crate::network::{HandleNetworkMessage, NetworkMessage};
use futures::{channel::oneshot, FutureExt};
use parity_scale_codec::Encode;
use polkadot_node_network_protocol::request_response::{
@@ -34,9 +34,10 @@ impl HeadSupportsParachains for AlwaysSupportsParachains {
}
// An orchestra with dummy subsystems
#[macro_export]
macro_rules! dummy_builder {
($spawn_task_handle: ident, $metrics: ident) => {{
use $crate::core::mock::dummy::*;
use $crate::mock::dummy::*;
// Initialize a mock overseer.
// All subsystem except approval_voting and approval_distribution are mock subsystems.
@@ -72,7 +73,6 @@ macro_rules! dummy_builder {
.spawner(SpawnGlue($spawn_task_handle))
}};
}
pub(crate) use dummy_builder;
#[derive(Clone)]
pub struct TestSyncOracle {}
@@ -17,7 +17,7 @@
//! Mocked `network-bridge` subsystems that uses a `NetworkInterface` to access
//! the emulated network.
use crate::core::{
use crate::{
configuration::TestAuthorities,
network::{NetworkEmulatorHandle, NetworkInterfaceReceiver, NetworkMessage, RequestExt},
};
@@ -16,7 +16,7 @@
//! A generic runtime api subsystem mockup suitable to be used in benchmarks.
use crate::core::configuration::{TestAuthorities, TestConfiguration};
use crate::configuration::{TestAuthorities, TestConfiguration};
use bitvec::prelude::BitVec;
use futures::FutureExt;
use itertools::Itertools;
@@ -33,7 +33,7 @@
// |
// Subsystems under test
use crate::core::{
use crate::{
configuration::{random_latency, TestAuthorities, TestConfiguration},
environment::TestEnvironmentDependencies,
NODE_UNDER_TEST,
@@ -0,0 +1,146 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! Test usage implementation
use colored::Colorize;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Serialize, Deserialize)]
pub struct BenchmarkUsage {
pub benchmark_name: String,
pub network_usage: Vec<ResourceUsage>,
pub cpu_usage: Vec<ResourceUsage>,
}
impl std::fmt::Display for BenchmarkUsage {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"\n{}\n\n{}\n{}\n\n{}\n{}\n",
self.benchmark_name.purple(),
format!("{:<32}{:>12}{:>12}", "Network usage, KiB", "total", "per block").blue(),
self.network_usage
.iter()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join("\n"),
format!("{:<32}{:>12}{:>12}", "CPU usage, seconds", "total", "per block").blue(),
self.cpu_usage.iter().map(|v| v.to_string()).collect::<Vec<String>>().join("\n")
)
}
}
impl BenchmarkUsage {
pub fn average(usages: &[Self]) -> Self {
let all_network_usages: Vec<&ResourceUsage> =
usages.iter().flat_map(|v| &v.network_usage).collect();
let all_cpu_usage: Vec<&ResourceUsage> = usages.iter().flat_map(|v| &v.cpu_usage).collect();
Self {
benchmark_name: usages.first().map(|v| v.benchmark_name.clone()).unwrap_or_default(),
network_usage: ResourceUsage::average_by_resource_name(&all_network_usages),
cpu_usage: ResourceUsage::average_by_resource_name(&all_cpu_usage),
}
}
pub fn check_network_usage(&self, checks: &[ResourceUsageCheck]) -> Vec<String> {
check_usage(&self.benchmark_name, &self.network_usage, checks)
}
pub fn check_cpu_usage(&self, checks: &[ResourceUsageCheck]) -> Vec<String> {
check_usage(&self.benchmark_name, &self.cpu_usage, checks)
}
pub fn cpu_usage_diff(&self, other: &Self, resource_name: &str) -> Option<f64> {
let self_res = self.cpu_usage.iter().find(|v| v.resource_name == resource_name);
let other_res = other.cpu_usage.iter().find(|v| v.resource_name == resource_name);
match (self_res, other_res) {
(Some(self_res), Some(other_res)) => Some(self_res.diff(other_res)),
_ => None,
}
}
}
fn check_usage(
benchmark_name: &str,
usage: &[ResourceUsage],
checks: &[ResourceUsageCheck],
) -> Vec<String> {
checks
.iter()
.filter_map(|check| {
check_resource_usage(usage, check)
.map(|message| format!("{}: {}", benchmark_name, message))
})
.collect()
}
fn check_resource_usage(
usage: &[ResourceUsage],
(resource_name, base, precision): &ResourceUsageCheck,
) -> Option<String> {
if let Some(usage) = usage.iter().find(|v| v.resource_name == *resource_name) {
let diff = (base - usage.per_block).abs() / base;
if diff < *precision {
None
} else {
Some(format!(
"The resource `{}` is expected to be equal to {} with a precision {}, but the current value is {}",
resource_name, base, precision, usage.per_block
))
}
} else {
Some(format!("The resource `{}` is not found", resource_name))
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ResourceUsage {
pub resource_name: String,
pub total: f64,
pub per_block: f64,
}
impl std::fmt::Display for ResourceUsage {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:<32}{:>12.3}{:>12.3}", self.resource_name.cyan(), self.total, self.per_block)
}
}
impl ResourceUsage {
fn average_by_resource_name(usages: &[&Self]) -> Vec<Self> {
let mut by_name: HashMap<String, Vec<&Self>> = Default::default();
for usage in usages {
by_name.entry(usage.resource_name.clone()).or_default().push(usage);
}
let mut average = vec![];
for (resource_name, values) in by_name {
let total = values.iter().map(|v| v.total).sum::<f64>() / values.len() as f64;
let per_block = values.iter().map(|v| v.per_block).sum::<f64>() / values.len() as f64;
average.push(Self { resource_name, total, per_block });
}
average
}
fn diff(&self, other: &Self) -> f64 {
(self.per_block - other.per_block).abs() / self.per_block
}
}
type ResourceUsageCheck<'a> = (&'a str, f64, f64);