substrate_benchmark_machine/
lib.rs1pub 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
36pub use hardware::SUBSTRATE_REFERENCE_HARDWARE;
38
39#[derive(Debug, Parser)]
48pub struct MachineCmd {
49 #[arg(long, short = 'd')]
51 pub base_path: Option<String>,
52
53 #[arg(long, short = 'f')]
55 pub full: bool,
56
57 #[arg(long)]
61 pub allow_fail: bool,
62
63 #[arg(long, default_value_t = 10.0, value_name = "PERCENT")]
68 pub tolerance: f64,
69
70 #[arg(long, default_value_t = 5.0, value_name = "SECONDS")]
72 pub verify_duration: f32,
73
74 #[arg(long, default_value_t = 5.0, value_name = "SECONDS")]
76 pub hash_duration: f32,
77
78 #[arg(long, default_value_t = 5.0, value_name = "SECONDS")]
80 pub memory_duration: f32,
81
82 #[arg(long, default_value_t = 5.0, value_name = "SECONDS")]
84 pub disk_duration: f32,
85}
86
87#[derive(Debug)]
89pub struct BenchResult {
90 passed: bool,
92
93 score: Throughput,
95
96 rel_score: f64,
100}
101
102#[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("Benchmark results are off by at least factor 100")]
112 BadResults,
113}
114
115impl MachineCmd {
116 pub fn run_benchmark(&self, requirement: &Requirement, dir: &Path) -> Result<BenchResult> {
118 let score = self.measure(&requirement.metric, dir)?;
121 let rel_score = score.as_bytes() / requirement.minimum.as_bytes();
122
123 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 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 pub fn print_summary(
169 &self,
170 requirements: Requirements,
171 results: Vec<BenchResult>,
172 ) -> Result<()> {
173 let mut table = Table::new();
175 table.set_header(["Category", "Function", "Score", "Minimum", "Result"]);
176 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 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 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 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 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 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
249pub 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}