// 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 . //! 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, /// Number of received sent per peer. n_cores: Gauge, /// PoV size pov_size: Histogram, /// Current block current_block: Gauge, /// Current block block_time: Gauge, } impl TestEnvironmentMetrics { pub fn new(registry: &Registry) -> Result { 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, 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::() / (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() ) } }