mirror of
https://github.com/pezkuwichain/pezkuwi-telemetry.git
synced 2026-06-09 20:21:01 +00:00
Add per-chain aggregate software/hardware telemetry (#464)
* Add per-chain aggregate software/hardware telemetry * Fix tests' compilation * Add extra comments for the `Counter` struct * Replace the boolean argument with an enum * Rename `replace_hwbench` to `update_hwbench` * Move `Counter` into a separate file * Move `ChainStatsCollator` to `chain_stats.rs` * Fix incorrect key on the unknown table * Improve types for the stats component; get rid of `any`
This commit is contained in:
@@ -60,6 +60,7 @@ pub enum Payload {
|
||||
BlockImport(Block),
|
||||
NotifyFinalized(Finalized),
|
||||
AfgAuthoritySet(AfgAuthoritySet),
|
||||
HwBench(NodeHwBench),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
@@ -93,6 +94,14 @@ pub struct AfgAuthoritySet {
|
||||
pub authority_set_id: Box<str>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct NodeHwBench {
|
||||
pub cpu_hashrate_score: u64,
|
||||
pub memory_memcpy_score: u64,
|
||||
pub disk_sequential_write_score: Option<u64>,
|
||||
pub disk_random_write_score: Option<u64>,
|
||||
}
|
||||
|
||||
impl Payload {
|
||||
pub fn best_block(&self) -> Option<&Block> {
|
||||
match self {
|
||||
@@ -145,9 +154,13 @@ mod tests {
|
||||
name: "foo".into(),
|
||||
implementation: "foo".into(),
|
||||
version: "foo".into(),
|
||||
target_arch: Some("x86_64".into()),
|
||||
target_os: Some("linux".into()),
|
||||
target_env: Some("env".into()),
|
||||
validator: None,
|
||||
network_id: ArrayString::new(),
|
||||
startup_time: None,
|
||||
sysinfo: None,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -38,6 +38,40 @@ pub struct NodeDetails {
|
||||
pub validator: Option<Box<str>>,
|
||||
pub network_id: NetworkId,
|
||||
pub startup_time: Option<Box<str>>,
|
||||
pub target_os: Option<Box<str>>,
|
||||
pub target_arch: Option<Box<str>>,
|
||||
pub target_env: Option<Box<str>>,
|
||||
pub sysinfo: Option<NodeSysInfo>,
|
||||
}
|
||||
|
||||
/// Hardware and software information for the node.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct NodeSysInfo {
|
||||
/// The exact CPU model.
|
||||
pub cpu: Option<Box<str>>,
|
||||
/// The total amount of memory, in bytes.
|
||||
pub memory: Option<u64>,
|
||||
/// The number of physical CPU cores.
|
||||
pub core_count: Option<u32>,
|
||||
/// The Linux kernel version.
|
||||
pub linux_kernel: Option<Box<str>>,
|
||||
/// The exact Linux distribution used.
|
||||
pub linux_distro: Option<Box<str>>,
|
||||
/// Whether the node's running under a virtual machine.
|
||||
pub is_virtual_machine: Option<bool>,
|
||||
}
|
||||
|
||||
/// Hardware benchmark results for the node.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct NodeHwBench {
|
||||
/// The CPU speed, as measured in how many MB/s it can hash using the BLAKE2b-256 hash.
|
||||
pub cpu_hashrate_score: u64,
|
||||
/// Memory bandwidth in MB/s, calculated by measuring the throughput of `memcpy`.
|
||||
pub memory_memcpy_score: u64,
|
||||
/// Sequential disk write speed in MB/s.
|
||||
pub disk_sequential_write_score: Option<u64>,
|
||||
/// Random disk write speed in MB/s.
|
||||
pub disk_random_write_score: Option<u64>,
|
||||
}
|
||||
|
||||
/// A couple of node statistics.
|
||||
|
||||
@@ -514,6 +514,7 @@ impl InnerLoop {
|
||||
new_chain.finalized_block().height,
|
||||
new_chain.finalized_block().hash,
|
||||
));
|
||||
feed_serializer.push(feed_message::ChainStatsUpdate(new_chain.stats()));
|
||||
if let Some(bytes) = feed_serializer.into_finalized() {
|
||||
let _ = feed_channel.send(ToFeedWebsocket::Bytes(bytes));
|
||||
}
|
||||
|
||||
@@ -122,6 +122,7 @@ actions! {
|
||||
// We maintain existing IDs for backward compatibility.
|
||||
20: StaleNode,
|
||||
21: NodeIOUpdate<'_>,
|
||||
22: ChainStatsUpdate<'_>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -202,3 +203,30 @@ impl FeedMessageWrite for AddedNode<'_> {
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ChainStatsUpdate<'a>(pub &'a ChainStats);
|
||||
|
||||
#[derive(Serialize, PartialEq, Eq, Default)]
|
||||
pub struct Ranking<K> {
|
||||
pub list: Vec<(K, u64)>,
|
||||
pub other: u64,
|
||||
pub unknown: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, PartialEq, Eq, Default)]
|
||||
pub struct ChainStats {
|
||||
pub version: Ranking<String>,
|
||||
pub target_os: Ranking<String>,
|
||||
pub target_arch: Ranking<String>,
|
||||
pub cpu: Ranking<String>,
|
||||
pub memory: Ranking<(u32, Option<u32>)>,
|
||||
pub core_count: Ranking<u32>,
|
||||
pub linux_kernel: Ranking<String>,
|
||||
pub linux_distro: Ranking<String>,
|
||||
pub is_virtual_machine: Ranking<bool>,
|
||||
pub cpu_hashrate_score: Ranking<(u32, Option<u32>)>,
|
||||
pub memory_memcpy_score: Ranking<(u32, Option<u32>)>,
|
||||
pub disk_sequential_write_score: Ranking<(u32, Option<u32>)>,
|
||||
pub disk_random_write_score: Ranking<(u32, Option<u32>)>,
|
||||
}
|
||||
|
||||
@@ -21,10 +21,13 @@ use common::{id_type, time, DenseMap, MostSeen, NumStats};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::HashSet;
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::feed_message::{self, FeedMessageSerializer};
|
||||
use crate::feed_message::{self, ChainStats, FeedMessageSerializer};
|
||||
use crate::find_location;
|
||||
|
||||
use super::chain_stats::ChainStatsCollator;
|
||||
use super::counter::CounterValue;
|
||||
use super::node::Node;
|
||||
|
||||
id_type! {
|
||||
@@ -35,6 +38,7 @@ id_type! {
|
||||
pub type Label = Box<str>;
|
||||
|
||||
const STALE_TIMEOUT: u64 = 2 * 60 * 1000; // 2 minutes
|
||||
const STATS_UPDATE_INTERVAL: Duration = Duration::from_secs(5);
|
||||
|
||||
pub struct Chain {
|
||||
/// Labels that nodes use for this chain. We keep track of
|
||||
@@ -56,6 +60,12 @@ pub struct Chain {
|
||||
genesis_hash: BlockHash,
|
||||
/// Maximum number of nodes allowed to connect from this chain
|
||||
max_nodes: usize,
|
||||
/// Collator for the stats.
|
||||
stats_collator: ChainStatsCollator,
|
||||
/// Stats for this chain.
|
||||
stats: ChainStats,
|
||||
/// Timestamp of when the stats were last regenerated.
|
||||
stats_last_regenerated: Instant,
|
||||
}
|
||||
|
||||
pub enum AddNodeResult {
|
||||
@@ -105,6 +115,9 @@ impl Chain {
|
||||
timestamp: None,
|
||||
genesis_hash,
|
||||
max_nodes,
|
||||
stats_collator: Default::default(),
|
||||
stats: Default::default(),
|
||||
stats_last_regenerated: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +132,11 @@ impl Chain {
|
||||
return AddNodeResult::Overquota;
|
||||
}
|
||||
|
||||
let node_chain_label = &node.details().chain;
|
||||
let details = node.details();
|
||||
self.stats_collator
|
||||
.add_or_remove_node(details, None, CounterValue::Increment);
|
||||
|
||||
let node_chain_label = &details.chain;
|
||||
let label_result = self.labels.insert(node_chain_label);
|
||||
let node_id = self.nodes.add(node);
|
||||
|
||||
@@ -140,6 +157,10 @@ impl Chain {
|
||||
}
|
||||
};
|
||||
|
||||
let details = node.details();
|
||||
self.stats_collator
|
||||
.add_or_remove_node(details, node.hwbench(), CounterValue::Decrement);
|
||||
|
||||
let node_chain_label = &node.details().chain;
|
||||
let label_result = self.labels.remove(node_chain_label);
|
||||
|
||||
@@ -181,6 +202,19 @@ impl Chain {
|
||||
}
|
||||
return;
|
||||
}
|
||||
Payload::HwBench(ref hwbench) => {
|
||||
let new_hwbench = common::node_types::NodeHwBench {
|
||||
cpu_hashrate_score: hwbench.cpu_hashrate_score,
|
||||
memory_memcpy_score: hwbench.memory_memcpy_score,
|
||||
disk_sequential_write_score: hwbench.disk_sequential_write_score,
|
||||
disk_random_write_score: hwbench.disk_random_write_score,
|
||||
};
|
||||
let old_hwbench = node.update_hwbench(new_hwbench);
|
||||
self.stats_collator
|
||||
.update_hwbench(old_hwbench.as_ref(), CounterValue::Decrement);
|
||||
self.stats_collator
|
||||
.update_hwbench(node.hwbench(), CounterValue::Increment);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -210,6 +244,7 @@ impl Chain {
|
||||
let nodes_len = self.nodes.len();
|
||||
|
||||
self.update_stale_nodes(now, feed);
|
||||
self.regenerate_stats_if_necessary(feed);
|
||||
|
||||
let node = match self.nodes.get_mut(nid) {
|
||||
Some(node) => node,
|
||||
@@ -300,6 +335,21 @@ impl Chain {
|
||||
}
|
||||
}
|
||||
|
||||
fn regenerate_stats_if_necessary(&mut self, feed: &mut FeedMessageSerializer) {
|
||||
let now = Instant::now();
|
||||
let elapsed = now - self.stats_last_regenerated;
|
||||
if elapsed < STATS_UPDATE_INTERVAL {
|
||||
return;
|
||||
}
|
||||
|
||||
self.stats_last_regenerated = now;
|
||||
let new_stats = self.stats_collator.generate();
|
||||
if new_stats != self.stats {
|
||||
self.stats = new_stats;
|
||||
feed.push(feed_message::ChainStatsUpdate(&self.stats));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_node_location(
|
||||
&mut self,
|
||||
node_id: ChainNodeId,
|
||||
@@ -340,4 +390,7 @@ impl Chain {
|
||||
pub fn genesis_hash(&self) -> BlockHash {
|
||||
self.genesis_hash
|
||||
}
|
||||
pub fn stats(&self) -> &ChainStats {
|
||||
&self.stats
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
// Source code for the Substrate Telemetry Server.
|
||||
// Copyright (C) 2022 Parity Technologies (UK) Ltd.
|
||||
//
|
||||
// This program 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.
|
||||
//
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use super::counter::{Counter, CounterValue};
|
||||
use crate::feed_message::ChainStats;
|
||||
|
||||
// These are the benchmark scores generated on our reference hardware.
|
||||
const REFERENCE_CPU_SCORE: u64 = 1028;
|
||||
const REFERENCE_MEMORY_SCORE: u64 = 14899;
|
||||
const REFERENCE_DISK_SEQUENTIAL_WRITE_SCORE: u64 = 485;
|
||||
const REFERENCE_DISK_RANDOM_WRITE_SCORE: u64 = 222;
|
||||
|
||||
macro_rules! buckets {
|
||||
(@try $value:expr, $bucket_min:expr, $bucket_max:expr,) => {
|
||||
if $value < $bucket_max {
|
||||
return ($bucket_min, Some($bucket_max));
|
||||
}
|
||||
};
|
||||
|
||||
($value:expr, $bucket_min:expr, $bucket_max:expr, $($remaining:expr,)*) => {
|
||||
buckets! { @try $value, $bucket_min, $bucket_max, }
|
||||
buckets! { $value, $bucket_max, $($remaining,)* }
|
||||
};
|
||||
|
||||
($value:expr, $bucket_last:expr,) => {
|
||||
($bucket_last, None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Translates a given raw benchmark score into a relative measure
|
||||
/// of how the score compares to the reference score.
|
||||
///
|
||||
/// The value returned is the range (in percent) within which the given score
|
||||
/// falls into. For example, a value of `(90, Some(110))` means that the score
|
||||
/// is between 90% and 110% of the reference score, with the lower bound being
|
||||
/// inclusive and the upper bound being exclusive.
|
||||
fn bucket_score(score: u64, reference_score: u64) -> (u32, Option<u32>) {
|
||||
let relative_score = ((score as f64 / reference_score as f64) * 100.0) as u32;
|
||||
|
||||
buckets! {
|
||||
relative_score,
|
||||
0,
|
||||
10,
|
||||
30,
|
||||
50,
|
||||
70,
|
||||
90,
|
||||
110,
|
||||
130,
|
||||
150,
|
||||
200,
|
||||
300,
|
||||
400,
|
||||
500,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bucket_score() {
|
||||
assert_eq!(bucket_score(0, 100), (0, Some(10)));
|
||||
assert_eq!(bucket_score(9, 100), (0, Some(10)));
|
||||
assert_eq!(bucket_score(10, 100), (10, Some(30)));
|
||||
assert_eq!(bucket_score(29, 100), (10, Some(30)));
|
||||
assert_eq!(bucket_score(30, 100), (30, Some(50)));
|
||||
assert_eq!(bucket_score(100, 100), (90, Some(110)));
|
||||
assert_eq!(bucket_score(500, 100), (500, None));
|
||||
}
|
||||
|
||||
fn bucket_memory(memory: u64) -> (u32, Option<u32>) {
|
||||
let memory = memory / (1024 * 1024) / 1000;
|
||||
|
||||
buckets! {
|
||||
memory,
|
||||
1,
|
||||
2,
|
||||
4,
|
||||
6,
|
||||
8,
|
||||
10,
|
||||
16,
|
||||
24,
|
||||
32,
|
||||
48,
|
||||
56,
|
||||
64,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ChainStatsCollator {
|
||||
version: Counter<String>,
|
||||
target_os: Counter<String>,
|
||||
target_arch: Counter<String>,
|
||||
cpu: Counter<String>,
|
||||
memory: Counter<(u32, Option<u32>)>,
|
||||
core_count: Counter<u32>,
|
||||
linux_kernel: Counter<String>,
|
||||
linux_distro: Counter<String>,
|
||||
is_virtual_machine: Counter<bool>,
|
||||
cpu_hashrate_score: Counter<(u32, Option<u32>)>,
|
||||
memory_memcpy_score: Counter<(u32, Option<u32>)>,
|
||||
disk_sequential_write_score: Counter<(u32, Option<u32>)>,
|
||||
disk_random_write_score: Counter<(u32, Option<u32>)>,
|
||||
}
|
||||
|
||||
impl ChainStatsCollator {
|
||||
pub fn add_or_remove_node(
|
||||
&mut self,
|
||||
details: &common::node_types::NodeDetails,
|
||||
hwbench: Option<&common::node_types::NodeHwBench>,
|
||||
op: CounterValue,
|
||||
) {
|
||||
self.version.modify(Some(&*details.version), op);
|
||||
|
||||
self.target_os
|
||||
.modify(details.target_os.as_ref().map(|value| &**value), op);
|
||||
|
||||
self.target_arch
|
||||
.modify(details.target_arch.as_ref().map(|value| &**value), op);
|
||||
|
||||
let sysinfo = details.sysinfo.as_ref();
|
||||
self.cpu.modify(
|
||||
sysinfo
|
||||
.and_then(|sysinfo| sysinfo.cpu.as_ref())
|
||||
.map(|value| &**value),
|
||||
op,
|
||||
);
|
||||
|
||||
let memory = sysinfo.and_then(|sysinfo| sysinfo.memory.map(bucket_memory));
|
||||
self.memory.modify(memory.as_ref(), op);
|
||||
|
||||
self.core_count
|
||||
.modify(sysinfo.and_then(|sysinfo| sysinfo.core_count.as_ref()), op);
|
||||
|
||||
self.linux_kernel.modify(
|
||||
sysinfo
|
||||
.and_then(|sysinfo| sysinfo.linux_kernel.as_ref())
|
||||
.map(|value| &**value),
|
||||
op,
|
||||
);
|
||||
|
||||
self.linux_distro.modify(
|
||||
sysinfo
|
||||
.and_then(|sysinfo| sysinfo.linux_distro.as_ref())
|
||||
.map(|value| &**value),
|
||||
op,
|
||||
);
|
||||
|
||||
self.is_virtual_machine.modify(
|
||||
sysinfo.and_then(|sysinfo| sysinfo.is_virtual_machine.as_ref()),
|
||||
op,
|
||||
);
|
||||
|
||||
self.update_hwbench(hwbench, op);
|
||||
}
|
||||
|
||||
pub fn update_hwbench(
|
||||
&mut self,
|
||||
hwbench: Option<&common::node_types::NodeHwBench>,
|
||||
op: CounterValue,
|
||||
) {
|
||||
self.cpu_hashrate_score.modify(
|
||||
hwbench
|
||||
.map(|hwbench| bucket_score(hwbench.cpu_hashrate_score, REFERENCE_CPU_SCORE))
|
||||
.as_ref(),
|
||||
op,
|
||||
);
|
||||
|
||||
self.memory_memcpy_score.modify(
|
||||
hwbench
|
||||
.map(|hwbench| bucket_score(hwbench.memory_memcpy_score, REFERENCE_MEMORY_SCORE))
|
||||
.as_ref(),
|
||||
op,
|
||||
);
|
||||
|
||||
self.disk_sequential_write_score.modify(
|
||||
hwbench
|
||||
.and_then(|hwbench| hwbench.disk_sequential_write_score)
|
||||
.map(|score| bucket_score(score, REFERENCE_DISK_SEQUENTIAL_WRITE_SCORE))
|
||||
.as_ref(),
|
||||
op,
|
||||
);
|
||||
|
||||
self.disk_random_write_score.modify(
|
||||
hwbench
|
||||
.and_then(|hwbench| hwbench.disk_random_write_score)
|
||||
.map(|score| bucket_score(score, REFERENCE_DISK_RANDOM_WRITE_SCORE))
|
||||
.as_ref(),
|
||||
op,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn generate(&self) -> ChainStats {
|
||||
ChainStats {
|
||||
version: self.version.generate_ranking_top(10),
|
||||
target_os: self.target_os.generate_ranking_top(10),
|
||||
target_arch: self.target_arch.generate_ranking_top(10),
|
||||
cpu: self.cpu.generate_ranking_top(10),
|
||||
memory: self.memory.generate_ranking_ordered(),
|
||||
core_count: self.core_count.generate_ranking_top(10),
|
||||
linux_kernel: self.linux_kernel.generate_ranking_top(10),
|
||||
linux_distro: self.linux_distro.generate_ranking_top(10),
|
||||
is_virtual_machine: self.is_virtual_machine.generate_ranking_ordered(),
|
||||
cpu_hashrate_score: self.cpu_hashrate_score.generate_ranking_top(10),
|
||||
memory_memcpy_score: self.memory_memcpy_score.generate_ranking_ordered(),
|
||||
disk_sequential_write_score: self
|
||||
.disk_sequential_write_score
|
||||
.generate_ranking_ordered(),
|
||||
disk_random_write_score: self.disk_random_write_score.generate_ranking_ordered(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// Source code for the Substrate Telemetry Server.
|
||||
// Copyright (C) 2022 Parity Technologies (UK) Ltd.
|
||||
//
|
||||
// This program 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.
|
||||
//
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::feed_message::Ranking;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A data structure which counts how many occurences of a given key we've seen.
|
||||
#[derive(Default)]
|
||||
pub struct Counter<K> {
|
||||
/// A map containing the number of occurences of a given key.
|
||||
///
|
||||
/// If there are none then the entry is removed.
|
||||
map: HashMap<K, u64>,
|
||||
|
||||
/// The number of occurences where the key is `None`.
|
||||
empty: u64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum CounterValue {
|
||||
Increment,
|
||||
Decrement,
|
||||
}
|
||||
|
||||
impl<K> Counter<K>
|
||||
where
|
||||
K: Sized + std::hash::Hash + Eq,
|
||||
{
|
||||
/// Either adds or removes a single occurence of a given `key`.
|
||||
pub fn modify<'a, Q>(&mut self, key: Option<&'a Q>, op: CounterValue)
|
||||
where
|
||||
Q: ?Sized + std::hash::Hash + Eq,
|
||||
K: std::borrow::Borrow<Q>,
|
||||
Q: std::borrow::ToOwned<Owned = K>,
|
||||
{
|
||||
if let Some(key) = key {
|
||||
if let Some(entry) = self.map.get_mut(key) {
|
||||
match op {
|
||||
CounterValue::Increment => {
|
||||
*entry += 1;
|
||||
}
|
||||
CounterValue::Decrement => {
|
||||
*entry -= 1;
|
||||
if *entry == 0 {
|
||||
// Don't keep entries for which there are no hits.
|
||||
self.map.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert_eq!(op, CounterValue::Increment);
|
||||
self.map.insert(key.to_owned(), 1);
|
||||
}
|
||||
} else {
|
||||
match op {
|
||||
CounterValue::Increment => {
|
||||
self.empty += 1;
|
||||
}
|
||||
CounterValue::Decrement => {
|
||||
self.empty -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a top-N table of the most common keys.
|
||||
pub fn generate_ranking_top(&self, max_count: usize) -> Ranking<K>
|
||||
where
|
||||
K: Clone,
|
||||
{
|
||||
let mut all: Vec<(&K, u64)> = self.map.iter().map(|(key, count)| (key, *count)).collect();
|
||||
all.sort_unstable_by_key(|&(_, count)| !count);
|
||||
|
||||
let list = all
|
||||
.iter()
|
||||
.take(max_count)
|
||||
.map(|&(key, count)| (key.clone(), count))
|
||||
.collect();
|
||||
|
||||
let other = all
|
||||
.iter()
|
||||
.skip(max_count)
|
||||
.fold(0, |sum, (_, count)| sum + *count);
|
||||
|
||||
Ranking {
|
||||
list,
|
||||
other,
|
||||
unknown: self.empty,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a sorted table of all of the keys.
|
||||
pub fn generate_ranking_ordered(&self) -> Ranking<K>
|
||||
where
|
||||
K: Copy + Clone + Ord,
|
||||
{
|
||||
let mut list: Vec<(K, u64)> = self.map.iter().map(|(key, count)| (*key, *count)).collect();
|
||||
list.sort_unstable_by_key(|&(key, count)| (key, !count));
|
||||
|
||||
Ranking {
|
||||
list,
|
||||
other: 0,
|
||||
unknown: self.empty,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
mod chain;
|
||||
mod chain_stats;
|
||||
mod counter;
|
||||
mod node;
|
||||
|
||||
mod state;
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
use crate::find_location;
|
||||
use common::node_message::SystemInterval;
|
||||
use common::node_types::{
|
||||
Block, BlockDetails, NodeDetails, NodeHardware, NodeIO, NodeLocation, NodeStats, Timestamp,
|
||||
Block, BlockDetails, NodeDetails, NodeHardware, NodeHwBench, NodeIO, NodeLocation, NodeStats,
|
||||
Timestamp,
|
||||
};
|
||||
use common::time;
|
||||
|
||||
@@ -47,6 +48,8 @@ pub struct Node {
|
||||
stale: bool,
|
||||
/// Unix timestamp for when node started up (falls back to connection time)
|
||||
startup_time: Option<Timestamp>,
|
||||
/// Hardware benchmark results for the node
|
||||
hwbench: Option<NodeHwBench>,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
@@ -67,6 +70,7 @@ impl Node {
|
||||
location: None,
|
||||
stale: false,
|
||||
startup_time,
|
||||
hwbench: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +114,14 @@ impl Node {
|
||||
&self.best
|
||||
}
|
||||
|
||||
pub fn hwbench(&self) -> Option<&NodeHwBench> {
|
||||
self.hwbench.as_ref()
|
||||
}
|
||||
|
||||
pub fn update_hwbench(&mut self, hwbench: NodeHwBench) -> Option<NodeHwBench> {
|
||||
self.hwbench.replace(hwbench)
|
||||
}
|
||||
|
||||
pub fn update_block(&mut self, block: Block) -> bool {
|
||||
if block.height > self.best.block.height {
|
||||
self.stale = false;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use super::node::Node;
|
||||
use crate::feed_message::FeedMessageSerializer;
|
||||
use crate::feed_message::{ChainStats, FeedMessageSerializer};
|
||||
use crate::find_location;
|
||||
use common::node_message::Payload;
|
||||
use common::node_types::{Block, BlockHash, NodeDetails, Timestamp};
|
||||
@@ -277,6 +277,9 @@ impl<'a> StateChain<'a> {
|
||||
pub fn nodes_slice(&self) -> &[Option<Node>] {
|
||||
self.chain.nodes_slice()
|
||||
}
|
||||
pub fn stats(&self) -> &ChainStats {
|
||||
self.chain.stats()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -289,10 +292,14 @@ mod test {
|
||||
chain: chain.into(),
|
||||
name: name.into(),
|
||||
implementation: "Bar".into(),
|
||||
target_arch: Some("x86_64".into()),
|
||||
target_os: Some("linux".into()),
|
||||
target_env: Some("env".into()),
|
||||
version: "0.1".into(),
|
||||
validator: None,
|
||||
network_id: NetworkId::new(),
|
||||
startup_time: None,
|
||||
sysinfo: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,8 @@ pub enum Payload {
|
||||
NotifyFinalized(Finalized),
|
||||
#[serde(rename = "afg.authority_set")]
|
||||
AfgAuthoritySet(AfgAuthoritySet),
|
||||
#[serde(rename = "sysinfo.hwbench")]
|
||||
HwBench(NodeHwBench),
|
||||
}
|
||||
|
||||
impl From<Payload> for internal::Payload {
|
||||
@@ -86,6 +88,7 @@ impl From<Payload> for internal::Payload {
|
||||
Payload::BlockImport(m) => internal::Payload::BlockImport(m.into()),
|
||||
Payload::NotifyFinalized(m) => internal::Payload::NotifyFinalized(m.into()),
|
||||
Payload::AfgAuthoritySet(m) => internal::Payload::AfgAuthoritySet(m.into()),
|
||||
Payload::HwBench(m) => internal::Payload::HwBench(m.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,6 +186,59 @@ impl From<Block> for node_types::Block {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct NodeSysInfo {
|
||||
pub cpu: Option<Box<str>>,
|
||||
pub memory: Option<u64>,
|
||||
pub core_count: Option<u32>,
|
||||
pub linux_kernel: Option<Box<str>>,
|
||||
pub linux_distro: Option<Box<str>>,
|
||||
pub is_virtual_machine: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<NodeSysInfo> for node_types::NodeSysInfo {
|
||||
fn from(sysinfo: NodeSysInfo) -> Self {
|
||||
node_types::NodeSysInfo {
|
||||
cpu: sysinfo.cpu,
|
||||
memory: sysinfo.memory,
|
||||
core_count: sysinfo.core_count,
|
||||
linux_kernel: sysinfo.linux_kernel,
|
||||
linux_distro: sysinfo.linux_distro,
|
||||
is_virtual_machine: sysinfo.is_virtual_machine,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct NodeHwBench {
|
||||
pub cpu_hashrate_score: u64,
|
||||
pub memory_memcpy_score: u64,
|
||||
pub disk_sequential_write_score: Option<u64>,
|
||||
pub disk_random_write_score: Option<u64>,
|
||||
}
|
||||
|
||||
impl From<NodeHwBench> for node_types::NodeHwBench {
|
||||
fn from(hwbench: NodeHwBench) -> Self {
|
||||
node_types::NodeHwBench {
|
||||
cpu_hashrate_score: hwbench.cpu_hashrate_score,
|
||||
memory_memcpy_score: hwbench.memory_memcpy_score,
|
||||
disk_sequential_write_score: hwbench.disk_sequential_write_score,
|
||||
disk_random_write_score: hwbench.disk_random_write_score,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NodeHwBench> for internal::NodeHwBench {
|
||||
fn from(msg: NodeHwBench) -> Self {
|
||||
internal::NodeHwBench {
|
||||
cpu_hashrate_score: msg.cpu_hashrate_score,
|
||||
memory_memcpy_score: msg.memory_memcpy_score,
|
||||
disk_sequential_write_score: msg.disk_sequential_write_score,
|
||||
disk_random_write_score: msg.disk_random_write_score,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct NodeDetails {
|
||||
pub chain: Box<str>,
|
||||
@@ -192,10 +248,30 @@ pub struct NodeDetails {
|
||||
pub validator: Option<Box<str>>,
|
||||
pub network_id: node_types::NetworkId,
|
||||
pub startup_time: Option<Box<str>>,
|
||||
pub target_os: Option<Box<str>>,
|
||||
pub target_arch: Option<Box<str>>,
|
||||
pub target_env: Option<Box<str>>,
|
||||
pub sysinfo: Option<NodeSysInfo>,
|
||||
}
|
||||
|
||||
impl From<NodeDetails> for node_types::NodeDetails {
|
||||
fn from(details: NodeDetails) -> Self {
|
||||
fn from(mut details: NodeDetails) -> Self {
|
||||
// Migrate old-style `version` to the split metrics.
|
||||
// TODO: Remove this once everyone updates their nodes.
|
||||
if details.target_os.is_none()
|
||||
&& details.target_arch.is_none()
|
||||
&& details.target_env.is_none()
|
||||
{
|
||||
if let Some((version, target_arch, target_os, target_env)) =
|
||||
split_old_style_version(&details.version)
|
||||
{
|
||||
details.target_arch = Some(target_arch.into());
|
||||
details.target_os = Some(target_os.into());
|
||||
details.target_env = Some(target_env.into());
|
||||
details.version = version.into();
|
||||
}
|
||||
}
|
||||
|
||||
node_types::NodeDetails {
|
||||
chain: details.chain,
|
||||
name: details.name,
|
||||
@@ -204,6 +280,10 @@ impl From<NodeDetails> for node_types::NodeDetails {
|
||||
validator: details.validator,
|
||||
network_id: details.network_id,
|
||||
startup_time: details.startup_time,
|
||||
target_os: details.target_os,
|
||||
target_arch: details.target_arch,
|
||||
target_env: details.target_env,
|
||||
sysinfo: details.sysinfo.map(|sysinfo| sysinfo.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,6 +291,52 @@ impl From<NodeDetails> for node_types::NodeDetails {
|
||||
type NodeMessageId = u64;
|
||||
type BlockNumber = u64;
|
||||
|
||||
fn is_version_or_hash(name: &str) -> bool {
|
||||
name.bytes().all(|byte| {
|
||||
byte.is_ascii_digit()
|
||||
|| byte == b'.'
|
||||
|| byte == b'a'
|
||||
|| byte == b'b'
|
||||
|| byte == b'c'
|
||||
|| byte == b'd'
|
||||
|| byte == b'e'
|
||||
|| byte == b'f'
|
||||
})
|
||||
}
|
||||
|
||||
/// Split an old style version string into its version + target_arch + target_os + target_arch parts.
|
||||
fn split_old_style_version(version_and_target: &str) -> Option<(&str, &str, &str, &str)> {
|
||||
// Old style versions are composed of the following parts:
|
||||
// $version-$commit_hash-$arch-$os-$env
|
||||
// where $commit_hash and $env are optional.
|
||||
//
|
||||
// For example these are all valid:
|
||||
// 0.9.17-75dd6c7d0-x86_64-linux-gnu
|
||||
// 0.9.17-75dd6c7d0-x86_64-linux
|
||||
// 0.9.17-x86_64-linux-gnu
|
||||
// 0.9.17-x86_64-linux
|
||||
// 2.0.0-alpha.5-da487d19d-x86_64-linux
|
||||
|
||||
let mut iter = version_and_target.rsplit('-').take(3).skip(2);
|
||||
|
||||
// This will one of these: $arch, $commit_hash, $version
|
||||
let item = iter.next()?;
|
||||
|
||||
let target_offset = if is_version_or_hash(item) {
|
||||
item.as_ptr() as usize + item.len() + 1
|
||||
} else {
|
||||
item.as_ptr() as usize
|
||||
} - version_and_target.as_ptr() as usize;
|
||||
|
||||
let version = version_and_target.get(0..target_offset - 1)?;
|
||||
let mut target = version_and_target.get(target_offset..)?.split('-');
|
||||
let target_arch = target.next()?;
|
||||
let target_os = target.next()?;
|
||||
let target_env = target.next().unwrap_or("");
|
||||
|
||||
Some((version, target_arch, target_os, target_env))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -279,4 +405,46 @@ mod tests {
|
||||
"message did not match the expected output",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_old_style_version_works() {
|
||||
let (version, target_arch, target_os, target_env) =
|
||||
split_old_style_version("0.9.17-75dd6c7d0-x86_64-linux-gnu").unwrap();
|
||||
assert_eq!(version, "0.9.17-75dd6c7d0");
|
||||
assert_eq!(target_arch, "x86_64");
|
||||
assert_eq!(target_os, "linux");
|
||||
assert_eq!(target_env, "gnu");
|
||||
|
||||
let (version, target_arch, target_os, target_env) =
|
||||
split_old_style_version("0.9.17-75dd6c7d0-x86_64-linux").unwrap();
|
||||
assert_eq!(version, "0.9.17-75dd6c7d0");
|
||||
assert_eq!(target_arch, "x86_64");
|
||||
assert_eq!(target_os, "linux");
|
||||
assert_eq!(target_env, "");
|
||||
|
||||
let (version, target_arch, target_os, target_env) =
|
||||
split_old_style_version("0.9.17-x86_64-linux-gnu").unwrap();
|
||||
assert_eq!(version, "0.9.17");
|
||||
assert_eq!(target_arch, "x86_64");
|
||||
assert_eq!(target_os, "linux");
|
||||
assert_eq!(target_env, "gnu");
|
||||
|
||||
let (version, target_arch, target_os, target_env) =
|
||||
split_old_style_version("0.9.17-x86_64-linux").unwrap();
|
||||
assert_eq!(version, "0.9.17");
|
||||
assert_eq!(target_arch, "x86_64");
|
||||
assert_eq!(target_os, "linux");
|
||||
assert_eq!(target_env, "");
|
||||
|
||||
let (version, target_arch, target_os, target_env) =
|
||||
split_old_style_version("2.0.0-alpha.5-da487d19d-x86_64-linux").unwrap();
|
||||
assert_eq!(version, "2.0.0-alpha.5-da487d19d");
|
||||
assert_eq!(target_arch, "x86_64");
|
||||
assert_eq!(target_os, "linux");
|
||||
assert_eq!(target_env, "");
|
||||
|
||||
assert_eq!(split_old_style_version(""), None);
|
||||
assert_eq!(split_old_style_version("a"), None);
|
||||
assert_eq!(split_old_style_version("a-b"), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,7 @@ export default class App extends React.Component<{}, {}> {
|
||||
sortBy: this.sortBy.get(),
|
||||
selectedColumns: this.selectedColumns(this.settings.raw()),
|
||||
tab,
|
||||
chainStats: null,
|
||||
});
|
||||
this.appState = this.appUpdate({});
|
||||
|
||||
|
||||
@@ -362,6 +362,11 @@ export class Connection {
|
||||
break;
|
||||
}
|
||||
|
||||
case ACTIONS.ChainStatsUpdate: {
|
||||
this.appUpdate({ chainStats: message.payload });
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
ChainLabel,
|
||||
GenesisHash,
|
||||
AuthoritySetInfo,
|
||||
ChainStats,
|
||||
} from './types';
|
||||
|
||||
export const ACTIONS = {
|
||||
@@ -62,6 +63,7 @@ export const ACTIONS = {
|
||||
AfgAuthoritySet: 0x13 as 0x13,
|
||||
StaleNode: 0x14 as 0x14,
|
||||
NodeIO: 0x15 as 0x15,
|
||||
ChainStatsUpdate: 0x16 as 0x16,
|
||||
};
|
||||
|
||||
export type Action = typeof ACTIONS[keyof typeof ACTIONS];
|
||||
@@ -190,6 +192,11 @@ export namespace Variants {
|
||||
action: typeof ACTIONS.StaleNode;
|
||||
payload: NodeId;
|
||||
}
|
||||
|
||||
export interface ChainStatsUpdate extends MessageBase {
|
||||
action: typeof ACTIONS.ChainStatsUpdate;
|
||||
payload: ChainStats;
|
||||
}
|
||||
}
|
||||
|
||||
export type Message =
|
||||
@@ -214,7 +221,8 @@ export type Message =
|
||||
| Variants.AfgAuthoritySet
|
||||
| Variants.StaleNodeMessage
|
||||
| Variants.PongMessage
|
||||
| Variants.NodeIOMessage;
|
||||
| Variants.NodeIOMessage
|
||||
| Variants.ChainStatsUpdate;
|
||||
|
||||
/**
|
||||
* Data type to be sent to the feed. Passing through strings means we can only serialize once,
|
||||
|
||||
@@ -87,3 +87,27 @@ export declare type ImplicitPrecommit = Opaque<boolean, 'ImplicitPrecommit'>;
|
||||
export declare type ImplicitPrevote = Opaque<boolean, 'ImplicitPrevote'>;
|
||||
export declare type ImplicitFinalized = Opaque<boolean, 'ImplicitFinalized'>;
|
||||
export declare type ImplicitPointer = Opaque<BlockNumber, 'ImplicitPointer'>;
|
||||
|
||||
export type Ranking<T> = {
|
||||
list: Array<[T, number]>;
|
||||
other: number;
|
||||
unknown: number;
|
||||
};
|
||||
|
||||
export type Range = [number, number | null];
|
||||
|
||||
export type ChainStats = {
|
||||
version: Maybe<Ranking<string>>;
|
||||
target_os: Maybe<Ranking<string>>;
|
||||
target_arch: Maybe<Ranking<string>>;
|
||||
cpu: Maybe<Ranking<string>>;
|
||||
core_count: Maybe<Ranking<number>>;
|
||||
memory: Maybe<Ranking<Range>>;
|
||||
is_virtual_machine: Maybe<Ranking<boolean>>;
|
||||
linux_distro: Maybe<Ranking<string>>;
|
||||
linux_kernel: Maybe<Ranking<string>>;
|
||||
cpu_hashrate_score: Maybe<Ranking<Range>>;
|
||||
memory_memcpy_score: Maybe<Ranking<Range>>;
|
||||
disk_sequential_write_score: Maybe<Ranking<Range>>;
|
||||
disk_random_write_score: Maybe<Ranking<Range>>;
|
||||
};
|
||||
|
||||
@@ -20,13 +20,13 @@ import { Types, Maybe } from '../../common';
|
||||
import { State as AppState, Update as AppUpdate } from '../../state';
|
||||
import { getHashData } from '../../utils';
|
||||
import { Header } from './';
|
||||
import { List, Map, Settings } from '../';
|
||||
import { List, Map, Settings, Stats } from '../';
|
||||
import { Persistent, PersistentObject, PersistentSet } from '../../persist';
|
||||
|
||||
import './Chain.css';
|
||||
|
||||
export namespace Chain {
|
||||
export type Display = 'list' | 'map' | 'settings' | 'consensus';
|
||||
export type Display = 'list' | 'map' | 'settings' | 'consensus' | 'stats';
|
||||
|
||||
export interface Props {
|
||||
appState: Readonly<AppState>;
|
||||
@@ -93,16 +93,26 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
|
||||
const { appState, appUpdate, connection, pins, sortBy } = this.props;
|
||||
|
||||
return display === 'list' ? (
|
||||
<List
|
||||
appState={appState}
|
||||
appUpdate={appUpdate}
|
||||
pins={pins}
|
||||
sortBy={sortBy}
|
||||
/>
|
||||
) : (
|
||||
<Map appState={appState} />
|
||||
);
|
||||
if (display === 'list') {
|
||||
return (
|
||||
<List
|
||||
appState={appState}
|
||||
appUpdate={appUpdate}
|
||||
pins={pins}
|
||||
sortBy={sortBy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (display === 'map') {
|
||||
return <Map appState={appState} />;
|
||||
}
|
||||
|
||||
if (display === 'stats') {
|
||||
return <Stats appState={appState} />;
|
||||
}
|
||||
|
||||
throw new Error('invalid `display`: ${display}');
|
||||
}
|
||||
|
||||
private setDisplay = (display: Chain.Display) => {
|
||||
|
||||
@@ -28,6 +28,7 @@ import listIcon from '../../icons/list-alt-regular.svg';
|
||||
import worldIcon from '../../icons/location.svg';
|
||||
import settingsIcon from '../../icons/settings.svg';
|
||||
import consensusIcon from '../../icons/cube-alt.svg';
|
||||
import statsIcon from '../../icons/graph.svg';
|
||||
|
||||
import './Header.css';
|
||||
|
||||
@@ -90,6 +91,14 @@ export class Header extends React.Component<Header.Props, {}> {
|
||||
current={currentTab}
|
||||
setDisplay={setDisplay}
|
||||
/>
|
||||
<Tab
|
||||
icon={statsIcon}
|
||||
label="Stats"
|
||||
display="stats"
|
||||
tab="stats"
|
||||
current={currentTab}
|
||||
setDisplay={setDisplay}
|
||||
/>
|
||||
<Tab
|
||||
icon={settingsIcon}
|
||||
label="Settings"
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
Source code for the Substrate Telemetry Server.
|
||||
Copyright (C) 2022 Parity Technologies (UK) Ltd.
|
||||
|
||||
This program 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.
|
||||
|
||||
This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.Stats {
|
||||
text-align: center;
|
||||
padding-top: 2.5rem;
|
||||
padding-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
.Stats-category {
|
||||
text-align: left;
|
||||
background-color: #fff;
|
||||
margin-bottom: 2.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.Stats-category table {
|
||||
color: #000;
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.Stats-category tr:nth-child(even) {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.Stats-percent {
|
||||
width: 6em;
|
||||
text-align: right;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.Stats-count {
|
||||
width: 6.5em;
|
||||
text-align: right;
|
||||
padding-right: 1.5rem;
|
||||
border-right: 1px solid black;
|
||||
}
|
||||
|
||||
.Stats-value {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
th.Stats-value {
|
||||
padding-left: 1rem;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.Stats-category td {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.Stats-unknown {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// Source code for the Substrate Telemetry Server.
|
||||
// Copyright (C) 2022 Parity Technologies (UK) Ltd.
|
||||
//
|
||||
// This program 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.
|
||||
//
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import * as React from 'react';
|
||||
import { Maybe } from '../../common';
|
||||
import { State as AppState } from '../../state';
|
||||
import { Row } from '../List';
|
||||
import { PersistentObject } from '../../persist';
|
||||
import { Ranking, Range } from '../../common/types';
|
||||
|
||||
import './Stats.css';
|
||||
|
||||
export namespace Stats {
|
||||
export type Display = 'list' | 'map' | 'Stats';
|
||||
|
||||
export interface Props {
|
||||
appState: Readonly<AppState>;
|
||||
}
|
||||
}
|
||||
|
||||
function displayPercentage(percent: number): string {
|
||||
return (Math.round(percent * 100) / 100).toFixed(2);
|
||||
}
|
||||
|
||||
function generateRankingTable<T>(
|
||||
key: string,
|
||||
label: string,
|
||||
format: (value: T) => string,
|
||||
ranking: Ranking<T>
|
||||
) {
|
||||
let total = ranking.other + ranking.unknown;
|
||||
ranking.list.forEach(([_, count]) => {
|
||||
total += count;
|
||||
});
|
||||
|
||||
if (ranking.unknown === total) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entries: React.ReactNode[] = [];
|
||||
ranking.list.forEach(([value, count]) => {
|
||||
const percent = displayPercentage((count / total) * 100);
|
||||
const index = entries.length;
|
||||
entries.push(
|
||||
<tr key={index}>
|
||||
<td className="Stats-percent">{percent}%</td>
|
||||
<td className="Stats-count">{count}</td>
|
||||
<td className="Stats-value">{format(value)}</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
||||
if (ranking.other > 0) {
|
||||
const percent = displayPercentage((ranking.other / total) * 100);
|
||||
entries.push(
|
||||
<tr key="other">
|
||||
<td className="Stats-percent">{percent}%</td>
|
||||
<td className="Stats-count">{ranking.other}</td>
|
||||
<td className="Stats-value">Other</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
if (ranking.unknown > 0) {
|
||||
const percent = displayPercentage((ranking.unknown / total) * 100);
|
||||
entries.push(
|
||||
<tr key="unknown">
|
||||
<td className="Stats-percent">{percent}%</td>
|
||||
<td className="Stats-count">{ranking.unknown}</td>
|
||||
<td className="Stats-value Stats-unknown">Unknown</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="Stats-category" key={key}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="Stats-percent" />
|
||||
<th className="Stats-count" />
|
||||
<th className="Stats-value">{label}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{entries}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function identity(value: string | number): string {
|
||||
return value + '';
|
||||
}
|
||||
|
||||
function formatMemory(value: Range): string {
|
||||
const [min, max] = value;
|
||||
if (min === 0) {
|
||||
return 'Less than ' + max + ' GB';
|
||||
}
|
||||
if (max === null) {
|
||||
return 'At least ' + min + ' GB';
|
||||
}
|
||||
return min + ' GB';
|
||||
}
|
||||
|
||||
function formatYesNo(value: boolean): string {
|
||||
if (value) {
|
||||
return 'Yes';
|
||||
} else {
|
||||
return 'No';
|
||||
}
|
||||
}
|
||||
|
||||
function formatScore(value: Range): string {
|
||||
const [min, max] = value;
|
||||
if (max === null) {
|
||||
return 'More than ' + (min / 100).toFixed(1) + 'x';
|
||||
}
|
||||
if (min === 0) {
|
||||
return 'Less than ' + (max / 100).toFixed(1) + 'x';
|
||||
}
|
||||
if (min <= 100 && max >= 100) {
|
||||
return 'Baseline';
|
||||
}
|
||||
return (min / 100).toFixed(1) + 'x';
|
||||
}
|
||||
|
||||
export class Stats extends React.Component<Stats.Props, {}> {
|
||||
public render() {
|
||||
const { appState } = this.props;
|
||||
|
||||
const children: React.ReactNode[] = [];
|
||||
function add<T>(
|
||||
key: string,
|
||||
label: string,
|
||||
format: (value: T) => string,
|
||||
ranking: Maybe<Ranking<T>>
|
||||
) {
|
||||
if (ranking) {
|
||||
const child = generateRankingTable(key, label, format, ranking);
|
||||
if (child !== null) {
|
||||
children.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const stats = appState.chainStats;
|
||||
if (stats) {
|
||||
add('version', 'Version', identity, stats.version);
|
||||
add('target_os', 'Operating System', identity, stats.target_os);
|
||||
add('target_arch', 'CPU Architecture', identity, stats.target_arch);
|
||||
add('cpu', 'CPU', identity, stats.cpu);
|
||||
add('core_count', 'CPU Cores', identity, stats.core_count);
|
||||
add('memory', 'Memory', formatMemory, stats.memory);
|
||||
add(
|
||||
'is_virtual_machine',
|
||||
'Is Virtual Machine?',
|
||||
formatYesNo,
|
||||
stats.is_virtual_machine
|
||||
);
|
||||
add('linux_distro', 'Linux Distribution', identity, stats.linux_distro);
|
||||
add('linux_kernel', 'Linux Kernel', identity, stats.linux_kernel);
|
||||
add(
|
||||
'cpu_hashrate_score',
|
||||
'CPU Speed',
|
||||
formatScore,
|
||||
stats.cpu_hashrate_score
|
||||
);
|
||||
add(
|
||||
'memory_memcpy_score',
|
||||
'Memory Speed',
|
||||
formatScore,
|
||||
stats.memory_memcpy_score
|
||||
);
|
||||
add(
|
||||
'disk_sequential_write_score',
|
||||
'Disk Speed (sequential writes)',
|
||||
formatScore,
|
||||
stats.disk_sequential_write_score
|
||||
);
|
||||
add(
|
||||
'disk_random_write_score',
|
||||
'Disk Speed (random writes)',
|
||||
formatScore,
|
||||
stats.disk_random_write_score
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="Stats">{children}</div>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Source code for the Substrate Telemetry Server.
|
||||
// Copyright (C) 2022 Parity Technologies (UK) Ltd.
|
||||
//
|
||||
// This program 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.
|
||||
//
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
export * from './Stats';
|
||||
@@ -20,6 +20,7 @@ export * from './Chain';
|
||||
export * from './List';
|
||||
export * from './Map';
|
||||
export * from './Settings';
|
||||
export * from './Stats';
|
||||
export * from './Icon';
|
||||
export * from './Tile';
|
||||
export * from './Ago';
|
||||
|
||||
@@ -279,6 +279,7 @@ export interface State {
|
||||
pins: Readonly<Set<Types.NodeName>>;
|
||||
sortBy: Readonly<Maybe<number>>;
|
||||
selectedColumns: Column[];
|
||||
chainStats: Maybe<Types.ChainStats>;
|
||||
}
|
||||
|
||||
export type Update = <K extends keyof State>(
|
||||
|
||||
Reference in New Issue
Block a user