use crate::{ExecutionLimit, HwBench};
use sc_telemetry::SysInfo;
use sp_core::{sr25519, Pair};
use sp_io::crypto::sr25519_verify;
use sp_std::{fmt, fmt::Formatter, prelude::*};
use rand::{seq::SliceRandom, Rng, RngCore};
use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
use std::{
fs::File,
io::{Seek, SeekFrom, Write},
ops::{Deref, DerefMut},
path::{Path, PathBuf},
time::{Duration, Instant},
};
#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq)]
pub enum Metric {
Sr25519Verify,
Blake2256,
MemCopy,
DiskSeqWrite,
DiskRndWrite,
}
impl Metric {
pub fn category(&self) -> &'static str {
match self {
Self::Sr25519Verify | Self::Blake2256 => "CPU",
Self::MemCopy => "Memory",
Self::DiskSeqWrite | Self::DiskRndWrite => "Disk",
}
}
pub fn name(&self) -> &'static str {
match self {
Self::Sr25519Verify => "SR25519-Verify",
Self::Blake2256 => "BLAKE2-256",
Self::MemCopy => "Copy",
Self::DiskSeqWrite => "Seq Write",
Self::DiskRndWrite => "Rnd Write",
}
}
}
pub enum Unit {
GiBs,
MiBs,
KiBs,
}
impl fmt::Display for Unit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Unit::GiBs => "GiBs",
Unit::MiBs => "MiBs",
Unit::KiBs => "KiBs",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Throughput(f64);
const KIBIBYTE: f64 = (1 << 10) as f64;
const MEBIBYTE: f64 = (1 << 20) as f64;
const GIBIBYTE: f64 = (1 << 30) as f64;
impl Throughput {
pub fn from_kibs(kibs: f64) -> Throughput {
Throughput(kibs * KIBIBYTE)
}
pub fn from_mibs(mibs: f64) -> Throughput {
Throughput(mibs * MEBIBYTE)
}
pub fn from_gibs(gibs: f64) -> Throughput {
Throughput(gibs * GIBIBYTE)
}
pub fn as_bytes(&self) -> f64 {
self.0
}
pub fn as_kibs(&self) -> f64 {
self.0 / KIBIBYTE
}
pub fn as_mibs(&self) -> f64 {
self.0 / MEBIBYTE
}
pub fn as_gibs(&self) -> f64 {
self.0 / GIBIBYTE
}
pub fn normalize(&self) -> (f64, Unit) {
let bs = self.0;
if bs >= GIBIBYTE {
(self.as_gibs(), Unit::GiBs)
} else if bs >= MEBIBYTE {
(self.as_mibs(), Unit::MiBs)
} else {
(self.as_kibs(), Unit::KiBs)
}
}
}
impl fmt::Display for Throughput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (value, unit) = self.normalize();
write!(f, "{:.2?} {}", value, unit)
}
}
pub fn serialize_throughput<S>(throughput: &Throughput, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u64(throughput.as_mibs() as u64)
}
pub fn serialize_throughput_option<S>(
maybe_throughput: &Option<Throughput>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(throughput) = maybe_throughput {
return serializer.serialize_some(&(throughput.as_mibs() as u64))
}
serializer.serialize_none()
}
fn serialize_throughput_as_f64<S>(throughput: &Throughput, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_f64(throughput.as_mibs())
}
struct ThroughputVisitor;
impl<'de> Visitor<'de> for ThroughputVisitor {
type Value = Throughput;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
formatter.write_str("A value that is a f64.")
}
fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Throughput::from_mibs(value))
}
}
fn deserialize_throughput<'de, D>(deserializer: D) -> Result<Throughput, D::Error>
where
D: Deserializer<'de>,
{
Ok(deserializer.deserialize_f64(ThroughputVisitor))?
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Requirements(pub Vec<Requirement>);
#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq)]
pub struct Requirement {
pub metric: Metric,
#[serde(
serialize_with = "serialize_throughput_as_f64",
deserialize_with = "deserialize_throughput"
)]
pub minimum: Throughput,
}
#[inline(always)]
pub(crate) fn benchmark<E>(
name: &str,
size: usize,
max_iterations: usize,
max_duration: Duration,
mut run: impl FnMut() -> Result<(), E>,
) -> Result<Throughput, E> {
run()?;
let timestamp = Instant::now();
let mut elapsed = Duration::default();
let mut count = 0;
for _ in 0..max_iterations {
run()?;
count += 1;
elapsed = timestamp.elapsed();
if elapsed >= max_duration {
break
}
}
let score = Throughput::from_kibs((size * count) as f64 / (elapsed.as_secs_f64() * 1024.0));
log::trace!(
"Calculated {} of {} in {} iterations in {}ms",
name,
score,
count,
elapsed.as_millis()
);
Ok(score)
}
pub fn gather_sysinfo() -> SysInfo {
#[allow(unused_mut)]
let mut sysinfo = SysInfo {
cpu: None,
memory: None,
core_count: None,
linux_kernel: None,
linux_distro: None,
is_virtual_machine: None,
};
#[cfg(target_os = "linux")]
crate::sysinfo_linux::gather_linux_sysinfo(&mut sysinfo);
sysinfo
}
#[inline(never)]
fn clobber_slice<T>(slice: &mut [T]) {
assert!(!slice.is_empty());
unsafe {
let value = std::ptr::read_volatile(slice.as_ptr());
std::ptr::write_volatile(slice.as_mut_ptr(), value);
}
}
#[inline(never)]
fn clobber_value<T>(input: &mut T) {
unsafe {
let value = std::ptr::read_volatile(input);
std::ptr::write_volatile(input, value);
}
}
pub const DEFAULT_CPU_EXECUTION_LIMIT: ExecutionLimit =
ExecutionLimit::Both { max_iterations: 4 * 1024, max_duration: Duration::from_millis(100) };
pub fn benchmark_cpu(limit: ExecutionLimit) -> Throughput {
const SIZE: usize = 32 * 1024;
let mut buffer = Vec::new();
buffer.resize(SIZE, 0x66);
let mut hash = Default::default();
let run = || -> Result<(), ()> {
clobber_slice(&mut buffer);
hash = sp_core::hashing::blake2_256(&buffer);
clobber_slice(&mut hash);
Ok(())
};
benchmark("CPU score", SIZE, limit.max_iterations(), limit.max_duration(), run)
.expect("benchmark cannot fail; qed")
}
pub const DEFAULT_MEMORY_EXECUTION_LIMIT: ExecutionLimit =
ExecutionLimit::Both { max_iterations: 32, max_duration: Duration::from_millis(100) };
pub fn benchmark_memory(limit: ExecutionLimit) -> Throughput {
const SIZE: usize = 64 * 1024 * 1024;
let mut src = Vec::new();
let mut dst = Vec::new();
src.resize(SIZE, 0x66);
dst.resize(SIZE, 0x77);
let run = || -> Result<(), ()> {
clobber_slice(&mut src);
clobber_slice(&mut dst);
unsafe {
libc::memcpy(dst.as_mut_ptr().cast(), src.as_ptr().cast(), SIZE);
}
clobber_slice(&mut dst);
clobber_slice(&mut src);
Ok(())
};
benchmark("memory score", SIZE, limit.max_iterations(), limit.max_duration(), run)
.expect("benchmark cannot fail; qed")
}
struct TemporaryFile {
fp: Option<File>,
path: PathBuf,
}
impl Drop for TemporaryFile {
fn drop(&mut self) {
let _ = self.fp.take();
if let Err(error) = std::fs::remove_file(&self.path) {
log::warn!("Failed to remove the file used for the disk benchmark: {}", error);
}
}
}
impl Deref for TemporaryFile {
type Target = File;
fn deref(&self) -> &Self::Target {
self.fp.as_ref().expect("`fp` is None only during `drop`")
}
}
impl DerefMut for TemporaryFile {
fn deref_mut(&mut self) -> &mut Self::Target {
self.fp.as_mut().expect("`fp` is None only during `drop`")
}
}
fn rng() -> rand_pcg::Pcg64 {
rand_pcg::Pcg64::new(0xcafef00dd15ea5e5, 0xa02bdbf7bb3c0a7ac28fa16a64abf96)
}
fn random_data(size: usize) -> Vec<u8> {
let mut buffer = Vec::new();
buffer.resize(size, 0);
rng().fill(&mut buffer[..]);
buffer
}
pub const DEFAULT_DISK_EXECUTION_LIMIT: ExecutionLimit =
ExecutionLimit::Both { max_iterations: 32, max_duration: Duration::from_millis(300) };
pub fn benchmark_disk_sequential_writes(
limit: ExecutionLimit,
directory: &Path,
) -> Result<Throughput, String> {
const SIZE: usize = 64 * 1024 * 1024;
let buffer = random_data(SIZE);
let path = directory.join(".disk_bench_seq_wr.tmp");
let fp =
File::create(&path).map_err(|error| format!("failed to create a test file: {}", error))?;
let mut fp = TemporaryFile { fp: Some(fp), path };
fp.sync_all()
.map_err(|error| format!("failed to fsync the test file: {}", error))?;
let run = || {
fp.write_all(&buffer)
.map_err(|error| format!("failed to write to the test file: {}", error))?;
fp.sync_all()
.map_err(|error| format!("failed to fsync the test file: {}", error))?;
fp.seek(SeekFrom::Start(0))
.map_err(|error| format!("failed to seek to the start of the test file: {}", error))?;
Ok(())
};
benchmark(
"disk sequential write score",
SIZE,
limit.max_iterations(),
limit.max_duration(),
run,
)
}
pub fn benchmark_disk_random_writes(
limit: ExecutionLimit,
directory: &Path,
) -> Result<Throughput, String> {
const SIZE: usize = 64 * 1024 * 1024;
let buffer = random_data(SIZE);
let path = directory.join(".disk_bench_rand_wr.tmp");
let fp =
File::create(&path).map_err(|error| format!("failed to create a test file: {}", error))?;
let mut fp = TemporaryFile { fp: Some(fp), path };
fp.write_all(&buffer)
.map_err(|error| format!("failed to write to the test file: {}", error))?;
fp.sync_all()
.map_err(|error| format!("failed to fsync the test file: {}", error))?;
let mut positions = Vec::with_capacity(SIZE / 4096);
{
let mut position = 0;
while position < SIZE {
positions.push(position);
position += 4096;
}
}
positions.shuffle(&mut rng());
let run = || {
for &position in &positions {
fp.seek(SeekFrom::Start(position as u64))
.map_err(|error| format!("failed to seek in the test file: {}", error))?;
let chunk = &buffer[position..position + 2048];
fp.write_all(&chunk)
.map_err(|error| format!("failed to write to the test file: {}", error))?;
}
fp.sync_all()
.map_err(|error| format!("failed to fsync the test file: {}", error))?;
Ok(())
};
benchmark(
"disk random write score",
SIZE / 2,
limit.max_iterations(),
limit.max_duration(),
run,
)
}
pub fn benchmark_sr25519_verify(limit: ExecutionLimit) -> Throughput {
const INPUT_SIZE: usize = 32;
const ITERATION_SIZE: usize = 2048;
let pair = sr25519::Pair::from_string("//Alice", None).unwrap();
let mut rng = rng();
let mut msgs = Vec::new();
let mut sigs = Vec::new();
for _ in 0..ITERATION_SIZE {
let mut msg = vec![0u8; INPUT_SIZE];
rng.fill_bytes(&mut msg[..]);
sigs.push(pair.sign(&msg));
msgs.push(msg);
}
let run = || -> Result<(), String> {
for (sig, msg) in sigs.iter().zip(msgs.iter()) {
let mut ok = sr25519_verify(&sig, &msg[..], &pair.public());
clobber_value(&mut ok);
}
Ok(())
};
benchmark(
"sr25519 verification score",
INPUT_SIZE * ITERATION_SIZE,
limit.max_iterations(),
limit.max_duration(),
run,
)
.expect("sr25519 verification cannot fail; qed")
}
pub fn gather_hwbench(scratch_directory: Option<&Path>) -> HwBench {
#[allow(unused_mut)]
let mut hwbench = HwBench {
cpu_hashrate_score: benchmark_cpu(DEFAULT_CPU_EXECUTION_LIMIT),
memory_memcpy_score: benchmark_memory(DEFAULT_MEMORY_EXECUTION_LIMIT),
disk_sequential_write_score: None,
disk_random_write_score: None,
};
if let Some(scratch_directory) = scratch_directory {
hwbench.disk_sequential_write_score =
match benchmark_disk_sequential_writes(DEFAULT_DISK_EXECUTION_LIMIT, scratch_directory)
{
Ok(score) => Some(score),
Err(error) => {
log::warn!("Failed to run the sequential write disk benchmark: {}", error);
None
},
};
hwbench.disk_random_write_score =
match benchmark_disk_random_writes(DEFAULT_DISK_EXECUTION_LIMIT, scratch_directory) {
Ok(score) => Some(score),
Err(error) => {
log::warn!("Failed to run the random write disk benchmark: {}", error);
None
},
};
}
hwbench
}
impl Requirements {
pub fn check_hardware(&self, hwbench: &HwBench) -> bool {
for requirement in self.0.iter() {
match requirement.metric {
Metric::Blake2256 =>
if requirement.minimum > hwbench.cpu_hashrate_score {
return false
},
Metric::MemCopy =>
if requirement.minimum > hwbench.memory_memcpy_score {
return false
},
Metric::DiskSeqWrite =>
if let Some(score) = hwbench.disk_sequential_write_score {
if requirement.minimum > score {
return false
}
},
Metric::DiskRndWrite =>
if let Some(score) = hwbench.disk_random_write_score {
if requirement.minimum > score {
return false
}
},
Metric::Sr25519Verify => {},
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use sp_runtime::assert_eq_error_rate_float;
#[cfg(target_os = "linux")]
#[test]
fn test_gather_sysinfo_linux() {
let sysinfo = gather_sysinfo();
assert!(sysinfo.cpu.unwrap().len() > 0);
assert!(sysinfo.core_count.unwrap() > 0);
assert!(sysinfo.memory.unwrap() > 0);
assert_ne!(sysinfo.is_virtual_machine, None);
assert_ne!(sysinfo.linux_kernel, None);
assert_ne!(sysinfo.linux_distro, None);
}
#[test]
fn test_benchmark_cpu() {
assert!(benchmark_cpu(DEFAULT_CPU_EXECUTION_LIMIT) > Throughput::from_mibs(0.0));
}
#[test]
fn test_benchmark_memory() {
assert!(benchmark_memory(DEFAULT_MEMORY_EXECUTION_LIMIT) > Throughput::from_mibs(0.0));
}
#[test]
fn test_benchmark_disk_sequential_writes() {
assert!(
benchmark_disk_sequential_writes(DEFAULT_DISK_EXECUTION_LIMIT, "./".as_ref()).unwrap() >
Throughput::from_mibs(0.0)
);
}
#[test]
fn test_benchmark_disk_random_writes() {
assert!(
benchmark_disk_random_writes(DEFAULT_DISK_EXECUTION_LIMIT, "./".as_ref()).unwrap() >
Throughput::from_mibs(0.0)
);
}
#[test]
fn test_benchmark_sr25519_verify() {
assert!(
benchmark_sr25519_verify(ExecutionLimit::MaxIterations(1)) > Throughput::from_mibs(0.0)
);
}
#[test]
fn throughput_works() {
const EPS: f64 = 0.1;
let gib = Throughput::from_gibs(14.324);
assert_eq_error_rate_float!(14.324, gib.as_gibs(), EPS);
assert_eq_error_rate_float!(14667.776, gib.as_mibs(), EPS);
assert_eq_error_rate_float!(14667.776 * 1024.0, gib.as_kibs(), EPS);
assert_eq!("14.32 GiBs", gib.to_string());
let mib = Throughput::from_mibs(1029.0);
assert_eq!("1.00 GiBs", mib.to_string());
}
#[test]
fn hwbench_serialize_works() {
let hwbench = HwBench {
cpu_hashrate_score: Throughput::from_gibs(1.32),
memory_memcpy_score: Throughput::from_kibs(9342.432),
disk_sequential_write_score: Some(Throughput::from_kibs(4332.12)),
disk_random_write_score: None,
};
let serialized = serde_json::to_string(&hwbench).unwrap();
assert_eq!(serialized, "{\"cpu_hashrate_score\":1351,\"memory_memcpy_score\":9,\"disk_sequential_write_score\":4}");
}
}