1use crate::{ExecutionLimit, HwBench};
20
21use sc_telemetry::SysInfo;
22use sp_core::{sr25519, Pair};
23use sp_io::crypto::sr25519_verify;
24
25use core::f64;
26use derive_more::From;
27use rand::{seq::SliceRandom, Rng, RngCore};
28use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
29use std::{
30 borrow::Cow,
31 fmt::{self, Display, Formatter},
32 fs::File,
33 io::{Seek, SeekFrom, Write},
34 ops::{Deref, DerefMut},
35 path::{Path, PathBuf},
36 sync::{Arc, Barrier},
37 time::{Duration, Instant},
38};
39
40#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq)]
42pub enum Metric {
43 Sr25519Verify,
45 Blake2256,
47 Blake2256Parallel { num_cores: usize },
49 MemCopy,
51 DiskSeqWrite,
53 DiskRndWrite,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq)]
59pub struct CheckFailure {
60 pub metric: Metric,
62 pub expected: Throughput,
64 pub found: Throughput,
66}
67
68#[derive(Debug, Clone, PartialEq, From)]
70pub struct CheckFailures(pub Vec<CheckFailure>);
71
72impl Display for CheckFailures {
73 fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
74 write!(formatter, "Failed checks: ")?;
75 for failure in &self.0 {
76 write!(
77 formatter,
78 "{}(expected: {}, found: {}), ",
79 failure.metric.name(),
80 failure.expected,
81 failure.found
82 )?
83 }
84 Ok(())
85 }
86}
87
88impl Metric {
89 pub fn category(&self) -> &'static str {
91 match self {
92 Self::Sr25519Verify | Self::Blake2256 | Self::Blake2256Parallel { .. } => "CPU",
93 Self::MemCopy => "Memory",
94 Self::DiskSeqWrite | Self::DiskRndWrite => "Disk",
95 }
96 }
97
98 pub fn name(&self) -> Cow<'static, str> {
100 match self {
101 Self::Sr25519Verify => Cow::Borrowed("SR25519-Verify"),
102 Self::Blake2256 => Cow::Borrowed("BLAKE2-256"),
103 Self::Blake2256Parallel { num_cores } =>
104 Cow::Owned(format!("BLAKE2-256-Parallel-{}", num_cores)),
105 Self::MemCopy => Cow::Borrowed("Copy"),
106 Self::DiskSeqWrite => Cow::Borrowed("Seq Write"),
107 Self::DiskRndWrite => Cow::Borrowed("Rnd Write"),
108 }
109 }
110}
111
112pub enum Unit {
114 GiBs,
115 MiBs,
116 KiBs,
117}
118
119impl fmt::Display for Unit {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 f.write_str(match self {
122 Unit::GiBs => "GiBs",
123 Unit::MiBs => "MiBs",
124 Unit::KiBs => "KiBs",
125 })
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
131pub struct Throughput(f64);
132
133const KIBIBYTE: f64 = (1 << 10) as f64;
134const MEBIBYTE: f64 = (1 << 20) as f64;
135const GIBIBYTE: f64 = (1 << 30) as f64;
136
137impl Throughput {
138 pub fn from_kibs(kibs: f64) -> Throughput {
140 Throughput(kibs * KIBIBYTE)
141 }
142
143 pub fn from_mibs(mibs: f64) -> Throughput {
145 Throughput(mibs * MEBIBYTE)
146 }
147
148 pub fn from_gibs(gibs: f64) -> Throughput {
150 Throughput(gibs * GIBIBYTE)
151 }
152
153 pub fn as_bytes(&self) -> f64 {
155 self.0
156 }
157
158 pub fn as_kibs(&self) -> f64 {
160 self.0 / KIBIBYTE
161 }
162
163 pub fn as_mibs(&self) -> f64 {
165 self.0 / MEBIBYTE
166 }
167
168 pub fn as_gibs(&self) -> f64 {
170 self.0 / GIBIBYTE
171 }
172
173 pub fn normalize(&self) -> (f64, Unit) {
175 let bs = self.0;
176
177 if bs >= GIBIBYTE {
178 (self.as_gibs(), Unit::GiBs)
179 } else if bs >= MEBIBYTE {
180 (self.as_mibs(), Unit::MiBs)
181 } else {
182 (self.as_kibs(), Unit::KiBs)
183 }
184 }
185}
186
187impl fmt::Display for Throughput {
188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 let (value, unit) = self.normalize();
190 write!(f, "{:.2?} {}", value, unit)
191 }
192}
193
194pub fn serialize_throughput<S>(throughput: &Throughput, serializer: S) -> Result<S::Ok, S::Error>
196where
197 S: Serializer,
198{
199 serializer.serialize_u64(throughput.as_mibs() as u64)
200}
201
202pub fn serialize_throughput_option<S>(
204 maybe_throughput: &Option<Throughput>,
205 serializer: S,
206) -> Result<S::Ok, S::Error>
207where
208 S: Serializer,
209{
210 if let Some(throughput) = maybe_throughput {
211 return serializer.serialize_some(&(throughput.as_mibs() as u64))
212 }
213 serializer.serialize_none()
214}
215
216fn serialize_throughput_as_f64<S>(throughput: &Throughput, serializer: S) -> Result<S::Ok, S::Error>
218where
219 S: Serializer,
220{
221 serializer.serialize_f64(throughput.as_mibs())
222}
223
224struct ThroughputVisitor;
225impl<'de> Visitor<'de> for ThroughputVisitor {
226 type Value = Throughput;
227
228 fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
229 formatter.write_str("A value that is a f64.")
230 }
231
232 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
233 where
234 E: serde::de::Error,
235 {
236 Ok(Throughput::from_mibs(value))
237 }
238}
239
240fn deserialize_throughput<'de, D>(deserializer: D) -> Result<Throughput, D::Error>
241where
242 D: Deserializer<'de>,
243{
244 Ok(deserializer.deserialize_f64(ThroughputVisitor))?
245}
246
247#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
249pub struct Requirements(pub Vec<Requirement>);
250
251#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq)]
253pub struct Requirement {
254 pub metric: Metric,
256 #[serde(
258 serialize_with = "serialize_throughput_as_f64",
259 deserialize_with = "deserialize_throughput"
260 )]
261 pub minimum: Throughput,
262 #[serde(default)]
264 #[serde(skip_serializing_if = "core::ops::Not::not")]
265 pub validator_only: bool,
266}
267
268#[inline(always)]
269pub(crate) fn benchmark<E>(
270 name: &str,
271 size: usize,
272 max_iterations: usize,
273 max_duration: Duration,
274 mut run: impl FnMut() -> Result<(), E>,
275) -> Result<Throughput, E> {
276 run()?;
278
279 let timestamp = Instant::now();
281 let mut elapsed = Duration::default();
282 let mut count = 0;
283 for _ in 0..max_iterations {
284 run()?;
285
286 count += 1;
287 elapsed = timestamp.elapsed();
288
289 if elapsed >= max_duration {
290 break
291 }
292 }
293
294 let score = Throughput::from_kibs((size * count) as f64 / (elapsed.as_secs_f64() * 1024.0));
295 log::trace!(
296 "Calculated {} of {} in {} iterations in {}ms",
297 name,
298 score,
299 count,
300 elapsed.as_millis()
301 );
302 Ok(score)
303}
304
305pub fn gather_sysinfo() -> SysInfo {
307 #[allow(unused_mut)]
308 let mut sysinfo = SysInfo {
309 cpu: None,
310 memory: None,
311 core_count: None,
312 linux_kernel: None,
313 linux_distro: None,
314 is_virtual_machine: None,
315 };
316
317 #[cfg(target_os = "linux")]
318 crate::sysinfo_linux::gather_linux_sysinfo(&mut sysinfo);
319
320 #[cfg(target_os = "freebsd")]
321 crate::sysinfo_freebsd::gather_freebsd_sysinfo(&mut sysinfo);
322
323 sysinfo
324}
325
326#[inline(never)]
327fn clobber_slice<T>(slice: &mut [T]) {
328 assert!(!slice.is_empty());
329
330 unsafe {
341 let value = std::ptr::read_volatile(slice.as_ptr());
342 std::ptr::write_volatile(slice.as_mut_ptr(), value);
343 }
344}
345
346#[inline(never)]
347fn clobber_value<T>(input: &mut T) {
348 unsafe {
350 let value = std::ptr::read_volatile(input);
351 std::ptr::write_volatile(input, value);
352 }
353}
354
355pub const DEFAULT_CPU_EXECUTION_LIMIT: ExecutionLimit =
357 ExecutionLimit::Both { max_iterations: 4 * 1024, max_duration: Duration::from_millis(100) };
358
359pub fn benchmark_cpu(limit: ExecutionLimit) -> Throughput {
362 benchmark_cpu_parallelism(limit, 1)
363}
364
365pub fn benchmark_cpu_parallelism(limit: ExecutionLimit, refhw_num_cores: usize) -> Throughput {
371 const SIZE: usize = 32 * 1024;
384
385 let ready_to_run_benchmark = Arc::new(Barrier::new(refhw_num_cores));
386 let mut benchmark_threads = Vec::new();
387
388 for _ in 0..refhw_num_cores {
390 let ready_to_run_benchmark = ready_to_run_benchmark.clone();
391
392 let handle = std::thread::spawn(move || {
393 let mut buffer = Vec::new();
394 buffer.resize(SIZE, 0x66);
395 let mut hash = Default::default();
396
397 let run = || -> Result<(), ()> {
398 clobber_slice(&mut buffer);
399 hash = sp_crypto_hashing::blake2_256(&buffer);
400 clobber_slice(&mut hash);
401
402 Ok(())
403 };
404 ready_to_run_benchmark.wait();
405 benchmark("CPU score", SIZE, limit.max_iterations(), limit.max_duration(), run)
406 .expect("benchmark cannot fail; qed")
407 });
408 benchmark_threads.push(handle);
409 }
410
411 let average_score = benchmark_threads
412 .into_iter()
413 .map(|thread| thread.join().map(|throughput| throughput.as_kibs()).unwrap_or(0.0))
414 .sum::<f64>() /
415 refhw_num_cores as f64;
416 Throughput::from_kibs(average_score)
417}
418
419pub const DEFAULT_MEMORY_EXECUTION_LIMIT: ExecutionLimit =
421 ExecutionLimit::Both { max_iterations: 32, max_duration: Duration::from_millis(100) };
422
423pub fn benchmark_memory(limit: ExecutionLimit) -> Throughput {
429 const SIZE: usize = 64 * 1024 * 1024;
436
437 let mut src = Vec::new();
438 let mut dst = Vec::new();
439
440 src.resize(SIZE, 0x66);
443 dst.resize(SIZE, 0x77);
444
445 let run = || -> Result<(), ()> {
446 clobber_slice(&mut src);
447 clobber_slice(&mut dst);
448
449 unsafe {
452 libc::memcpy(dst.as_mut_ptr().cast(), src.as_ptr().cast(), SIZE);
455 }
456
457 clobber_slice(&mut dst);
458 clobber_slice(&mut src);
459
460 Ok(())
461 };
462
463 benchmark("memory score", SIZE, limit.max_iterations(), limit.max_duration(), run)
464 .expect("benchmark cannot fail; qed")
465}
466
467struct TemporaryFile {
468 fp: Option<File>,
469 path: PathBuf,
470}
471
472impl Drop for TemporaryFile {
473 fn drop(&mut self) {
474 let _ = self.fp.take();
475
476 if let Err(error) = std::fs::remove_file(&self.path) {
483 log::warn!("Failed to remove the file used for the disk benchmark: {}", error);
484 }
485 }
486}
487
488impl Deref for TemporaryFile {
489 type Target = File;
490 fn deref(&self) -> &Self::Target {
491 self.fp.as_ref().expect("`fp` is None only during `drop`")
492 }
493}
494
495impl DerefMut for TemporaryFile {
496 fn deref_mut(&mut self) -> &mut Self::Target {
497 self.fp.as_mut().expect("`fp` is None only during `drop`")
498 }
499}
500
501fn rng() -> rand_pcg::Pcg64 {
502 rand_pcg::Pcg64::new(0xcafef00dd15ea5e5, 0xa02bdbf7bb3c0a7ac28fa16a64abf96)
503}
504
505fn random_data(size: usize) -> Vec<u8> {
506 let mut buffer = Vec::new();
507 buffer.resize(size, 0);
508 rng().fill(&mut buffer[..]);
509 buffer
510}
511
512pub const DEFAULT_DISK_EXECUTION_LIMIT: ExecutionLimit =
515 ExecutionLimit::Both { max_iterations: 32, max_duration: Duration::from_millis(300) };
516
517pub fn benchmark_disk_sequential_writes(
518 limit: ExecutionLimit,
519 directory: &Path,
520) -> Result<Throughput, String> {
521 const SIZE: usize = 64 * 1024 * 1024;
522
523 let buffer = random_data(SIZE);
524 let path = directory.join(".disk_bench_seq_wr.tmp");
525
526 let fp =
527 File::create(&path).map_err(|error| format!("failed to create a test file: {}", error))?;
528
529 let mut fp = TemporaryFile { fp: Some(fp), path };
530
531 fp.sync_all()
532 .map_err(|error| format!("failed to fsync the test file: {}", error))?;
533
534 let run = || {
535 fp.write_all(&buffer)
537 .map_err(|error| format!("failed to write to the test file: {}", error))?;
538
539 fp.sync_all()
541 .map_err(|error| format!("failed to fsync the test file: {}", error))?;
542
543 fp.seek(SeekFrom::Start(0))
545 .map_err(|error| format!("failed to seek to the start of the test file: {}", error))?;
546
547 Ok(())
548 };
549
550 benchmark(
551 "disk sequential write score",
552 SIZE,
553 limit.max_iterations(),
554 limit.max_duration(),
555 run,
556 )
557}
558
559pub fn benchmark_disk_random_writes(
560 limit: ExecutionLimit,
561 directory: &Path,
562) -> Result<Throughput, String> {
563 const SIZE: usize = 64 * 1024 * 1024;
564
565 let buffer = random_data(SIZE);
566 let path = directory.join(".disk_bench_rand_wr.tmp");
567
568 let fp =
569 File::create(&path).map_err(|error| format!("failed to create a test file: {}", error))?;
570
571 let mut fp = TemporaryFile { fp: Some(fp), path };
572
573 fp.write_all(&buffer)
576 .map_err(|error| format!("failed to write to the test file: {}", error))?;
577
578 fp.sync_all()
579 .map_err(|error| format!("failed to fsync the test file: {}", error))?;
580
581 let mut positions = Vec::with_capacity(SIZE / 4096);
583 {
584 let mut position = 0;
585 while position < SIZE {
586 positions.push(position);
587 position += 4096;
588 }
589 }
590
591 positions.shuffle(&mut rng());
592
593 let run = || {
594 for &position in &positions {
595 fp.seek(SeekFrom::Start(position as u64))
596 .map_err(|error| format!("failed to seek in the test file: {}", error))?;
597
598 let chunk = &buffer[position..position + 2048];
605 fp.write_all(&chunk)
606 .map_err(|error| format!("failed to write to the test file: {}", error))?;
607 }
608
609 fp.sync_all()
610 .map_err(|error| format!("failed to fsync the test file: {}", error))?;
611
612 Ok(())
613 };
614
615 benchmark(
617 "disk random write score",
618 SIZE / 2,
619 limit.max_iterations(),
620 limit.max_duration(),
621 run,
622 )
623}
624
625pub fn benchmark_sr25519_verify(limit: ExecutionLimit) -> Throughput {
630 const INPUT_SIZE: usize = 32;
631 const ITERATION_SIZE: usize = 2048;
632 let pair = sr25519::Pair::from_string("//Alice", None).unwrap();
633
634 let mut rng = rng();
635 let mut msgs = Vec::new();
636 let mut sigs = Vec::new();
637
638 for _ in 0..ITERATION_SIZE {
639 let mut msg = vec![0u8; INPUT_SIZE];
640 rng.fill_bytes(&mut msg[..]);
641
642 sigs.push(pair.sign(&msg));
643 msgs.push(msg);
644 }
645
646 let run = || -> Result<(), String> {
647 for (sig, msg) in sigs.iter().zip(msgs.iter()) {
648 let mut ok = sr25519_verify(&sig, &msg[..], &pair.public());
649 clobber_value(&mut ok);
650 }
651 Ok(())
652 };
653 benchmark(
654 "sr25519 verification score",
655 INPUT_SIZE * ITERATION_SIZE,
656 limit.max_iterations(),
657 limit.max_duration(),
658 run,
659 )
660 .expect("sr25519 verification cannot fail; qed")
661}
662
663pub fn gather_hwbench(scratch_directory: Option<&Path>, requirements: &Requirements) -> HwBench {
669 let cpu_hashrate_score = benchmark_cpu(DEFAULT_CPU_EXECUTION_LIMIT);
670 let (parallel_cpu_hashrate_score, parallel_cpu_cores) = requirements
671 .0
672 .iter()
673 .filter_map(|req| {
674 if let Metric::Blake2256Parallel { num_cores } = req.metric {
675 Some((benchmark_cpu_parallelism(DEFAULT_CPU_EXECUTION_LIMIT, num_cores), num_cores))
676 } else {
677 None
678 }
679 })
680 .next()
681 .unwrap_or((cpu_hashrate_score, 1));
682 #[allow(unused_mut)]
683 let mut hwbench = HwBench {
684 cpu_hashrate_score,
685 parallel_cpu_hashrate_score,
686 parallel_cpu_cores,
687 memory_memcpy_score: benchmark_memory(DEFAULT_MEMORY_EXECUTION_LIMIT),
688 disk_sequential_write_score: None,
689 disk_random_write_score: None,
690 };
691
692 if let Some(scratch_directory) = scratch_directory {
693 hwbench.disk_sequential_write_score =
694 match benchmark_disk_sequential_writes(DEFAULT_DISK_EXECUTION_LIMIT, scratch_directory)
695 {
696 Ok(score) => Some(score),
697 Err(error) => {
698 log::warn!("Failed to run the sequential write disk benchmark: {}", error);
699 None
700 },
701 };
702
703 hwbench.disk_random_write_score =
704 match benchmark_disk_random_writes(DEFAULT_DISK_EXECUTION_LIMIT, scratch_directory) {
705 Ok(score) => Some(score),
706 Err(error) => {
707 log::warn!("Failed to run the random write disk benchmark: {}", error);
708 None
709 },
710 };
711 }
712
713 hwbench
714}
715
716impl Requirements {
717 pub fn check_hardware(
719 &self,
720 hwbench: &HwBench,
721 is_rc_authority: bool,
722 ) -> Result<(), CheckFailures> {
723 let mut failures = Vec::new();
724 for requirement in self.0.iter() {
725 if requirement.validator_only && !is_rc_authority {
726 continue
727 }
728
729 match requirement.metric {
730 Metric::Blake2256 =>
731 if requirement.minimum > hwbench.cpu_hashrate_score {
732 failures.push(CheckFailure {
733 metric: requirement.metric,
734 expected: requirement.minimum,
735 found: hwbench.cpu_hashrate_score,
736 });
737 },
738 Metric::Blake2256Parallel { .. } =>
739 if requirement.minimum > hwbench.parallel_cpu_hashrate_score {
740 failures.push(CheckFailure {
741 metric: requirement.metric,
742 expected: requirement.minimum,
743 found: hwbench.parallel_cpu_hashrate_score,
744 });
745 },
746 Metric::MemCopy =>
747 if requirement.minimum > hwbench.memory_memcpy_score {
748 failures.push(CheckFailure {
749 metric: requirement.metric,
750 expected: requirement.minimum,
751 found: hwbench.memory_memcpy_score,
752 });
753 },
754 Metric::DiskSeqWrite =>
755 if let Some(score) = hwbench.disk_sequential_write_score {
756 if requirement.minimum > score {
757 failures.push(CheckFailure {
758 metric: requirement.metric,
759 expected: requirement.minimum,
760 found: score,
761 });
762 }
763 },
764 Metric::DiskRndWrite =>
765 if let Some(score) = hwbench.disk_random_write_score {
766 if requirement.minimum > score {
767 failures.push(CheckFailure {
768 metric: requirement.metric,
769 expected: requirement.minimum,
770 found: score,
771 });
772 }
773 },
774 Metric::Sr25519Verify => {},
775 }
776 }
777 if failures.is_empty() {
778 Ok(())
779 } else {
780 Err(failures.into())
781 }
782 }
783}
784
785#[cfg(test)]
786mod tests {
787 use super::*;
788 use sp_runtime::assert_eq_error_rate_float;
789
790 #[cfg(target_os = "linux")]
791 #[test]
792 fn test_gather_sysinfo_linux() {
793 let sysinfo = gather_sysinfo();
794 assert!(sysinfo.cpu.unwrap().len() > 0);
795 assert!(sysinfo.core_count.unwrap() > 0);
796 assert!(sysinfo.memory.unwrap() > 0);
797 assert_ne!(sysinfo.is_virtual_machine, None);
798 assert_ne!(sysinfo.linux_kernel, None);
799 assert_ne!(sysinfo.linux_distro, None);
800 }
801
802 #[test]
803 fn test_benchmark_cpu() {
804 assert!(benchmark_cpu(DEFAULT_CPU_EXECUTION_LIMIT) > Throughput::from_mibs(0.0));
805 }
806
807 #[test]
808 fn test_benchmark_parallel_cpu() {
809 assert!(
810 benchmark_cpu_parallelism(DEFAULT_CPU_EXECUTION_LIMIT, 8) > Throughput::from_mibs(0.0)
811 );
812 }
813
814 #[test]
815 fn test_benchmark_memory() {
816 assert!(benchmark_memory(DEFAULT_MEMORY_EXECUTION_LIMIT) > Throughput::from_mibs(0.0));
817 }
818
819 #[test]
820 fn test_benchmark_disk_sequential_writes() {
821 assert!(
822 benchmark_disk_sequential_writes(DEFAULT_DISK_EXECUTION_LIMIT, "./".as_ref()).unwrap() >
823 Throughput::from_mibs(0.0)
824 );
825 }
826
827 #[test]
828 fn test_benchmark_disk_random_writes() {
829 assert!(
830 benchmark_disk_random_writes(DEFAULT_DISK_EXECUTION_LIMIT, "./".as_ref()).unwrap() >
831 Throughput::from_mibs(0.0)
832 );
833 }
834
835 #[test]
836 fn test_benchmark_sr25519_verify() {
837 assert!(
838 benchmark_sr25519_verify(ExecutionLimit::MaxIterations(1)) > Throughput::from_mibs(0.0)
839 );
840 }
841
842 #[test]
844 fn throughput_works() {
845 const EPS: f64 = 0.1;
847 let gib = Throughput::from_gibs(14.324);
848
849 assert_eq_error_rate_float!(14.324, gib.as_gibs(), EPS);
850 assert_eq_error_rate_float!(14667.776, gib.as_mibs(), EPS);
851 assert_eq_error_rate_float!(14667.776 * 1024.0, gib.as_kibs(), EPS);
852 assert_eq!("14.32 GiBs", gib.to_string());
853
854 let mib = Throughput::from_mibs(1029.0);
855 assert_eq!("1.00 GiBs", mib.to_string());
856 }
857
858 #[test]
860 fn hwbench_serialize_works() {
861 let hwbench = HwBench {
862 cpu_hashrate_score: Throughput::from_gibs(1.32),
863 parallel_cpu_hashrate_score: Throughput::from_gibs(1.32),
864 parallel_cpu_cores: 4,
865 memory_memcpy_score: Throughput::from_kibs(9342.432),
866 disk_sequential_write_score: Some(Throughput::from_kibs(4332.12)),
867 disk_random_write_score: None,
868 };
869
870 let serialized = serde_json::to_string(&hwbench).unwrap();
871 assert_eq!(serialized, "{\"cpu_hashrate_score\":1351,\"parallel_cpu_hashrate_score\":1351,\"parallel_cpu_cores\":4,\"memory_memcpy_score\":9,\"disk_sequential_write_score\":4}");
873 }
874}