mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-07-27 02:15:42 +00:00
8a6e9ef189
This tool makes it easy to run parachain consensus stress/performance
testing on your development machine or in CI.
## Motivation
The parachain consensus node implementation spans across many modules
which we call subsystems. Each subsystem is responsible for a small part
of logic of the parachain consensus pipeline, but in general the most
load and performance issues are localized in just a few core subsystems
like `availability-recovery`, `approval-voting` or
`dispute-coordinator`. In the absence of such a tool, we would run large
test nets to load/stress test these parts of the system. Setting up and
making sense of the amount of data produced by such a large test is very
expensive, hard to orchestrate and is a huge development time sink.
## PR contents
- CLI tool
- Data Availability Read test
- reusable mockups and components needed so far
- Documentation on how to get started
### Data Availability Read test
An overseer is built with using a real `availability-recovery` susbsytem
instance while dependent subsystems like `av-store`, `network-bridge`
and `runtime-api` are mocked. The network bridge will emulate all the
network peers and their answering to requests.
The test is going to be run for a number of blocks. For each block it
will generate send a “RecoverAvailableData” request for an arbitrary
number of candidates. We wait for the subsystem to respond to all
requests before moving to the next block.
At the same time we collect the usual subsystem metrics and task CPU
metrics and show some nice progress reports while running.
### Here is how the CLI looks like:
```
[2023-11-28T13:06:27Z INFO subsystem_bench::core::display] n_validators = 1000, n_cores = 20, pov_size = 5120 - 5120, error = 3, latency = Some(PeerLatency { min_latency: 1ms, max_latency: 100ms })
[2023-11-28T13:06:27Z INFO subsystem-bench::availability] Generating template candidate index=0 pov_size=5242880
[2023-11-28T13:06:27Z INFO subsystem-bench::availability] Created test environment.
[2023-11-28T13:06:27Z INFO subsystem-bench::availability] Pre-generating 60 candidates.
[2023-11-28T13:06:30Z INFO subsystem-bench::core] Initializing network emulation for 1000 peers.
[2023-11-28T13:06:30Z INFO subsystem-bench::availability] Current block 1/3
[2023-11-28T13:06:30Z INFO substrate_prometheus_endpoint] 〽️ Prometheus exporter started at 127.0.0.1:9999
[2023-11-28T13:06:30Z INFO subsystem_bench::availability] 20 recoveries pending
[2023-11-28T13:06:37Z INFO subsystem_bench::availability] Block time 6262ms
[2023-11-28T13:06:37Z INFO subsystem-bench::availability] Sleeping till end of block (0ms)
[2023-11-28T13:06:37Z INFO subsystem-bench::availability] Current block 2/3
[2023-11-28T13:06:37Z INFO subsystem_bench::availability] 20 recoveries pending
[2023-11-28T13:06:43Z INFO subsystem_bench::availability] Block time 6369ms
[2023-11-28T13:06:43Z INFO subsystem-bench::availability] Sleeping till end of block (0ms)
[2023-11-28T13:06:43Z INFO subsystem-bench::availability] Current block 3/3
[2023-11-28T13:06:43Z INFO subsystem_bench::availability] 20 recoveries pending
[2023-11-28T13:06:49Z INFO subsystem_bench::availability] Block time 6194ms
[2023-11-28T13:06:49Z INFO subsystem-bench::availability] Sleeping till end of block (0ms)
[2023-11-28T13:06:49Z INFO subsystem_bench::availability] All blocks processed in 18829ms
[2023-11-28T13:06:49Z INFO subsystem_bench::availability] Throughput: 102400 KiB/block
[2023-11-28T13:06:49Z INFO subsystem_bench::availability] Block time: 6276 ms
[2023-11-28T13:06:49Z INFO subsystem_bench::availability]
Total received from network: 415 MiB
Total sent to network: 724 KiB
Total subsystem CPU usage 24.00s
CPU usage per block 8.00s
Total test environment CPU usage 0.15s
CPU usage per block 0.05s
```
### Prometheus/Grafana stack in action
<img width="1246" alt="Screenshot 2023-11-28 at 15 11 10"
src="https://github.com/paritytech/polkadot-sdk/assets/54316454/eaa47422-4a5e-4a3a-aaef-14ca644c1574">
<img width="1246" alt="Screenshot 2023-11-28 at 15 12 01"
src="https://github.com/paritytech/polkadot-sdk/assets/54316454/237329d6-1710-4c27-8f67-5fb11d7f66ea">
<img width="1246" alt="Screenshot 2023-11-28 at 15 12 38"
src="https://github.com/paritytech/polkadot-sdk/assets/54316454/a07119e8-c9f1-4810-a1b3-f1b7b01cf357">
---------
Signed-off-by: Andrei Sandu <andrei-mihail@parity.io>
334 lines
9.6 KiB
Rust
334 lines
9.6 KiB
Rust
// 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 environment implementation
|
|
use crate::{
|
|
core::{mock::AlwaysSupportsParachains, network::NetworkEmulator},
|
|
TestConfiguration,
|
|
};
|
|
use colored::Colorize;
|
|
use core::time::Duration;
|
|
use futures::FutureExt;
|
|
use polkadot_overseer::{BlockInfo, Handle as OverseerHandle};
|
|
|
|
use polkadot_node_subsystem::{messages::AllMessages, Overseer, SpawnGlue, TimeoutExt};
|
|
use polkadot_node_subsystem_types::Hash;
|
|
use polkadot_node_subsystem_util::metrics::prometheus::{
|
|
self, Gauge, Histogram, PrometheusError, Registry, U64,
|
|
};
|
|
|
|
use sc_network::peer_store::LOG_TARGET;
|
|
use sc_service::{SpawnTaskHandle, TaskManager};
|
|
use std::{
|
|
fmt::Display,
|
|
net::{Ipv4Addr, SocketAddr},
|
|
};
|
|
use tokio::runtime::Handle;
|
|
|
|
const MIB: f64 = 1024.0 * 1024.0;
|
|
|
|
/// Test environment/configuration metrics
|
|
#[derive(Clone)]
|
|
pub struct TestEnvironmentMetrics {
|
|
/// Number of bytes sent per peer.
|
|
n_validators: Gauge<U64>,
|
|
/// Number of received sent per peer.
|
|
n_cores: Gauge<U64>,
|
|
/// PoV size
|
|
pov_size: Histogram,
|
|
/// Current block
|
|
current_block: Gauge<U64>,
|
|
/// Current block
|
|
block_time: Gauge<U64>,
|
|
}
|
|
|
|
impl TestEnvironmentMetrics {
|
|
pub fn new(registry: &Registry) -> Result<Self, PrometheusError> {
|
|
let mut buckets = prometheus::exponential_buckets(16384.0, 2.0, 9)
|
|
.expect("arguments are always valid; qed");
|
|
buckets.extend(vec![5.0 * MIB, 6.0 * MIB, 7.0 * MIB, 8.0 * MIB, 9.0 * MIB, 10.0 * MIB]);
|
|
|
|
Ok(Self {
|
|
n_validators: prometheus::register(
|
|
Gauge::new(
|
|
"subsystem_benchmark_n_validators",
|
|
"Total number of validators in the test",
|
|
)?,
|
|
registry,
|
|
)?,
|
|
n_cores: prometheus::register(
|
|
Gauge::new(
|
|
"subsystem_benchmark_n_cores",
|
|
"Number of cores we fetch availability for each block",
|
|
)?,
|
|
registry,
|
|
)?,
|
|
current_block: prometheus::register(
|
|
Gauge::new("subsystem_benchmark_current_block", "The current test block")?,
|
|
registry,
|
|
)?,
|
|
block_time: prometheus::register(
|
|
Gauge::new("subsystem_benchmark_block_time", "The time it takes for the target subsystems(s) to complete all the requests in a block")?,
|
|
registry,
|
|
)?,
|
|
pov_size: prometheus::register(
|
|
Histogram::with_opts(
|
|
prometheus::HistogramOpts::new(
|
|
"subsystem_benchmark_pov_size",
|
|
"The compressed size of the proof of validity of a candidate",
|
|
)
|
|
.buckets(buckets),
|
|
)?,
|
|
registry,
|
|
)?,
|
|
})
|
|
}
|
|
|
|
pub fn set_n_validators(&self, n_validators: usize) {
|
|
self.n_validators.set(n_validators as u64);
|
|
}
|
|
|
|
pub fn set_n_cores(&self, n_cores: usize) {
|
|
self.n_cores.set(n_cores as u64);
|
|
}
|
|
|
|
pub fn set_current_block(&self, current_block: usize) {
|
|
self.current_block.set(current_block as u64);
|
|
}
|
|
|
|
pub fn set_block_time(&self, block_time_ms: u64) {
|
|
self.block_time.set(block_time_ms);
|
|
}
|
|
|
|
pub fn on_pov_size(&self, pov_size: usize) {
|
|
self.pov_size.observe(pov_size as f64);
|
|
}
|
|
}
|
|
|
|
fn new_runtime() -> tokio::runtime::Runtime {
|
|
tokio::runtime::Builder::new_multi_thread()
|
|
.thread_name("subsystem-bench")
|
|
.enable_all()
|
|
.thread_stack_size(3 * 1024 * 1024)
|
|
.build()
|
|
.unwrap()
|
|
}
|
|
|
|
/// Wrapper for dependencies
|
|
pub struct TestEnvironmentDependencies {
|
|
pub registry: Registry,
|
|
pub task_manager: TaskManager,
|
|
pub runtime: tokio::runtime::Runtime,
|
|
}
|
|
|
|
impl Default for TestEnvironmentDependencies {
|
|
fn default() -> Self {
|
|
let runtime = new_runtime();
|
|
let registry = Registry::new();
|
|
let task_manager: TaskManager =
|
|
TaskManager::new(runtime.handle().clone(), Some(®istry)).unwrap();
|
|
|
|
Self { runtime, registry, task_manager }
|
|
}
|
|
}
|
|
|
|
// A dummy genesis hash
|
|
pub const GENESIS_HASH: Hash = Hash::repeat_byte(0xff);
|
|
|
|
// We use this to bail out sending messages to the subsystem if it is overloaded such that
|
|
// the time of flight is breaches 5s.
|
|
// This should eventually be a test parameter.
|
|
const MAX_TIME_OF_FLIGHT: Duration = Duration::from_millis(5000);
|
|
|
|
/// The test environment is the high level wrapper of all things required to test
|
|
/// a certain subsystem.
|
|
///
|
|
/// ## Mockups
|
|
/// The overseer is passed in during construction and it can host an arbitrary number of
|
|
/// real subsystems instances and the corresponding mocked instances such that the real
|
|
/// subsystems can get their messages answered.
|
|
///
|
|
/// As the subsystem's performance depends on network connectivity, the test environment
|
|
/// emulates validator nodes on the network, see `NetworkEmulator`. The network emulation
|
|
/// is configurable in terms of peer bandwidth, latency and connection error rate using
|
|
/// uniform distribution sampling.
|
|
///
|
|
///
|
|
/// ## Usage
|
|
/// `TestEnvironment` is used in tests to send `Overseer` messages or signals to the subsystem
|
|
/// under test.
|
|
///
|
|
/// ## Collecting test metrics
|
|
///
|
|
/// ### Prometheus
|
|
/// A prometheus endpoint is exposed while the test is running. A local Prometheus instance
|
|
/// can scrape it every 1s and a Grafana dashboard is the preferred way of visualizing
|
|
/// the performance characteristics of the subsystem.
|
|
///
|
|
/// ### CLI
|
|
/// A subset of the Prometheus metrics are printed at the end of the test.
|
|
pub struct TestEnvironment {
|
|
/// Test dependencies
|
|
dependencies: TestEnvironmentDependencies,
|
|
/// A runtime handle
|
|
runtime_handle: tokio::runtime::Handle,
|
|
/// A handle to the lovely overseer
|
|
overseer_handle: OverseerHandle,
|
|
/// The test configuration.
|
|
config: TestConfiguration,
|
|
/// A handle to the network emulator.
|
|
network: NetworkEmulator,
|
|
/// Configuration/env metrics
|
|
metrics: TestEnvironmentMetrics,
|
|
}
|
|
|
|
impl TestEnvironment {
|
|
/// Create a new test environment
|
|
pub fn new(
|
|
dependencies: TestEnvironmentDependencies,
|
|
config: TestConfiguration,
|
|
network: NetworkEmulator,
|
|
overseer: Overseer<SpawnGlue<SpawnTaskHandle>, AlwaysSupportsParachains>,
|
|
overseer_handle: OverseerHandle,
|
|
) -> Self {
|
|
let metrics = TestEnvironmentMetrics::new(&dependencies.registry)
|
|
.expect("Metrics need to be registered");
|
|
|
|
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();
|
|
},
|
|
);
|
|
|
|
TestEnvironment {
|
|
runtime_handle: dependencies.runtime.handle().clone(),
|
|
dependencies,
|
|
overseer_handle,
|
|
config,
|
|
network,
|
|
metrics,
|
|
}
|
|
}
|
|
|
|
pub fn config(&self) -> &TestConfiguration {
|
|
&self.config
|
|
}
|
|
|
|
pub fn network(&self) -> &NetworkEmulator {
|
|
&self.network
|
|
}
|
|
|
|
pub fn registry(&self) -> &Registry {
|
|
&self.dependencies.registry
|
|
}
|
|
|
|
pub fn metrics(&self) -> &TestEnvironmentMetrics {
|
|
&self.metrics
|
|
}
|
|
|
|
pub fn runtime(&self) -> Handle {
|
|
self.runtime_handle.clone()
|
|
}
|
|
|
|
// Send a message to the subsystem under test environment.
|
|
pub async fn send_message(&mut self, msg: AllMessages) {
|
|
self.overseer_handle
|
|
.send_msg(msg, LOG_TARGET)
|
|
.timeout(MAX_TIME_OF_FLIGHT)
|
|
.await
|
|
.unwrap_or_else(|| {
|
|
panic!("{}ms maximum time of flight breached", MAX_TIME_OF_FLIGHT.as_millis())
|
|
});
|
|
}
|
|
|
|
// Send an `ActiveLeavesUpdate` signal to all subsystems under test.
|
|
pub async fn import_block(&mut self, block: BlockInfo) {
|
|
self.overseer_handle
|
|
.block_imported(block)
|
|
.timeout(MAX_TIME_OF_FLIGHT)
|
|
.await
|
|
.unwrap_or_else(|| {
|
|
panic!("{}ms maximum time of flight breached", MAX_TIME_OF_FLIGHT.as_millis())
|
|
});
|
|
}
|
|
|
|
// Stop overseer and subsystems.
|
|
pub async fn stop(&mut self) {
|
|
self.overseer_handle.stop().await;
|
|
}
|
|
}
|
|
|
|
impl Display for TestEnvironment {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
let stats = self.network().stats();
|
|
|
|
writeln!(f, "\n")?;
|
|
writeln!(
|
|
f,
|
|
"Total received from network: {}",
|
|
format!(
|
|
"{} MiB",
|
|
stats
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(_index, stats)| stats.tx_bytes_total as u128)
|
|
.sum::<u128>() / (1024 * 1024)
|
|
)
|
|
.cyan()
|
|
)?;
|
|
writeln!(
|
|
f,
|
|
"Total sent to network: {}",
|
|
format!("{} KiB", stats[0].tx_bytes_total / (1024)).cyan()
|
|
)?;
|
|
|
|
let test_metrics = super::display::parse_metrics(self.registry());
|
|
let subsystem_cpu_metrics =
|
|
test_metrics.subset_with_label_value("task_group", "availability-recovery");
|
|
let total_cpu = subsystem_cpu_metrics.sum_by("substrate_tasks_polling_duration_sum");
|
|
writeln!(f, "Total subsystem CPU usage {}", format!("{:.2}s", total_cpu).bright_purple())?;
|
|
writeln!(
|
|
f,
|
|
"CPU usage per block {}",
|
|
format!("{:.2}s", total_cpu / self.config().num_blocks as f64).bright_purple()
|
|
)?;
|
|
|
|
let test_env_cpu_metrics =
|
|
test_metrics.subset_with_label_value("task_group", "test-environment");
|
|
let total_cpu = test_env_cpu_metrics.sum_by("substrate_tasks_polling_duration_sum");
|
|
writeln!(
|
|
f,
|
|
"Total test environment CPU usage {}",
|
|
format!("{:.2}s", total_cpu).bright_purple()
|
|
)?;
|
|
writeln!(
|
|
f,
|
|
"CPU usage per block {}",
|
|
format!("{:.2}s", total_cpu / self.config().num_blocks as f64).bright_purple()
|
|
)
|
|
}
|
|
}
|