substrate_benchmark_machine/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Contains the [`MachineCmd`] as entry point for the node
19//! and the core benchmarking logic.
20
21pub mod hardware;
22
23use std::{boxed::Box, path::Path};
24
25use clap::Parser;
26use comfy_table::{Row, Table};
27use log::{error, info, warn};
28
29use sc_cli::Result;
30use sc_sysinfo::{
31    benchmark_cpu, benchmark_cpu_parallelism, benchmark_disk_random_writes,
32    benchmark_disk_sequential_writes, benchmark_memory, benchmark_sr25519_verify, ExecutionLimit,
33    HwBench, Metric, Requirement, Requirements, Throughput,
34};
35
36// use crate::shared::check_build_profile;
37pub use hardware::SUBSTRATE_REFERENCE_HARDWARE;
38
39/// Command to benchmark the hardware.
40///
41/// Runs multiple benchmarks and prints their output to console.
42/// Can be used to gauge if the hardware is fast enough to keep up with a chain's requirements.
43/// This command must be integrated by the client since the client can set compiler flags
44/// which influence the results.
45///
46/// You can use the `--base-path` flag to set a location for the disk benchmarks.
47#[derive(Debug, Parser)]
48pub struct MachineCmd {
49    /// Path to database.
50    #[arg(long, short = 'd')]
51    pub base_path: Option<String>,
52
53    /// Run full benchmarks instead of quick hardware check.
54    #[arg(long, short = 'f')]
55    pub full: bool,
56
57    /// Do not return an error if any check fails.
58    ///
59    /// Should only be used for debugging.
60    #[arg(long)]
61    pub allow_fail: bool,
62
63    /// Set a fault tolerance for passing a requirement.
64    ///
65    /// 10% means that the test would pass even when only 90% score was archived.
66    /// Can be used to mitigate outliers of the benchmarks.
67    #[arg(long, default_value_t = 10.0, value_name = "PERCENT")]
68    pub tolerance: f64,
69
70    /// Time limit for the verification benchmark.
71    #[arg(long, default_value_t = 5.0, value_name = "SECONDS")]
72    pub verify_duration: f32,
73
74    /// Time limit for the hash function benchmark.
75    #[arg(long, default_value_t = 5.0, value_name = "SECONDS")]
76    pub hash_duration: f32,
77
78    /// Time limit for the memory benchmark.
79    #[arg(long, default_value_t = 5.0, value_name = "SECONDS")]
80    pub memory_duration: f32,
81
82    /// Time limit for each disk benchmark.
83    #[arg(long, default_value_t = 5.0, value_name = "SECONDS")]
84    pub disk_duration: f32,
85}
86
87/// Helper for the result of a concrete benchmark.
88#[derive(Debug)]
89pub struct BenchResult {
90    /// Did the hardware pass the benchmark?
91    passed: bool,
92
93    /// The absolute score that was archived.
94    score: Throughput,
95
96    /// The score relative to the minimal required score.
97    ///
98    /// Is in range [0, 1].
99    rel_score: f64,
100}
101
102/// Errors that can be returned by the this command.
103#[derive(Debug, thiserror::Error)]
104#[allow(missing_docs)]
105pub enum Error {
106    #[error("One of the benchmarks had a score that was lower than its requirement")]
107    UnmetRequirement,
108
109    // #[error("The build profile is unfit for benchmarking: {0}")]
110    // BadBuildProfile(String),
111    #[error("Benchmark results are off by at least factor 100")]
112    BadResults,
113}
114
115impl MachineCmd {
116    /// Benchmarks a specific metric of the hardware and judges the resulting score.
117    pub fn run_benchmark(&self, requirement: &Requirement, dir: &Path) -> Result<BenchResult> {
118        // Dispatch the concrete function from `sc-sysinfo`.
119
120        let score = self.measure(&requirement.metric, dir)?;
121        let rel_score = score.as_bytes() / requirement.minimum.as_bytes();
122
123        // Sanity check if the result is off by factor >100x.
124        if rel_score >= 100.0 || rel_score <= 0.01 {
125            self.check_failed(Error::BadResults)?;
126        }
127        let passed = rel_score >= (1.0 - (self.tolerance / 100.0));
128        Ok(BenchResult {
129            passed,
130            score,
131            rel_score,
132        })
133    }
134
135    /// Measures a metric of the hardware.
136    fn measure(&self, metric: &Metric, dir: &Path) -> Result<Throughput> {
137        let verify_limit = ExecutionLimit::from_secs_f32(self.verify_duration);
138        let disk_limit = ExecutionLimit::from_secs_f32(self.disk_duration);
139        let hash_limit = ExecutionLimit::from_secs_f32(self.hash_duration);
140        let memory_limit = ExecutionLimit::from_secs_f32(self.memory_duration);
141
142        let score = match metric {
143            Metric::Blake2256 => benchmark_cpu(hash_limit),
144            Metric::Sr25519Verify => benchmark_sr25519_verify(verify_limit),
145            Metric::Blake2256Parallel { num_cores } => {
146                benchmark_cpu_parallelism(hash_limit, *num_cores)
147            }
148            Metric::MemCopy => benchmark_memory(memory_limit),
149            Metric::DiskSeqWrite => benchmark_disk_sequential_writes(disk_limit, dir)?,
150            Metric::DiskRndWrite => benchmark_disk_random_writes(disk_limit, dir)?,
151        };
152        Ok(score)
153    }
154
155    pub fn print_full_table(&self, dir: &Path) -> Result<()> {
156        info!("Running full machine benchmarks...");
157        let requirements = &SUBSTRATE_REFERENCE_HARDWARE.clone();
158        let mut results = Vec::new();
159        for requirement in &requirements.0 {
160            let result = self.run_benchmark(requirement, &dir)?;
161            results.push(result);
162        }
163        self.print_summary(requirements.clone(), results)?;
164        Ok(())
165    }
166
167    /// Prints a human-readable summary.
168    pub fn print_summary(
169        &self,
170        requirements: Requirements,
171        results: Vec<BenchResult>,
172    ) -> Result<()> {
173        // Use a table for nicer console output.
174        let mut table = Table::new();
175        table.set_header(["Category", "Function", "Score", "Minimum", "Result"]);
176        // Count how many passed and how many failed.
177        let (mut passed, mut failed) = (0, 0);
178        for (requirement, result) in requirements.0.iter().zip(results.iter()) {
179            if result.passed {
180                passed += 1
181            } else {
182                failed += 1
183            }
184
185            table.add_row(result.to_row(requirement));
186        }
187        // Print the table and a summary.
188        info!(
189            "\n{}\nFrom {} benchmarks in total, {} passed and {} failed ({:.0?}% fault tolerance).",
190            table,
191            passed + failed,
192            passed,
193            failed,
194            self.tolerance
195        );
196        // Print the final result.
197        if failed != 0 {
198            info!("The hardware fails to meet the requirements");
199            self.check_failed(Error::UnmetRequirement)?;
200        } else {
201            info!("The hardware meets the requirements ");
202        }
203        Ok(())
204    }
205
206    /// Returns `Ok` if [`self.allow_fail`] is set and otherwise the error argument.
207    fn check_failed(&self, e: Error) -> Result<()> {
208        if !self.allow_fail {
209            error!("Failing since --allow-fail is not set");
210            Err(sc_cli::Error::Application(Box::new(e)))
211        } else {
212            warn!("Ignoring error since --allow-fail is set: {:?}", e);
213            Ok(())
214        }
215    }
216
217    /// Validates the CLI arguments.
218    pub fn validate_args(&self) -> Result<()> {
219        if self.tolerance > 100.0 || self.tolerance < 0.0 {
220            return Err("The --tolerance argument is out of range".into());
221        }
222        Ok(())
223    }
224}
225
226impl BenchResult {
227    /// Format [`Self`] as row that can be printed in a table.
228    fn to_row(&self, req: &Requirement) -> Row {
229        let passed = if self.passed { "✅ Pass" } else { "❌ Fail" };
230        vec![
231            req.metric.category().into(),
232            req.metric.name().into(),
233            format!("{}", self.score),
234            format!("{}", req.minimum),
235            format!("{} ({: >5.1?} %)", passed, self.rel_score * 100.0),
236        ]
237        .into()
238    }
239}
240
241fn status_emoji(s: bool) -> String {
242    if s {
243        "✅".into()
244    } else {
245        "❌".into()
246    }
247}
248
249/// Whether the hardware requirements are met by the provided benchmark results.
250pub fn check_hardware(hwbench: &HwBench) -> bool {
251    info!("Performing quick hardware check...");
252    let req = &SUBSTRATE_REFERENCE_HARDWARE;
253
254    let mut cpu_ok = true;
255    let mut parallel_cpu_ok = true;
256    let mut mem_ok = true;
257    let mut dsk_seq_write_ok = true;
258    let mut dsk_rnd_write_ok = true;
259
260    for requirement in req.0.iter() {
261        match requirement.metric {
262            Metric::Blake2256 => {
263                if requirement.minimum > hwbench.cpu_hashrate_score {
264                    cpu_ok = false;
265                }
266                info!(
267                    "🏁 CPU score: {} ({})",
268                    hwbench.cpu_hashrate_score,
269                    format!(
270                        "{} Blake2256: expected minimum {}",
271                        status_emoji(cpu_ok),
272                        requirement.minimum
273                    )
274                );
275            }
276            Metric::Blake2256Parallel { .. } => {
277                if requirement.minimum > hwbench.parallel_cpu_hashrate_score {
278                    parallel_cpu_ok = false;
279                }
280                info!(
281                    "🏁 Parallel CPU score: {} ({})",
282                    hwbench.parallel_cpu_hashrate_score,
283                    format!(
284                        "{} Blake2256Parallel: expected minimum {}",
285                        status_emoji(parallel_cpu_ok),
286                        requirement.minimum
287                    )
288                );
289            }
290            Metric::MemCopy => {
291                if requirement.minimum > hwbench.memory_memcpy_score {
292                    mem_ok = false;
293                }
294                info!(
295                    "🏁 Memory score: {} ({})",
296                    hwbench.memory_memcpy_score,
297                    format!(
298                        "{} MemCopy: expected minimum {}",
299                        status_emoji(mem_ok),
300                        requirement.minimum
301                    )
302                );
303            }
304            Metric::DiskSeqWrite => {
305                if let Some(score) = hwbench.disk_sequential_write_score {
306                    if requirement.minimum > score {
307                        dsk_seq_write_ok = false;
308                    }
309                    info!(
310                        "🏁 Disk score (seq. writes): {} ({})",
311                        score,
312                        format!(
313                            "{} DiskSeqWrite: expected minimum {}",
314                            status_emoji(dsk_seq_write_ok),
315                            requirement.minimum
316                        )
317                    );
318                }
319            }
320            Metric::DiskRndWrite => {
321                if let Some(score) = hwbench.disk_random_write_score {
322                    if requirement.minimum > score {
323                        dsk_rnd_write_ok = false;
324                    }
325                    info!(
326                        "🏁 Disk score (rand. writes): {} ({})",
327                        score,
328                        format!(
329                            "{} DiskRndWrite: expected minimum {}",
330                            status_emoji(dsk_rnd_write_ok),
331                            requirement.minimum
332                        )
333                    );
334                }
335            }
336            Metric::Sr25519Verify => {}
337        }
338    }
339
340    cpu_ok && mem_ok && dsk_seq_write_ok && dsk_rnd_write_ok
341}