Offline indicator, average block time and stuff

This commit is contained in:
maciejhirsz
2018-07-13 23:20:29 +02:00
parent 799da38a9b
commit ef3f52f5c8
16 changed files with 166 additions and 37 deletions
+2
View File
@@ -23,6 +23,8 @@ export default class Aggregator {
public addFeed(feed: Feed) { public addFeed(feed: Feed) {
this.feeds.add(feed); this.feeds.add(feed);
feed.sendMessage(Feed.feedVersion());
for (const chain of this.chains.values()) { for (const chain of this.chains.values()) {
feed.sendMessage(Feed.addedChain(chain)); feed.sendMessage(Feed.addedChain(chain));
} }
+33 -5
View File
@@ -2,7 +2,9 @@ import * as EventEmitter from 'events';
import Node from './Node'; import Node from './Node';
import Feed from './Feed'; import Feed from './Feed';
import FeedSet from './FeedSet'; import FeedSet from './FeedSet';
import { timestamp, Types, FeedMessage } from '@dotstats/common'; import { timestamp, Maybe, Types, FeedMessage } from '@dotstats/common';
const BLOCK_TIME_HISTORY = 10;
export default class Chain { export default class Chain {
private nodes = new Set<Node>(); private nodes = new Set<Node>();
@@ -14,6 +16,8 @@ export default class Chain {
public height = 0 as Types.BlockNumber; public height = 0 as Types.BlockNumber;
public blockTimestamp = 0 as Types.Timestamp; public blockTimestamp = 0 as Types.Timestamp;
private blockTimes: Array<number> = new Array(BLOCK_TIME_HISTORY);
constructor(label: Types.ChainLabel) { constructor(label: Types.ChainLabel) {
this.label = label; this.label = label;
} }
@@ -48,7 +52,7 @@ export default class Chain {
feed.chain = this.label; feed.chain = this.label;
feed.sendMessage(Feed.timeSync()); feed.sendMessage(Feed.timeSync());
feed.sendMessage(Feed.bestBlock(this.height, this.blockTimestamp)); feed.sendMessage(Feed.bestBlock(this.height, this.blockTimestamp, this.averageBlockTime));
for (const node of this.nodes.values()) { for (const node of this.nodes.values()) {
feed.sendMessage(Feed.addedNode(node)); feed.sendMessage(Feed.addedNode(node));
@@ -75,11 +79,17 @@ export default class Chain {
private updateBlock(node: Node) { private updateBlock(node: Node) {
if (node.height > this.height) { if (node.height > this.height) {
this.height = node.height; const { height, blockTimestamp } = node;
this.blockTimestamp = node.blockTimestamp;
if (this.blockTimestamp) {
this.blockTimes[height * BLOCK_TIME_HISTORY] = blockTimestamp - this.blockTimestamp;
}
this.height = height;
this.blockTimestamp = blockTimestamp;
node.propagationTime = 0 as Types.PropagationTime; node.propagationTime = 0 as Types.PropagationTime;
this.feeds.broadcast(Feed.bestBlock(this.height, this.blockTimestamp)); this.feeds.broadcast(Feed.bestBlock(this.height, this.blockTimestamp, this.averageBlockTime));
console.log(`[${this.label}] New block ${this.height}`); console.log(`[${this.label}] New block ${this.height}`);
} else if (node.height === this.height) { } else if (node.height === this.height) {
@@ -90,4 +100,22 @@ export default class Chain {
console.log(`[${this.label}] ${node.name} imported ${node.height}, block time: ${node.blockTime / 1000}s, average: ${node.average / 1000}s | latency ${node.latency}`); console.log(`[${this.label}] ${node.name} imported ${node.height}, block time: ${node.blockTime / 1000}s, average: ${node.average / 1000}s | latency ${node.latency}`);
} }
private get averageBlockTime(): Maybe<Types.Milliseconds> {
let sum = 0;
let count = 0;
for (const time of this.blockTimes) {
if (time != null) {
sum += time;
count += 1;
}
}
if (count === 0) {
return null;
}
return (sum / count) as Types.Milliseconds;
}
} }
+10 -3
View File
@@ -2,7 +2,7 @@ import * as WebSocket from 'ws';
import * as EventEmitter from 'events'; import * as EventEmitter from 'events';
import Node from './Node'; import Node from './Node';
import Chain from './Chain'; import Chain from './Chain';
import { timestamp, Maybe, FeedMessage, Types, idGenerator } from '@dotstats/common'; import { VERSION, timestamp, Maybe, FeedMessage, Types, idGenerator } from '@dotstats/common';
const nextId = idGenerator<Types.FeedId>(); const nextId = idGenerator<Types.FeedId>();
const { Actions } = FeedMessage; const { Actions } = FeedMessage;
@@ -25,10 +25,17 @@ export default class Feed {
socket.on('close', () => this.disconnect()); socket.on('close', () => this.disconnect());
} }
public static bestBlock(height: Types.BlockNumber, ts: Types.Timestamp): FeedMessage.Message { public static feedVersion(): FeedMessage.Message {
return {
action: Actions.FeedVersion,
payload: VERSION
};
}
public static bestBlock(height: Types.BlockNumber, ts: Types.Timestamp, avg: Maybe<Types.Milliseconds>): FeedMessage.Message {
return { return {
action: Actions.BestBlock, action: Actions.BestBlock,
payload: [height, ts] payload: [height, ts, avg]
}; };
} }
+11 -2
View File
@@ -1,5 +1,6 @@
import { Opaque } from './helpers'; import { Opaque, Maybe } from './helpers';
import { import {
FeedVersion,
NodeId, NodeId,
NodeCount, NodeCount,
NodeDetails, NodeDetails,
@@ -7,10 +8,12 @@ import {
BlockNumber, BlockNumber,
BlockDetails, BlockDetails,
Timestamp, Timestamp,
Milliseconds,
ChainLabel ChainLabel
} from './types'; } from './types';
export const Actions = { export const Actions = {
FeedVersion: 255 as 255,
BestBlock: 0 as 0, BestBlock: 0 as 0,
AddedNode: 1 as 1, AddedNode: 1 as 1,
RemovedNode: 2 as 2, RemovedNode: 2 as 2,
@@ -31,9 +34,14 @@ export namespace Variants {
action: Action; action: Action;
} }
export interface FeedVersionMessage extends MessageBase {
action: typeof Actions.FeedVersion;
payload: FeedVersion;
}
export interface BestBlockMessage extends MessageBase { export interface BestBlockMessage extends MessageBase {
action: typeof Actions.BestBlock; action: typeof Actions.BestBlock;
payload: [BlockNumber, Timestamp]; payload: [BlockNumber, Timestamp, Maybe<Milliseconds>];
} }
export interface AddedNodeMessage extends MessageBase { export interface AddedNodeMessage extends MessageBase {
@@ -83,6 +91,7 @@ export namespace Variants {
} }
export type Message = export type Message =
| Variants.FeedVersionMessage
| Variants.BestBlockMessage | Variants.BestBlockMessage
| Variants.AddedNodeMessage | Variants.AddedNodeMessage
| Variants.RemovedNodeMessage | Variants.RemovedNodeMessage
+2
View File
@@ -6,3 +6,5 @@ import * as Types from './types';
import * as FeedMessage from './feed'; import * as FeedMessage from './feed';
export { Types, FeedMessage }; export { Types, FeedMessage };
export const VERSION: Types.FeedVersion = 1 as Types.FeedVersion;
+1
View File
@@ -1,6 +1,7 @@
import { Opaque, Maybe } from './helpers'; import { Opaque, Maybe } from './helpers';
import { Id } from './id'; import { Id } from './id';
export type FeedVersion = Opaque<number, 'FeedVersion'>;
export type ChainLabel = Opaque<string, 'ChainLabel'>; export type ChainLabel = Opaque<string, 'ChainLabel'>;
export type FeedId = Id<'Feed'>; export type FeedId = Id<'Feed'>;
export type NodeId = Id<'Node'>; export type NodeId = Id<'Node'>;
+2 -1
View File
@@ -1,13 +1,14 @@
.App { .App {
text-align: left; text-align: left;
font-family: Roboto, Helvetica, Arial, sans-serif; font-family: Roboto, Helvetica, Arial, sans-serif;
font-size: 16px;
} }
.App-no-telemetry { .App-no-telemetry {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
line-height: 80vh; line-height: 80vh;
font-size: 3.5em; font-size: 56px;
font-weight: 100; font-weight: 100;
text-align: center; text-align: center;
color: #888; color: #888;
+11 -3
View File
@@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { Types } from '@dotstats/common'; import { Types } from '@dotstats/common';
import { Chains, Chain, Ago } from './components'; import { Chains, Chain, Ago, OfflineIndicator } from './components';
import { Connection } from './Connection'; import { Connection } from './Connection';
import { State } from './state'; import { State } from './state';
@@ -8,8 +8,10 @@ import './App.css';
export default class App extends React.Component<{}, State> { export default class App extends React.Component<{}, State> {
public state: State = { public state: State = {
status: 'offline',
best: 0 as Types.BlockNumber, best: 0 as Types.BlockNumber,
blockTimestamp: 0 as Types.Timestamp, blockTimestamp: 0 as Types.Timestamp,
blockAverage: null,
timeDiff: 0 as Types.Milliseconds, timeDiff: 0 as Types.Milliseconds,
subscribed: null, subscribed: null,
chains: new Map(), chains: new Map(),
@@ -31,16 +33,22 @@ export default class App extends React.Component<{}, State> {
} }
public render() { public render() {
const { chains, timeDiff, subscribed } = this.state; const { chains, timeDiff, subscribed, status } = this.state;
Ago.timeDiff = timeDiff; Ago.timeDiff = timeDiff;
if (chains.size === 0) { if (chains.size === 0) {
return <div className="App App-no-telemetry">Waiting for telemetry data...</div>; return (
<div className="App App-no-telemetry">
<OfflineIndicator status={status} />
Waiting for telemetry data...
</div>
);
} }
return ( return (
<div className="App"> <div className="App">
<OfflineIndicator status={status} />
<Chains chains={chains} subscribed={subscribed} connection={this.connection} /> <Chains chains={chains} subscribed={subscribed} connection={this.connection} />
<Chain state={this.state} /> <Chain state={this.state} />
</div> </div>
+23 -5
View File
@@ -1,4 +1,4 @@
import { timestamp, FeedMessage, Types, Maybe, sleep } from '@dotstats/common'; import { VERSION, timestamp, FeedMessage, Types, Maybe, sleep } from '@dotstats/common';
import { State, Update } from './state'; import { State, Update } from './state';
const { Actions } = FeedMessage; const { Actions } = FeedMessage;
@@ -68,7 +68,10 @@ export class Connection {
} }
private bindSocket() { private bindSocket() {
this.state = this.update({ nodes: new Map() }); this.state = this.update({
status: 'online',
nodes: new Map()
});
this.socket.addEventListener('message', this.handleMessages); this.socket.addEventListener('message', this.handleMessages);
this.socket.addEventListener('close', this.handleDisconnect); this.socket.addEventListener('close', this.handleDisconnect);
this.socket.addEventListener('error', this.handleDisconnect); this.socket.addEventListener('error', this.handleDisconnect);
@@ -88,10 +91,24 @@ export class Connection {
messages: for (const message of FeedMessage.deserialize(data)) { messages: for (const message of FeedMessage.deserialize(data)) {
switch (message.action) { switch (message.action) {
case Actions.BestBlock: { case Actions.FeedVersion: {
const [best, blockTimestamp] = message.payload; if (message.payload !== VERSION) {
this.state = this.update({ status: 'upgrade-requested' });
this.clean();
this.state = this.update({ best, blockTimestamp }); // Force reload from the server
setTimeout(() => window.location.reload(true), 3000);
return;
}
continue messages;
}
case Actions.BestBlock: {
const [best, blockTimestamp, blockAverage] = message.payload;
this.state = this.update({ best, blockTimestamp, blockAverage });
continue messages; continue messages;
} }
@@ -215,6 +232,7 @@ export class Connection {
} }
private handleDisconnect = async () => { private handleDisconnect = async () => {
this.state = this.update({ status: 'offline' });
this.clean(); this.clean();
this.socket.close(); this.socket.close();
this.socket = await Connection.socket(); this.socket = await Connection.socket();
+8 -1
View File
@@ -25,11 +25,17 @@ function sortNodes(a: Node.Props, b: Node.Props): number {
const aPropagation = a.blockDetails[4] == null ? Infinity : a.blockDetails[4] as number; const aPropagation = a.blockDetails[4] == null ? Infinity : a.blockDetails[4] as number;
const bPropagation = b.blockDetails[4] == null ? Infinity : b.blockDetails[4] as number; const bPropagation = b.blockDetails[4] == null ? Infinity : b.blockDetails[4] as number;
if (aPropagation === bPropagation) {
// Descending sort by block number
return b.blockDetails[0] - a.blockDetails[0];
}
// Ascending sort by propagation time
return aPropagation - bPropagation; return aPropagation - bPropagation;
} }
export function Chain(props: Chain.Props) { export function Chain(props: Chain.Props) {
const { best, blockTimestamp } = props.state; const { best, blockTimestamp, blockAverage } = props.state;
const nodes = Array.from(props.state.nodes.values()).sort(sortNodes); const nodes = Array.from(props.state.nodes.values()).sort(sortNodes);
@@ -37,6 +43,7 @@ export function Chain(props: Chain.Props) {
<div className="Chain"> <div className="Chain">
<div className="Chain-header"> <div className="Chain-header">
<Tile icon={blockIcon} title="Best Block">#{formatNumber(best)}</Tile> <Tile icon={blockIcon} title="Best Block">#{formatNumber(best)}</Tile>
<Tile icon={blockTimeIcon} title="Avgerage Time">{ blockAverage == null ? '-' : (blockAverage / 1000).toFixed(3) + 's' }</Tile>
<Tile icon={lastTimeIcon} title="Last Block"><Ago when={blockTimestamp} /></Tile> <Tile icon={lastTimeIcon} title="Last Block"><Ago when={blockTimestamp} /></Tile>
</div> </div>
<div className="Chain-content"> <div className="Chain-content">
+2 -3
View File
@@ -23,7 +23,7 @@ export class Chains extends React.Component<Chains.Props, {}> {
public render() { public render() {
return ( return (
<div className="Chains"> <div className="Chains">
<Icon src={chainIcon} alt="Observed chain" /> <Icon src={chainIcon} alt="Observed Chain" />
{ {
this.chains.map((chain) => this.renderChain(chain)) this.chains.map((chain) => this.renderChain(chain))
} }
@@ -38,10 +38,9 @@ export class Chains extends React.Component<Chains.Props, {}> {
? 'Chains-chain Chains-chain-selected' ? 'Chains-chain Chains-chain-selected'
: 'Chains-chain'; : 'Chains-chain';
return ( return (
<a key={label} className={className} onClick={this.subscribe.bind(this, label)}> <a key={label} className={className} onClick={this.subscribe.bind(this, label)}>
{label} <span className="Chains-node-count">{nodeCount}</span> {label} <span className="Chains-node-count" title="Node Count">{nodeCount}</span>
</a> </a>
) )
} }
+8 -8
View File
@@ -20,14 +20,14 @@ export function Node(props: Node.Props) {
return ( return (
<tr> <tr>
<td>{name}</td> <td>{name}</td>
<td>{implementation} v{version}</td> <td style={{ width: 240 }}>{implementation} v{version}</td>
<td>{peers}</td> <td style={{ width: 26 }}>{peers}</td>
<td>{txcount}</td> <td style={{ width: 26 }}>{txcount}</td>
<td>#{formatNumber(height)}</td> <td style={{ width: 88 }}>#{formatNumber(height)}</td>
<td><span title={hash}>{trimHash(hash, 16)}</span></td> <td style={{ width: 154 }}><span title={hash}>{trimHash(hash, 16)}</span></td>
<td>{(blockTime / 1000).toFixed(3)}s</td> <td style={{ width: 80 }}>{(blockTime / 1000).toFixed(3)}s</td>
<td>{propagationTime === null ? '∞' : `${propagationTime}ms`}</td> <td style={{ width: 58 }}>{propagationTime === null ? '∞' : `${propagationTime}ms`}</td>
<td><Ago when={blockTimestamp} /></td> <td style={{ width: 82 }}><Ago when={blockTimestamp} /></td>
</tr> </tr>
); );
} }
@@ -0,0 +1,17 @@
.OfflineIndicator {
position: absolute;
top: 30px;
right: 30px;
z-index: 10;
background: #c00;
line-height: 16px;
color: #fff;
font-size: 30px;
padding: 16px;
border-radius: 50px;
box-shadow: rgba(0,0,0,0.5) 0 3px 20px;
}
.OfflineIndicator-upgrade {
background: #282;
}
@@ -0,0 +1,27 @@
import * as React from 'react';
import './OfflineIndicator.css';
import { Icon } from './Icon';
import { State } from '../state';
import offlineIcon from '../icons/zap.svg';
import upgradeIcon from '../icons/flame.svg';
export namespace OfflineIndicator {
export interface Props {
status: State["status"];
}
}
export function OfflineIndicator(props: OfflineIndicator.Props): React.ReactElement<any> | null {
switch (props.status) {
case 'online':
return null;
case 'offline':
return <div className="OfflineIndicator"><Icon src={offlineIcon} alt="Offline" /></div>;
case 'upgrade-requested':
return (
<div className="OfflineIndicator OfflineIndicator-upgrade">
<Icon src={upgradeIcon} alt="New Version Available" />
</div>
);
}
}
@@ -4,3 +4,4 @@ export * from './Icon';
export * from './Node'; export * from './Node';
export * from './Tile'; export * from './Tile';
export * from './Ago'; export * from './Ago';
export * from './OfflineIndicator';
+8 -6
View File
@@ -2,12 +2,14 @@ import { Node } from './components/Node';
import { Types, Maybe } from '@dotstats/common'; import { Types, Maybe } from '@dotstats/common';
export interface State { export interface State {
best: Types.BlockNumber, status: 'online' | 'offline' | 'upgrade-requested';
blockTimestamp: Types.Timestamp, best: Types.BlockNumber;
timeDiff: Types.Milliseconds, blockTimestamp: Types.Timestamp;
subscribed: Maybe<Types.ChainLabel>, blockAverage: Maybe<Types.Milliseconds>;
chains: Map<Types.ChainLabel, Types.NodeCount>, timeDiff: Types.Milliseconds;
nodes: Map<Types.NodeId, Node.Props>, subscribed: Maybe<Types.ChainLabel>;
chains: Map<Types.ChainLabel, Types.NodeCount>;
nodes: Map<Types.NodeId, Node.Props>;
} }
export type Update = <K extends keyof State>(changes: Pick<State, K> | null) => Readonly<State>; export type Update = <K extends keyof State>(changes: Pick<State, K> | null) => Readonly<State>;