diff --git a/backend/src/aggregator.rs b/backend/src/aggregator.rs index e222279..56a7b45 100644 --- a/backend/src/aggregator.rs +++ b/backend/src/aggregator.rs @@ -213,7 +213,7 @@ impl Handler for Aggregator { connector.do_send(Connected(fid)); - self.serializer.push(feed::Version(28)); + self.serializer.push(feed::Version(29)); // TODO: keep track on number of nodes connected to each chain for (_, entry) in self.chains.iter() { diff --git a/backend/src/chain.rs b/backend/src/chain.rs index b2d03b8..425ac5f 100644 --- a/backend/src/chain.rs +++ b/backend/src/chain.rs @@ -273,6 +273,10 @@ impl Handler for Chain { if let Some(stats) = node.update_stats(interval) { self.serializer.push(feed::NodeStatsUpdate(nid, stats)); } + + if let Some(io) = node.update_io(interval) { + self.serializer.push(feed::NodeIOUpdate(nid, io)); + } } Details::SystemNetworkState(_) => { if let Some(raw) = raw { diff --git a/backend/src/feed.rs b/backend/src/feed.rs index c34a179..f742e19 100644 --- a/backend/src/feed.rs +++ b/backend/src/feed.rs @@ -4,7 +4,7 @@ use serde::ser::{Serializer, SerializeTuple}; use serde_json::to_writer; use crate::node::Node; use crate::types::{ - NodeId, NodeStats, NodeHardware, BlockNumber, BlockHash, BlockDetails, Timestamp, Address, + NodeId, NodeStats, NodeHardware, NodeIO, BlockNumber, BlockHash, BlockDetails, Timestamp, Address, }; pub mod connector; @@ -91,6 +91,7 @@ actions! { 0x12: AfgReceivedPrecommit, 0x13: AfgAuthoritySet, 0x14: StaleNode, + 0x15: NodeIOUpdate<'_>, } #[derive(Serialize)] @@ -119,6 +120,9 @@ pub struct FinalizedBlock(pub NodeId, pub BlockNumber, pub BlockHash); #[derive(Serialize)] pub struct NodeStatsUpdate<'a>(pub NodeId, pub &'a NodeStats); +#[derive(Serialize)] +pub struct NodeIOUpdate<'a>(pub NodeId, pub &'a NodeIO); + #[derive(Serialize)] pub struct Hardware<'a>(pub NodeId, pub &'a NodeHardware); @@ -161,10 +165,11 @@ impl Serialize for AddedNode<'_> { S: Serializer, { let AddedNode(nid, node) = self; - let mut tup = serializer.serialize_tuple(7)?; + let mut tup = serializer.serialize_tuple(8)?; tup.serialize_element(nid)?; tup.serialize_element(node.details())?; tup.serialize_element(node.stats())?; + tup.serialize_element(node.io())?; tup.serialize_element(node.hardware())?; tup.serialize_element(node.block_details())?; tup.serialize_element(&node.location())?; diff --git a/backend/src/node.rs b/backend/src/node.rs index 95f0267..c269b7f 100644 --- a/backend/src/node.rs +++ b/backend/src/node.rs @@ -1,7 +1,7 @@ use bytes::Bytes; use std::sync::Arc; -use crate::types::{NodeId, NodeDetails, NodeStats, NodeHardware, NodeLocation, BlockDetails, Block, Timestamp}; +use crate::types::{NodeId, NodeDetails, NodeStats, NodeIO, NodeHardware, NodeLocation, BlockDetails, Block, Timestamp}; use crate::util::now; pub mod message; @@ -19,6 +19,8 @@ pub struct Node { details: NodeDetails, /// Basic stats stats: NodeStats, + /// Node IO stats + io: NodeIO, /// Best block best: BlockDetails, /// Finalized block @@ -42,16 +44,9 @@ impl Node { Node { details, - stats: NodeStats { - txcount: 0, - peers: 0, - }, - best: BlockDetails { - block: Block::zero(), - block_timestamp: now(), - block_time: 0, - propagation_time: None, - }, + stats: NodeStats::default(), + io: NodeIO::default(), + best: BlockDetails::default(), finalized: Block::zero(), throttle: 0, hardware: NodeHardware::default(), @@ -70,6 +65,10 @@ impl Node { &self.stats } + pub fn io(&self) -> &NodeIO { + &self.io + } + pub fn best(&self) -> &Block { &self.best.block } @@ -105,7 +104,7 @@ impl Node { if block.height > self.best.block.height { self.stale = false; self.best.block = block; - + true } else { false @@ -157,6 +156,29 @@ impl Node { } } + pub fn update_io(&mut self, interval: &SystemInterval) -> Option<&NodeIO> { + let mut changed = false; + + if let Some(size) = interval.used_state_cache_size { + changed |= self.io.used_state_cache_size.push(size); + } + if let Some(size) = interval.used_db_cache_size { + changed |= self.io.used_db_cache_size.push(size); + } + if let Some(bps) = interval.disk_read_per_sec { + changed |= self.io.disk_read_per_sec.push(bps); + } + if let Some(bps) = interval.disk_write_per_sec { + changed |= self.io.disk_write_per_sec.push(bps); + } + + if changed { + Some(&self.io) + } else { + None + } + } + pub fn update_finalized(&mut self, block: Block) -> Option<&Block> { if block.height > self.finalized.height { self.finalized = block; diff --git a/backend/src/node/message.rs b/backend/src/node/message.rs index 25ae4cd..d1f3989 100644 --- a/backend/src/node/message.rs +++ b/backend/src/node/message.rs @@ -74,6 +74,10 @@ pub struct SystemInterval { #[serde(flatten)] pub block: Block, pub network_state: Option, + pub used_state_cache_size: Option, + pub used_db_cache_size: Option, + pub disk_read_per_sec: Option, + pub disk_write_per_sec: Option, } #[derive(Deserialize, Debug)] diff --git a/backend/src/types.rs b/backend/src/types.rs index ee4f4a7..a4c11b6 100644 --- a/backend/src/types.rs +++ b/backend/src/types.rs @@ -1,7 +1,7 @@ use serde::ser::{Serialize, Serializer, SerializeTuple}; use serde::Deserialize; -use crate::util::MeanList; +use crate::util::{MeanList, now}; pub type NodeId = usize; pub type BlockNumber = u64; @@ -18,12 +18,20 @@ pub struct NodeDetails { pub network_id: Option>, } -#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct NodeStats { pub peers: u64, pub txcount: u64, } +#[derive(Default)] +pub struct NodeIO { + pub used_state_cache_size: MeanList, + pub used_db_cache_size: MeanList, + pub disk_read_per_sec: MeanList, + pub disk_write_per_sec: MeanList, +} + #[derive(Deserialize, Debug, Clone, Copy)] pub struct Block { #[serde(rename = "best")] @@ -39,6 +47,17 @@ pub struct BlockDetails { pub propagation_time: Option, } +impl Default for BlockDetails { + fn default() -> Self { + BlockDetails { + block: Block::zero(), + block_timestamp: now(), + block_time: 0, + propagation_time: None, + } + } +} + #[derive(Default)] pub struct NodeHardware { /// CPU use means @@ -87,6 +106,20 @@ impl Serialize for NodeStats { } } +impl Serialize for NodeIO { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut tup = serializer.serialize_tuple(4)?; + tup.serialize_element(self.used_state_cache_size.slice())?; + tup.serialize_element(self.used_db_cache_size.slice())?; + tup.serialize_element(self.disk_read_per_sec.slice())?; + tup.serialize_element(self.disk_write_per_sec.slice())?; + tup.end() + } +} + impl Serialize for BlockDetails { fn serialize(&self, serializer: S) -> Result where diff --git a/packages/common/src/feed.ts b/packages/common/src/feed.ts index 3be9fdb..a2f7f52 100644 --- a/packages/common/src/feed.ts +++ b/packages/common/src/feed.ts @@ -10,6 +10,7 @@ import { NodeCount, NodeDetails, NodeStats, + NodeIO, NodeHardware, NodeLocation, BlockNumber, @@ -43,6 +44,7 @@ export const Actions = { AfgReceivedPrecommit : 0x12 as 0x12, AfgAuthoritySet : 0x13 as 0x13, StaleNode : 0x14 as 0x14, + NodeIO : 0x15 as 0x15, }; export type Action = typeof Actions[keyof typeof Actions]; @@ -70,7 +72,7 @@ export namespace Variants { export interface AddedNodeMessage extends MessageBase { action: typeof Actions.AddedNode; - payload: [NodeId, NodeDetails, NodeStats, NodeHardware, BlockDetails, Maybe, Timestamp]; + payload: [NodeId, NodeDetails, NodeStats, NodeIO, NodeHardware, BlockDetails, Maybe, Timestamp]; } export interface RemovedNodeMessage extends MessageBase { @@ -103,6 +105,11 @@ export namespace Variants { payload: [NodeId, NodeHardware]; } + export interface NodeIOMessage extends MessageBase { + action: typeof Actions.NodeIO; + payload: [NodeId, NodeIO]; + } + export interface TimeSyncMessage extends MessageBase { action: typeof Actions.TimeSync; payload: Timestamp; diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 913a3f8..4bd1880 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -9,4 +9,4 @@ import * as FeedMessage from './feed'; export { Types, FeedMessage }; // Increment this if breaking changes were made to types in `feed.ts` -export const VERSION: Types.FeedVersion = 28 as Types.FeedVersion; +export const VERSION: Types.FeedVersion = 29 as Types.FeedVersion; diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index d2ed473..35843fc 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -22,6 +22,7 @@ export type Longitude = Opaque; export type City = Opaque; export type MemoryUse = Opaque; export type CPUUse = Opaque; +export type Bytes = Opaque; export type BytesPerSecond = Opaque; export type NetworkId = Opaque; export type NetworkState = Opaque; @@ -29,6 +30,7 @@ export type NetworkState = Opaque; export type BlockDetails = [BlockNumber, BlockHash, Milliseconds, Timestamp, Maybe]; export type NodeDetails = [NodeName, NodeImplementation, NodeVersion, Maybe
, Maybe]; export type NodeStats = [PeerCount, TransactionCount]; +export type NodeIO = [Array, Array, Array, Array]; export type NodeHardware = [Array, Array, Array, Array, Array]; export type NodeLocation = [Latitude, Longitude, City]; diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index b1cd830..b7dfcd2 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -34,6 +34,10 @@ export default class App extends React.Component<{}, State> { mem: true, upload: false, download: false, + stateCacheSize: false, + dbCacheSize: false, + diskRead: false, + diskWrite: false, blocknumber: true, blockhash: true, blocktime: true, diff --git a/packages/frontend/src/Connection.ts b/packages/frontend/src/Connection.ts index 74bcaad..a6026a3 100644 --- a/packages/frontend/src/Connection.ts +++ b/packages/frontend/src/Connection.ts @@ -166,9 +166,9 @@ export class Connection { } case Actions.AddedNode: { - const [id, nodeDetails, nodeStats, nodeHardware, blockDetails, location, connectedAt] = message.payload; + const [id, nodeDetails, nodeStats, nodeIO, nodeHardware, blockDetails, location, connectedAt] = message.payload; const pinned = this.pins.has(nodeDetails[0]); - const node = new Node(pinned, id, nodeDetails, nodeStats, nodeHardware, blockDetails, location, connectedAt); + const node = new Node(pinned, id, nodeDetails, nodeStats, nodeIO, nodeHardware, blockDetails, location, connectedAt); nodes.add(node); diff --git a/packages/frontend/src/components/List/Column.tsx b/packages/frontend/src/components/List/Column.tsx index 3a0c129..b68c1d1 100644 --- a/packages/frontend/src/components/List/Column.tsx +++ b/packages/frontend/src/components/List/Column.tsx @@ -31,6 +31,10 @@ import cpuIcon from '../../icons/microchip-solid.svg'; import memoryIcon from '../../icons/memory-solid.svg'; import uploadIcon from '../../icons/cloud-upload.svg'; import downloadIcon from '../../icons/cloud-download.svg'; +import readIcon from '../../icons/arrow-up.svg'; +import writeIcon from '../../icons/arrow-down.svg'; +import databaseIcon from '../../icons/database.svg'; +import stateIcon from '../../icons/git-branch.svg'; import networkIcon from '../../icons/network.svg'; import uptimeIcon from '../../icons/pulse.svg'; import externalLinkIcon from '../../icons/link-external.svg'; @@ -210,6 +214,74 @@ export namespace Column { } }; + export const STATE_CACHE: Column = { + label: 'State Cache Size', + icon: stateIcon, + width: 40, + setting: 'stateCacheSize', + sortBy: ({ stateCacheSize }) => stateCacheSize.length < 3 ? 0 : stateCacheSize[stateCacheSize.length - 1], + render: ({ stateCacheSize, chartstamps }) => { + if (stateCacheSize.length < 3) { + return '-'; + } + + return ( + + ); + } + }; + + export const DB_CACHE: Column = { + label: 'Database Cache Size', + icon: databaseIcon, + width: 40, + setting: 'dbCacheSize', + sortBy: ({ dbCacheSize }) => dbCacheSize.length < 3 ? 0 : dbCacheSize[dbCacheSize.length - 1], + render: ({ dbCacheSize, chartstamps }) => { + if (dbCacheSize.length < 3) { + return '-'; + } + + return ( + + ); + } + }; + + export const DISK_READ: Column = { + label: 'Disk Read', + icon: readIcon, + width: 40, + setting: 'diskRead', + sortBy: ({ diskRead }) => diskRead.length < 3 ? 0 : diskRead[diskRead.length - 1], + render: ({ diskRead, chartstamps }) => { + if (diskRead.length < 3) { + return '-'; + } + + return ( + + ); + } + }; + + export const DISK_WRITE: Column = { + label: 'Disk Write', + icon: writeIcon, + width: 40, + setting: 'diskWrite', + sortBy: ({ diskWrite }) => diskWrite.length < 3 ? 0 : diskWrite[diskWrite.length - 1], + render: ({ diskWrite, chartstamps }) => { + if (diskWrite.length < 3) { + return '-'; + } + + return ( + + ); + } + }; + export const BLOCK_NUMBER: Column = { label: 'Block', icon: blockIcon, @@ -330,6 +402,18 @@ function formatMemory(kbs: number, stamp: Maybe): string { } } +function formatBytes(bytes: number, stamp: Maybe): string { + const ago = stamp ? ` (${formatStamp(stamp)})` : ''; + + if (bytes >= 1024 * 1024) { + return `${(bytes / (1024 * 1024)).toFixed(1)} MB${ago}`; + } else if (bytes >= 1000) { + return `${(bytes / 1024).toFixed(1)} kB${ago}`; + } else { + return `${bytes} B${ago}`; + } +} + function formatBandwidth(bps: number, stamp: Maybe): string { const ago = stamp ? ` (${formatStamp(stamp)})` : ''; diff --git a/packages/frontend/src/components/List/Row.tsx b/packages/frontend/src/components/List/Row.tsx index 8573b69..caf9e3a 100644 --- a/packages/frontend/src/components/List/Row.tsx +++ b/packages/frontend/src/components/List/Row.tsx @@ -36,6 +36,10 @@ export class Row extends React.Component { Column.MEM, Column.UPLOAD, Column.DOWNLOAD, + Column.STATE_CACHE, + Column.DB_CACHE, + Column.DISK_READ, + Column.DISK_WRITE, Column.BLOCK_NUMBER, Column.BLOCK_HASH, Column.FINALIZED, diff --git a/packages/frontend/src/state.ts b/packages/frontend/src/state.ts index 400c033..bb0f6c9 100644 --- a/packages/frontend/src/state.ts +++ b/packages/frontend/src/state.ts @@ -43,6 +43,10 @@ export class Node { public cpu: Types.CPUUse[]; public upload: Types.BytesPerSecond[]; public download: Types.BytesPerSecond[]; + public stateCacheSize: Types.Bytes[]; + public dbCacheSize: Types.Bytes[]; + public diskRead: Types.BytesPerSecond[]; + public diskWrite: Types.BytesPerSecond[]; public chartstamps: Types.Timestamp[]; public height: Types.BlockNumber; @@ -66,6 +70,7 @@ export class Node { id: Types.NodeId, nodeDetails: Types.NodeDetails, nodeStats: Types.NodeStats, + nodeIO: Types.NodeIO, nodeHardware: Types.NodeHardware, blockDetails: Types.BlockDetails, location: Maybe, @@ -89,6 +94,7 @@ export class Node { this.sortableVersion = (major * 1000 + minor * 100 + patch) | 0; this.updateStats(nodeStats); + this.updateIO(nodeIO); this.updateHardware(nodeHardware); this.updateBlock(blockDetails); @@ -106,6 +112,17 @@ export class Node { this.trigger(); } + public updateIO(io: Types.NodeIO) { + const [stateCacheSize, dbCacheSize, diskRead, diskWrite] = io; + + this.stateCacheSize = stateCacheSize; + this.dbCacheSize = dbCacheSize; + this.diskRead = diskRead; + this.diskWrite = diskWrite; + + this.trigger(); + } + public updateHardware(hardware: Types.NodeHardware) { const [mem, cpu, upload, download, chartstamps] = hardware; @@ -202,6 +219,10 @@ export namespace State { mem: boolean; upload: boolean; download: boolean; + stateCacheSize: boolean; + dbCacheSize: boolean; + diskRead: boolean; + diskWrite: boolean; blocknumber: boolean; blockhash: boolean; finalized: boolean;